├── .build.yml ├── .gitignore ├── LICENSE ├── README.md ├── backend ├── backend.go ├── backendutil │ ├── backendutil.go │ ├── backendutil_test.go │ ├── body.go │ ├── body_test.go │ ├── bodystructure.go │ ├── bodystructure_test.go │ ├── envelope.go │ ├── envelope_test.go │ ├── flags.go │ ├── flags_test.go │ ├── search.go │ └── search_test.go ├── mailbox.go ├── memory │ ├── backend.go │ ├── mailbox.go │ ├── message.go │ └── user.go ├── updates.go └── user.go ├── client ├── client.go ├── client_test.go ├── cmd_any.go ├── cmd_any_test.go ├── cmd_auth.go ├── cmd_auth_test.go ├── cmd_noauth.go ├── cmd_noauth_test.go ├── cmd_selected.go ├── cmd_selected_test.go ├── example_test.go └── tag.go ├── command.go ├── command_test.go ├── commands ├── append.go ├── authenticate.go ├── capability.go ├── check.go ├── close.go ├── commands.go ├── copy.go ├── create.go ├── delete.go ├── expunge.go ├── fetch.go ├── list.go ├── login.go ├── logout.go ├── noop.go ├── rename.go ├── search.go ├── select.go ├── starttls.go ├── status.go ├── store.go ├── subscribe.go └── uid.go ├── conn.go ├── conn_test.go ├── date.go ├── date_test.go ├── go.mod ├── go.sum ├── imap.go ├── internal ├── testcert.go └── testutil.go ├── literal.go ├── logger.go ├── mailbox.go ├── mailbox_test.go ├── message.go ├── message_test.go ├── read.go ├── read_test.go ├── response.go ├── response_test.go ├── responses ├── authenticate.go ├── capability.go ├── expunge.go ├── fetch.go ├── list.go ├── list_test.go ├── responses.go ├── search.go ├── select.go └── status.go ├── search.go ├── search_test.go ├── seqset.go ├── seqset_test.go ├── server ├── cmd_any.go ├── cmd_any_test.go ├── cmd_auth.go ├── cmd_auth_test.go ├── cmd_noauth.go ├── cmd_noauth_test.go ├── cmd_selected.go ├── cmd_selected_test.go ├── conn.go ├── server.go └── server_test.go ├── status.go ├── status_test.go ├── utf7 ├── decoder.go ├── decoder_test.go ├── encoder.go ├── encoder_test.go └── utf7.go ├── write.go └── write_test.go /.build.yml: -------------------------------------------------------------------------------- 1 | image: alpine/edge 2 | packages: 3 | - go 4 | # Required by codecov 5 | - bash 6 | - findutils 7 | sources: 8 | - https://github.com/emersion/go-imap 9 | tasks: 10 | - build: | 11 | cd go-imap 12 | go build -v ./... 13 | - test: | 14 | cd go-imap 15 | go test -coverprofile=coverage.txt -covermode=atomic ./... 16 | - upload-coverage: | 17 | cd go-imap 18 | export CODECOV_TOKEN=8c0f7014-fcfa-4ed9-8972-542eb5958fb3 19 | curl -s https://codecov.io/bash | bash 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | /client.go 27 | /server.go 28 | coverage.txt 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 The Go-IMAP Authors 4 | Copyright (c) 2016 emersion 5 | Copyright (c) 2016 Proton Technologies AG 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-imap 2 | 3 | [![GoDoc](https://godoc.org/github.com/emersion/go-imap?status.svg)](https://godoc.org/github.com/emersion/go-imap) 4 | [![builds.sr.ht status](https://builds.sr.ht/~emersion/go-imap/commits.svg)](https://builds.sr.ht/~emersion/go-imap/commits?) 5 | [![Codecov](https://codecov.io/gh/emersion/go-imap/branch/master/graph/badge.svg)](https://codecov.io/gh/emersion/go-imap) 6 | 7 | An [IMAP4rev1](https://tools.ietf.org/html/rfc3501) library written in Go. It 8 | can be used to build a client and/or a server. 9 | 10 | ## Usage 11 | 12 | ### Client [![GoDoc](https://godoc.org/github.com/emersion/go-imap/client?status.svg)](https://godoc.org/github.com/emersion/go-imap/client) 13 | 14 | ```go 15 | package main 16 | 17 | import ( 18 | "log" 19 | 20 | "github.com/emersion/go-imap/client" 21 | "github.com/emersion/go-imap" 22 | ) 23 | 24 | func main() { 25 | log.Println("Connecting to server...") 26 | 27 | // Connect to server 28 | c, err := client.DialTLS("mail.example.org:993", nil) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | log.Println("Connected") 33 | 34 | // Don't forget to logout 35 | defer c.Logout() 36 | 37 | // Login 38 | if err := c.Login("username", "password"); err != nil { 39 | log.Fatal(err) 40 | } 41 | log.Println("Logged in") 42 | 43 | // List mailboxes 44 | mailboxes := make(chan *imap.MailboxInfo, 10) 45 | done := make(chan error, 1) 46 | go func () { 47 | done <- c.List("", "*", mailboxes) 48 | }() 49 | 50 | log.Println("Mailboxes:") 51 | for m := range mailboxes { 52 | log.Println("* " + m.Name) 53 | } 54 | 55 | if err := <-done; err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | // Select INBOX 60 | mbox, err := c.Select("INBOX", false) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | log.Println("Flags for INBOX:", mbox.Flags) 65 | 66 | // Get the last 4 messages 67 | from := uint32(1) 68 | to := mbox.Messages 69 | if mbox.Messages > 3 { 70 | // We're using unsigned integers here, only subtract if the result is > 0 71 | from = mbox.Messages - 3 72 | } 73 | seqset := new(imap.SeqSet) 74 | seqset.AddRange(from, to) 75 | 76 | messages := make(chan *imap.Message, 10) 77 | done = make(chan error, 1) 78 | go func() { 79 | done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages) 80 | }() 81 | 82 | log.Println("Last 4 messages:") 83 | for msg := range messages { 84 | log.Println("* " + msg.Envelope.Subject) 85 | } 86 | 87 | if err := <-done; err != nil { 88 | log.Fatal(err) 89 | } 90 | 91 | log.Println("Done!") 92 | } 93 | ``` 94 | 95 | ### Server [![GoDoc](https://godoc.org/github.com/emersion/go-imap/server?status.svg)](https://godoc.org/github.com/emersion/go-imap/server) 96 | 97 | ```go 98 | package main 99 | 100 | import ( 101 | "log" 102 | 103 | "github.com/emersion/go-imap/server" 104 | "github.com/emersion/go-imap/backend/memory" 105 | ) 106 | 107 | func main() { 108 | // Create a memory backend 109 | be := memory.New() 110 | 111 | // Create a new server 112 | s := server.New(be) 113 | s.Addr = ":1143" 114 | // Since we will use this server for testing only, we can allow plain text 115 | // authentication over unencrypted connections 116 | s.AllowInsecureAuth = true 117 | 118 | log.Println("Starting IMAP server at localhost:1143") 119 | if err := s.ListenAndServe(); err != nil { 120 | log.Fatal(err) 121 | } 122 | } 123 | ``` 124 | 125 | You can now use `telnet localhost 1143` to manually connect to the server. 126 | 127 | ## Extensions 128 | 129 | Support for several IMAP extensions is included in go-imap itself. This 130 | includes: 131 | 132 | * [IMPORTANT](https://tools.ietf.org/html/rfc8457) 133 | * [LITERAL+](https://tools.ietf.org/html/rfc7888) 134 | * [SASL-IR](https://tools.ietf.org/html/rfc4959) 135 | * [SPECIAL-USE](https://tools.ietf.org/html/rfc6154) 136 | * [CHILDREN](https://tools.ietf.org/html/rfc3348) 137 | 138 | Support for other extensions is provided via separate packages. See below. 139 | 140 | ## Extending go-imap 141 | 142 | ### Extensions 143 | 144 | Commands defined in IMAP extensions are available in other packages. See [the 145 | wiki](https://github.com/emersion/go-imap/wiki/Using-extensions#using-client-extensions) 146 | to learn how to use them. 147 | 148 | * [APPENDLIMIT](https://github.com/emersion/go-imap-appendlimit) 149 | * [COMPRESS](https://github.com/emersion/go-imap-compress) 150 | * [ENABLE](https://github.com/emersion/go-imap-enable) 151 | * [ID](https://github.com/ProtonMail/go-imap-id) 152 | * [IDLE](https://github.com/emersion/go-imap-idle) 153 | * [METADATA](https://github.com/emersion/go-imap-metadata) 154 | * [MOVE](https://github.com/emersion/go-imap-move) 155 | * [NAMESPACE](https://github.com/foxcpp/go-imap-namespace) 156 | * [QUOTA](https://github.com/emersion/go-imap-quota) 157 | * [SORT and THREAD](https://github.com/emersion/go-imap-sortthread) 158 | * [UNSELECT](https://github.com/emersion/go-imap-unselect) 159 | * [UIDPLUS](https://github.com/emersion/go-imap-uidplus) 160 | 161 | ### Server backends 162 | 163 | * [Memory](https://github.com/emersion/go-imap/tree/master/backend/memory) (for testing) 164 | * [Multi](https://github.com/emersion/go-imap-multi) 165 | * [PGP](https://github.com/emersion/go-imap-pgp) 166 | * [Proxy](https://github.com/emersion/go-imap-proxy) 167 | 168 | ### Related projects 169 | 170 | * [go-message](https://github.com/emersion/go-message) - parsing and formatting MIME and mail messages 171 | * [go-msgauth](https://github.com/emersion/go-msgauth) - handle DKIM, DMARC and Authentication-Results 172 | * [go-pgpmail](https://github.com/emersion/go-pgpmail) - decrypting and encrypting mails with OpenPGP 173 | * [go-sasl](https://github.com/emersion/go-sasl) - sending and receiving SASL authentications 174 | * [go-smtp](https://github.com/emersion/go-smtp) - building SMTP clients and servers 175 | 176 | ## License 177 | 178 | MIT 179 | -------------------------------------------------------------------------------- /backend/backend.go: -------------------------------------------------------------------------------- 1 | // Package backend defines an IMAP server backend interface. 2 | package backend 3 | 4 | import ( 5 | "errors" 6 | 7 | "github.com/emersion/go-imap" 8 | ) 9 | 10 | // ErrInvalidCredentials is returned by Backend.Login when a username or a 11 | // password is incorrect. 12 | var ErrInvalidCredentials = errors.New("Invalid credentials") 13 | 14 | // Backend is an IMAP server backend. A backend operation always deals with 15 | // users. 16 | type Backend interface { 17 | // Login authenticates a user. If the username or the password is incorrect, 18 | // it returns ErrInvalidCredentials. 19 | Login(connInfo *imap.ConnInfo, username, password string) (User, error) 20 | } 21 | -------------------------------------------------------------------------------- /backend/backendutil/backendutil.go: -------------------------------------------------------------------------------- 1 | // Package backendutil provides utility functions to implement IMAP backends. 2 | package backendutil 3 | -------------------------------------------------------------------------------- /backend/backendutil/backendutil_test.go: -------------------------------------------------------------------------------- 1 | package backendutil 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var testDate, _ = time.Parse(time.RFC1123Z, "Sat, 18 Jun 2016 12:00:00 +0900") 8 | 9 | const testHeaderString = "Content-Type: multipart/mixed; boundary=message-boundary\r\n" + 10 | "Date: Sat, 18 Jun 2016 12:00:00 +0900\r\n" + 11 | "Date: Sat, 19 Jun 2016 12:00:00 +0900\r\n" + 12 | "From: Mitsuha Miyamizu \r\n" + 13 | "Reply-To: Mitsuha Miyamizu \r\n" + 14 | "Message-Id: 42@example.org\r\n" + 15 | "Subject: Your Name.\r\n" + 16 | "To: Taki Tachibana \r\n" + 17 | "\r\n" 18 | 19 | const testHeaderFromToString = "From: Mitsuha Miyamizu \r\n" + 20 | "To: Taki Tachibana \r\n" + 21 | "\r\n" 22 | 23 | const testHeaderDateString = "Date: Sat, 18 Jun 2016 12:00:00 +0900\r\n" + 24 | "Date: Sat, 19 Jun 2016 12:00:00 +0900\r\n" + 25 | "\r\n" 26 | 27 | const testHeaderNoFromToString = "Content-Type: multipart/mixed; boundary=message-boundary\r\n" + 28 | "Date: Sat, 18 Jun 2016 12:00:00 +0900\r\n" + 29 | "Date: Sat, 19 Jun 2016 12:00:00 +0900\r\n" + 30 | "Reply-To: Mitsuha Miyamizu \r\n" + 31 | "Message-Id: 42@example.org\r\n" + 32 | "Subject: Your Name.\r\n" + 33 | "\r\n" 34 | 35 | const testAltHeaderString = "Content-Type: multipart/alternative; boundary=b2\r\n" + 36 | "\r\n" 37 | 38 | const testTextHeaderString = "Content-Disposition: inline\r\n" + 39 | "Content-Type: text/plain\r\n" + 40 | "\r\n" 41 | 42 | const testTextContentTypeString = "Content-Type: text/plain\r\n" + 43 | "\r\n" 44 | 45 | const testTextNoContentTypeString = "Content-Disposition: inline\r\n" + 46 | "\r\n" 47 | 48 | const testTextBodyString = "What's your name?" 49 | 50 | const testTextString = testTextHeaderString + testTextBodyString 51 | 52 | const testHTMLHeaderString = "Content-Disposition: inline\r\n" + 53 | "Content-Type: text/html\r\n" + 54 | "\r\n" 55 | 56 | const testHTMLBodyString = "
What's your\r\n name?
" 57 | 58 | const testHTMLString = testHTMLHeaderString + testHTMLBodyString 59 | 60 | const testAttachmentHeaderString = "Content-Disposition: attachment; filename=note.txt\r\n" + 61 | "Content-Type: text/plain\r\n" + 62 | "\r\n" 63 | 64 | const testAttachmentBodyString = "My name is Mitsuha." 65 | 66 | const testAttachmentString = testAttachmentHeaderString + testAttachmentBodyString 67 | 68 | const testBodyString = "--message-boundary\r\n" + 69 | testAltHeaderString + 70 | "\r\n--b2\r\n" + 71 | testTextString + 72 | "\r\n--b2\r\n" + 73 | testHTMLString + 74 | "\r\n--b2--\r\n" + 75 | "\r\n--message-boundary\r\n" + 76 | testAttachmentString + 77 | "\r\n--message-boundary--\r\n" 78 | 79 | const testMailString = testHeaderString + testBodyString 80 | -------------------------------------------------------------------------------- /backend/backendutil/body.go: -------------------------------------------------------------------------------- 1 | package backendutil 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "mime" 8 | nettextproto "net/textproto" 9 | "strings" 10 | 11 | "github.com/emersion/go-imap" 12 | "github.com/emersion/go-message/textproto" 13 | ) 14 | 15 | var errNoSuchPart = errors.New("backendutil: no such message body part") 16 | 17 | func multipartReader(header textproto.Header, body io.Reader) *textproto.MultipartReader { 18 | contentType := header.Get("Content-Type") 19 | if !strings.HasPrefix(strings.ToLower(contentType), "multipart/") { 20 | return nil 21 | } 22 | 23 | _, params, err := mime.ParseMediaType(contentType) 24 | if err != nil { 25 | return nil 26 | } 27 | 28 | return textproto.NewMultipartReader(body, params["boundary"]) 29 | } 30 | 31 | // FetchBodySection extracts a body section from a message. 32 | func FetchBodySection(header textproto.Header, body io.Reader, section *imap.BodySectionName) (imap.Literal, error) { 33 | // First, find the requested part using the provided path 34 | for i := 0; i < len(section.Path); i++ { 35 | n := section.Path[i] 36 | 37 | mr := multipartReader(header, body) 38 | if mr == nil { 39 | // First part of non-multipart message refers to the message itself. 40 | // See RFC 3501, Page 55. 41 | if len(section.Path) == 1 && section.Path[0] == 1 { 42 | break 43 | } 44 | return nil, errNoSuchPart 45 | } 46 | 47 | for j := 1; j <= n; j++ { 48 | p, err := mr.NextPart() 49 | if err == io.EOF { 50 | return nil, errNoSuchPart 51 | } else if err != nil { 52 | return nil, err 53 | } 54 | 55 | if j == n { 56 | body = p 57 | header = p.Header 58 | 59 | break 60 | } 61 | } 62 | } 63 | 64 | // Then, write the requested data to a buffer 65 | b := new(bytes.Buffer) 66 | 67 | resHeader := header 68 | if section.Fields != nil { 69 | // Copy header so we will not change value passed to us. 70 | resHeader = header.Copy() 71 | 72 | if section.NotFields { 73 | for _, fieldName := range section.Fields { 74 | resHeader.Del(fieldName) 75 | } 76 | } else { 77 | fieldsMap := make(map[string]struct{}, len(section.Fields)) 78 | for _, field := range section.Fields { 79 | fieldsMap[nettextproto.CanonicalMIMEHeaderKey(field)] = struct{}{} 80 | } 81 | 82 | for field := resHeader.Fields(); field.Next(); { 83 | if _, ok := fieldsMap[field.Key()]; !ok { 84 | field.Del() 85 | } 86 | } 87 | } 88 | } 89 | 90 | // Write the header 91 | err := textproto.WriteHeader(b, resHeader) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | switch section.Specifier { 97 | case imap.TextSpecifier: 98 | // The header hasn't been requested. Discard it. 99 | b.Reset() 100 | case imap.EntireSpecifier: 101 | if len(section.Path) > 0 { 102 | // When selecting a specific part by index, IMAP servers 103 | // return only the text, not the associated MIME header. 104 | b.Reset() 105 | } 106 | } 107 | 108 | // Write the body, if requested 109 | switch section.Specifier { 110 | case imap.EntireSpecifier, imap.TextSpecifier: 111 | if _, err := io.Copy(b, body); err != nil { 112 | return nil, err 113 | } 114 | } 115 | 116 | var l imap.Literal = b 117 | if section.Partial != nil { 118 | l = bytes.NewReader(section.ExtractPartial(b.Bytes())) 119 | } 120 | return l, nil 121 | } 122 | -------------------------------------------------------------------------------- /backend/backendutil/body_test.go: -------------------------------------------------------------------------------- 1 | package backendutil 2 | 3 | import ( 4 | "bufio" 5 | "io/ioutil" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/emersion/go-imap" 10 | "github.com/emersion/go-message/textproto" 11 | ) 12 | 13 | var bodyTests = []struct { 14 | section string 15 | body string 16 | }{ 17 | { 18 | section: "BODY[]", 19 | body: testMailString, 20 | }, 21 | { 22 | section: "BODY[1.1]", 23 | body: testTextBodyString, 24 | }, 25 | { 26 | section: "BODY[1.2]", 27 | body: testHTMLBodyString, 28 | }, 29 | { 30 | section: "BODY[2]", 31 | body: testAttachmentBodyString, 32 | }, 33 | { 34 | section: "BODY[HEADER]", 35 | body: testHeaderString, 36 | }, 37 | { 38 | section: "BODY[HEADER.FIELDS (From To)]", 39 | body: testHeaderFromToString, 40 | }, 41 | { 42 | section: "BODY[HEADER.FIELDS (FROM to)]", 43 | body: testHeaderFromToString, 44 | }, 45 | { 46 | section: "BODY[HEADER.FIELDS.NOT (From To)]", 47 | body: testHeaderNoFromToString, 48 | }, 49 | { 50 | section: "BODY[HEADER.FIELDS (Date)]", 51 | body: testHeaderDateString, 52 | }, 53 | { 54 | section: "BODY[1.1.HEADER]", 55 | body: testTextHeaderString, 56 | }, 57 | { 58 | section: "BODY[1.1.HEADER.FIELDS (Content-Type)]", 59 | body: testTextContentTypeString, 60 | }, 61 | { 62 | section: "BODY[1.1.HEADER.FIELDS.NOT (Content-Type)]", 63 | body: testTextNoContentTypeString, 64 | }, 65 | { 66 | section: "BODY[2.HEADER]", 67 | body: testAttachmentHeaderString, 68 | }, 69 | { 70 | section: "BODY[2.MIME]", 71 | body: testAttachmentHeaderString, 72 | }, 73 | { 74 | section: "BODY[TEXT]", 75 | body: testBodyString, 76 | }, 77 | { 78 | section: "BODY[1.1.TEXT]", 79 | body: testTextBodyString, 80 | }, 81 | { 82 | section: "BODY[2.TEXT]", 83 | body: testAttachmentBodyString, 84 | }, 85 | { 86 | section: "BODY[2.1]", 87 | body: "", 88 | }, 89 | { 90 | section: "BODY[3]", 91 | body: "", 92 | }, 93 | { 94 | section: "BODY[2.TEXT]<0.9>", 95 | body: testAttachmentBodyString[:9], 96 | }, 97 | } 98 | 99 | func TestFetchBodySection(t *testing.T) { 100 | for _, test := range bodyTests { 101 | test := test 102 | t.Run(test.section, func(t *testing.T) { 103 | bufferedBody := bufio.NewReader(strings.NewReader(testMailString)) 104 | 105 | header, err := textproto.ReadHeader(bufferedBody) 106 | if err != nil { 107 | t.Fatal("Expected no error while reading mail, got:", err) 108 | } 109 | 110 | section, err := imap.ParseBodySectionName(imap.FetchItem(test.section)) 111 | if err != nil { 112 | t.Fatal("Expected no error while parsing body section name, got:", err) 113 | } 114 | 115 | r, err := FetchBodySection(header, bufferedBody, section) 116 | if test.body == "" { 117 | if err == nil { 118 | t.Error("Expected an error while extracting non-existing body section") 119 | } 120 | } else { 121 | if err != nil { 122 | t.Fatal("Expected no error while extracting body section, got:", err) 123 | } 124 | 125 | b, err := ioutil.ReadAll(r) 126 | if err != nil { 127 | t.Fatal("Expected no error while reading body section, got:", err) 128 | } 129 | 130 | if s := string(b); s != test.body { 131 | t.Errorf("Expected body section %q to be \n%s\n but got \n%s", test.section, test.body, s) 132 | } 133 | } 134 | }) 135 | } 136 | } 137 | 138 | func TestFetchBodySection_NonMultipart(t *testing.T) { 139 | // https://tools.ietf.org/html/rfc3501#page-55: 140 | // Every message has at least one part number. Non-[MIME-IMB] 141 | // messages, and non-multipart [MIME-IMB] messages with no 142 | // encapsulated message, only have a part 1. 143 | 144 | testMsgHdr := "From: Mitsuha Miyamizu \r\n" + 145 | "To: Taki Tachibana \r\n" + 146 | "Subject: Your Name.\r\n" + 147 | "Message-Id: 42@example.org\r\n" + 148 | "\r\n" 149 | testMsgBody := "That's not multipart message. Thought it should be possible to get this text using BODY[1]." 150 | testMsg := testMsgHdr + testMsgBody 151 | 152 | tests := []struct { 153 | section string 154 | body string 155 | }{ 156 | { 157 | section: "BODY[1.MIME]", 158 | body: testMsgHdr, 159 | }, 160 | { 161 | section: "BODY[1]", 162 | body: testMsgBody, 163 | }, 164 | } 165 | 166 | for _, test := range tests { 167 | test := test 168 | t.Run(test.section, func(t *testing.T) { 169 | bufferedBody := bufio.NewReader(strings.NewReader(testMsg)) 170 | 171 | header, err := textproto.ReadHeader(bufferedBody) 172 | if err != nil { 173 | t.Fatal("Expected no error while reading mail, got:", err) 174 | } 175 | 176 | section, err := imap.ParseBodySectionName(imap.FetchItem(test.section)) 177 | if err != nil { 178 | t.Fatal("Expected no error while parsing body section name, got:", err) 179 | } 180 | 181 | r, err := FetchBodySection(header, bufferedBody, section) 182 | if err != nil { 183 | t.Fatal("Expected no error while extracting body section, got:", err) 184 | } 185 | 186 | b, err := ioutil.ReadAll(r) 187 | if err != nil { 188 | t.Fatal("Expected no error while reading body section, got:", err) 189 | } 190 | 191 | if s := string(b); s != test.body { 192 | t.Errorf("Expected body section %q to be \n%s\n but got \n%s", test.section, test.body, s) 193 | } 194 | }) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /backend/backendutil/bodystructure.go: -------------------------------------------------------------------------------- 1 | package backendutil 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "io/ioutil" 8 | "mime" 9 | "strings" 10 | 11 | "github.com/emersion/go-imap" 12 | "github.com/emersion/go-message/textproto" 13 | ) 14 | 15 | type countReader struct { 16 | r io.Reader 17 | bytes uint32 18 | newlines uint32 19 | endsWithLF bool 20 | } 21 | 22 | func (r *countReader) Read(b []byte) (int, error) { 23 | n, err := r.r.Read(b) 24 | r.bytes += uint32(n) 25 | if n != 0 { 26 | r.newlines += uint32(bytes.Count(b[:n], []byte{'\n'})) 27 | r.endsWithLF = b[n-1] == '\n' 28 | } 29 | // If the stream does not end with a newline - count missing newline. 30 | if err == io.EOF { 31 | if !r.endsWithLF { 32 | r.newlines++ 33 | } 34 | } 35 | return n, err 36 | } 37 | 38 | // FetchBodyStructure computes a message's body structure from its content. 39 | func FetchBodyStructure(header textproto.Header, body io.Reader, extended bool) (*imap.BodyStructure, error) { 40 | bs := new(imap.BodyStructure) 41 | 42 | mediaType, mediaParams, err := mime.ParseMediaType(header.Get("Content-Type")) 43 | if err == nil { 44 | typeParts := strings.SplitN(mediaType, "/", 2) 45 | bs.MIMEType = typeParts[0] 46 | if len(typeParts) == 2 { 47 | bs.MIMESubType = typeParts[1] 48 | } 49 | bs.Params = mediaParams 50 | } else { 51 | bs.MIMEType = "text" 52 | bs.MIMESubType = "plain" 53 | } 54 | 55 | bs.Id = header.Get("Content-Id") 56 | bs.Description = header.Get("Content-Description") 57 | bs.Encoding = header.Get("Content-Transfer-Encoding") 58 | 59 | if mr := multipartReader(header, body); mr != nil { 60 | var parts []*imap.BodyStructure 61 | for { 62 | p, err := mr.NextPart() 63 | if err == io.EOF { 64 | break 65 | } else if err != nil { 66 | return nil, err 67 | } 68 | 69 | pbs, err := FetchBodyStructure(p.Header, p, extended) 70 | if err != nil { 71 | return nil, err 72 | } 73 | parts = append(parts, pbs) 74 | } 75 | bs.Parts = parts 76 | } else { 77 | countedBody := countReader{r: body} 78 | needLines := false 79 | if bs.MIMEType == "message" && bs.MIMESubType == "rfc822" { 80 | // This will result in double-buffering if body is already a 81 | // bufio.Reader (most likely it is). :\ 82 | bufBody := bufio.NewReader(&countedBody) 83 | subMsgHdr, err := textproto.ReadHeader(bufBody) 84 | if err != nil { 85 | return nil, err 86 | } 87 | bs.Envelope, err = FetchEnvelope(subMsgHdr) 88 | if err != nil { 89 | return nil, err 90 | } 91 | bs.BodyStructure, err = FetchBodyStructure(subMsgHdr, bufBody, extended) 92 | if err != nil { 93 | return nil, err 94 | } 95 | needLines = true 96 | } else if bs.MIMEType == "text" { 97 | needLines = true 98 | } 99 | if _, err := io.Copy(ioutil.Discard, &countedBody); err != nil { 100 | return nil, err 101 | } 102 | bs.Size = countedBody.bytes 103 | if needLines { 104 | bs.Lines = countedBody.newlines 105 | } 106 | } 107 | 108 | if extended { 109 | bs.Extended = true 110 | bs.Disposition, bs.DispositionParams, _ = mime.ParseMediaType(header.Get("Content-Disposition")) 111 | 112 | // TODO: bs.Language, bs.Location 113 | // TODO: bs.MD5 114 | } 115 | 116 | return bs, nil 117 | } 118 | -------------------------------------------------------------------------------- /backend/backendutil/bodystructure_test.go: -------------------------------------------------------------------------------- 1 | package backendutil 2 | 3 | import ( 4 | "bufio" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/emersion/go-imap" 10 | "github.com/emersion/go-message/textproto" 11 | ) 12 | 13 | var testBodyStructure = &imap.BodyStructure{ 14 | MIMEType: "multipart", 15 | MIMESubType: "mixed", 16 | Params: map[string]string{"boundary": "message-boundary"}, 17 | Parts: []*imap.BodyStructure{ 18 | { 19 | MIMEType: "multipart", 20 | MIMESubType: "alternative", 21 | Params: map[string]string{"boundary": "b2"}, 22 | Extended: true, 23 | Parts: []*imap.BodyStructure{ 24 | { 25 | MIMEType: "text", 26 | MIMESubType: "plain", 27 | Params: map[string]string{}, 28 | Extended: true, 29 | Disposition: "inline", 30 | DispositionParams: map[string]string{}, 31 | Lines: 1, 32 | Size: 17, 33 | }, 34 | { 35 | MIMEType: "text", 36 | MIMESubType: "html", 37 | Params: map[string]string{}, 38 | Extended: true, 39 | Disposition: "inline", 40 | DispositionParams: map[string]string{}, 41 | Lines: 2, 42 | Size: 37, 43 | }, 44 | }, 45 | }, 46 | { 47 | MIMEType: "text", 48 | MIMESubType: "plain", 49 | Params: map[string]string{}, 50 | Extended: true, 51 | Disposition: "attachment", 52 | DispositionParams: map[string]string{"filename": "note.txt"}, 53 | Lines: 1, 54 | Size: 19, 55 | }, 56 | }, 57 | Extended: true, 58 | } 59 | 60 | func TestFetchBodyStructure(t *testing.T) { 61 | bufferedBody := bufio.NewReader(strings.NewReader(testMailString)) 62 | 63 | header, err := textproto.ReadHeader(bufferedBody) 64 | if err != nil { 65 | t.Fatal("Expected no error while reading mail, got:", err) 66 | } 67 | 68 | bs, err := FetchBodyStructure(header, bufferedBody, true) 69 | if err != nil { 70 | t.Fatal("Expected no error while fetching body structure, got:", err) 71 | } 72 | 73 | if !reflect.DeepEqual(testBodyStructure, bs) { 74 | t.Errorf("Expected body structure \n%+v\n but got \n%+v", testBodyStructure, bs) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /backend/backendutil/envelope.go: -------------------------------------------------------------------------------- 1 | package backendutil 2 | 3 | import ( 4 | "net/mail" 5 | "strings" 6 | 7 | "github.com/emersion/go-imap" 8 | "github.com/emersion/go-message/textproto" 9 | ) 10 | 11 | func headerAddressList(value string) ([]*imap.Address, error) { 12 | addrs, err := mail.ParseAddressList(value) 13 | if err != nil { 14 | return []*imap.Address{}, err 15 | } 16 | 17 | list := make([]*imap.Address, len(addrs)) 18 | for i, a := range addrs { 19 | parts := strings.SplitN(a.Address, "@", 2) 20 | mailbox := parts[0] 21 | var hostname string 22 | if len(parts) == 2 { 23 | hostname = parts[1] 24 | } 25 | 26 | list[i] = &imap.Address{ 27 | PersonalName: a.Name, 28 | MailboxName: mailbox, 29 | HostName: hostname, 30 | } 31 | } 32 | 33 | return list, err 34 | } 35 | 36 | // FetchEnvelope returns a message's envelope from its header. 37 | func FetchEnvelope(h textproto.Header) (*imap.Envelope, error) { 38 | env := new(imap.Envelope) 39 | 40 | env.Date, _ = mail.ParseDate(h.Get("Date")) 41 | env.Subject = h.Get("Subject") 42 | env.From, _ = headerAddressList(h.Get("From")) 43 | env.Sender, _ = headerAddressList(h.Get("Sender")) 44 | if len(env.Sender) == 0 { 45 | env.Sender = env.From 46 | } 47 | env.ReplyTo, _ = headerAddressList(h.Get("Reply-To")) 48 | if len(env.ReplyTo) == 0 { 49 | env.ReplyTo = env.From 50 | } 51 | env.To, _ = headerAddressList(h.Get("To")) 52 | env.Cc, _ = headerAddressList(h.Get("Cc")) 53 | env.Bcc, _ = headerAddressList(h.Get("Bcc")) 54 | env.InReplyTo = h.Get("In-Reply-To") 55 | env.MessageId = h.Get("Message-Id") 56 | 57 | return env, nil 58 | } 59 | -------------------------------------------------------------------------------- /backend/backendutil/envelope_test.go: -------------------------------------------------------------------------------- 1 | package backendutil 2 | 3 | import ( 4 | "bufio" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/emersion/go-imap" 10 | "github.com/emersion/go-message/textproto" 11 | ) 12 | 13 | var testEnvelope = &imap.Envelope{ 14 | Date: testDate, 15 | Subject: "Your Name.", 16 | From: []*imap.Address{{PersonalName: "Mitsuha Miyamizu", MailboxName: "mitsuha.miyamizu", HostName: "example.org"}}, 17 | Sender: []*imap.Address{{PersonalName: "Mitsuha Miyamizu", MailboxName: "mitsuha.miyamizu", HostName: "example.org"}}, 18 | ReplyTo: []*imap.Address{{PersonalName: "Mitsuha Miyamizu", MailboxName: "mitsuha.miyamizu+replyto", HostName: "example.org"}}, 19 | To: []*imap.Address{{PersonalName: "Taki Tachibana", MailboxName: "taki.tachibana", HostName: "example.org"}}, 20 | Cc: []*imap.Address{}, 21 | Bcc: []*imap.Address{}, 22 | InReplyTo: "", 23 | MessageId: "42@example.org", 24 | } 25 | 26 | func TestFetchEnvelope(t *testing.T) { 27 | hdr, err := textproto.ReadHeader(bufio.NewReader(strings.NewReader(testMailString))) 28 | if err != nil { 29 | t.Fatal("Expected no error while reading mail, got:", err) 30 | } 31 | 32 | env, err := FetchEnvelope(hdr) 33 | if err != nil { 34 | t.Fatal("Expected no error while fetching envelope, got:", err) 35 | } 36 | 37 | if !reflect.DeepEqual(env, testEnvelope) { 38 | t.Errorf("Expected envelope \n%+v\n but got \n%+v", testEnvelope, env) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/backendutil/flags.go: -------------------------------------------------------------------------------- 1 | package backendutil 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | // UpdateFlags executes a flag operation on the flag set current. 8 | func UpdateFlags(current []string, op imap.FlagsOp, flags []string) []string { 9 | // Don't modify contents of 'flags' slice. Only modify 'current'. 10 | // See https://github.com/golang/go/wiki/SliceTricks 11 | 12 | // Re-use current's backing store 13 | newFlags := current[:0] 14 | switch op { 15 | case imap.SetFlags: 16 | hasRecent := false 17 | // keep recent flag 18 | for _, flag := range current { 19 | if flag == imap.RecentFlag { 20 | newFlags = append(newFlags, imap.RecentFlag) 21 | hasRecent = true 22 | break 23 | } 24 | } 25 | // append new flags 26 | for _, flag := range flags { 27 | if flag == imap.RecentFlag { 28 | // Make sure we don't add the recent flag multiple times. 29 | if hasRecent { 30 | // Already have the recent flag, skip. 31 | continue 32 | } 33 | hasRecent = true 34 | } 35 | // append new flag 36 | newFlags = append(newFlags, flag) 37 | } 38 | case imap.AddFlags: 39 | // keep current flags 40 | newFlags = current 41 | // Only add new flag if it isn't already in current list. 42 | for _, addFlag := range flags { 43 | found := false 44 | for _, flag := range current { 45 | if addFlag == flag { 46 | found = true 47 | break 48 | } 49 | } 50 | // new flag not found, add it. 51 | if !found { 52 | newFlags = append(newFlags, addFlag) 53 | } 54 | } 55 | case imap.RemoveFlags: 56 | // Filter current flags 57 | for _, flag := range current { 58 | remove := false 59 | for _, removeFlag := range flags { 60 | if removeFlag == flag { 61 | remove = true 62 | } 63 | } 64 | if !remove { 65 | newFlags = append(newFlags, flag) 66 | } 67 | } 68 | default: 69 | // Unknown operation, return current flags unchanged 70 | newFlags = current 71 | } 72 | return newFlags 73 | } 74 | -------------------------------------------------------------------------------- /backend/backendutil/flags_test.go: -------------------------------------------------------------------------------- 1 | package backendutil 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/emersion/go-imap" 8 | ) 9 | 10 | var updateFlagsTests = []struct { 11 | op imap.FlagsOp 12 | flags []string 13 | res []string 14 | }{ 15 | { 16 | op: imap.AddFlags, 17 | flags: []string{"d", "e"}, 18 | res: []string{"a", "b", "c", "d", "e"}, 19 | }, 20 | { 21 | op: imap.AddFlags, 22 | flags: []string{"a", "d", "b"}, 23 | res: []string{"a", "b", "c", "d"}, 24 | }, 25 | { 26 | op: imap.RemoveFlags, 27 | flags: []string{"b", "v", "e", "a"}, 28 | res: []string{"c"}, 29 | }, 30 | { 31 | op: imap.SetFlags, 32 | flags: []string{"a", "d", "e"}, 33 | res: []string{"a", "d", "e"}, 34 | }, 35 | // Test unknown op for code coverage. 36 | { 37 | op: imap.FlagsOp("TestUnknownOp"), 38 | flags: []string{"a", "d", "e"}, 39 | res: []string{"a", "b", "c"}, 40 | }, 41 | } 42 | 43 | func TestUpdateFlags(t *testing.T) { 44 | flagsList := []string{"a", "b", "c"} 45 | for _, test := range updateFlagsTests { 46 | // Make a backup copy of 'test.flags' 47 | origFlags := append(test.flags[:0:0], test.flags...) 48 | // Copy flags 49 | current := append(flagsList[:0:0], flagsList...) 50 | got := UpdateFlags(current, test.op, test.flags) 51 | 52 | if !reflect.DeepEqual(got, test.res) { 53 | t.Errorf("Expected result to be \n%v\n but got \n%v", test.res, got) 54 | } 55 | // Verify that 'test.flags' wasn't modified 56 | if !reflect.DeepEqual(origFlags, test.flags) { 57 | t.Errorf("Unexpected change to operation flags list changed \nbefore %v\n after \n%v", 58 | origFlags, test.flags) 59 | } 60 | } 61 | } 62 | 63 | func TestUpdateFlags_Recent(t *testing.T) { 64 | current := []string{} 65 | 66 | current = UpdateFlags(current, imap.SetFlags, []string{imap.RecentFlag}) 67 | 68 | res := []string{imap.RecentFlag} 69 | if !reflect.DeepEqual(current, res) { 70 | t.Errorf("Expected result to be \n%v\n but got \n%v", res, current) 71 | } 72 | 73 | current = UpdateFlags(current, imap.SetFlags, []string{"something"}) 74 | 75 | res = []string{imap.RecentFlag, "something"} 76 | if !reflect.DeepEqual(current, res) { 77 | t.Errorf("Expected result to be \n%v\n but got \n%v", res, current) 78 | } 79 | 80 | current = UpdateFlags(current, imap.SetFlags, []string{"another", imap.RecentFlag}) 81 | 82 | res = []string{imap.RecentFlag, "another"} 83 | if !reflect.DeepEqual(current, res) { 84 | t.Errorf("Expected result to be \n%v\n but got \n%v", res, current) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /backend/backendutil/search.go: -------------------------------------------------------------------------------- 1 | package backendutil 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "time" 9 | 10 | "github.com/emersion/go-imap" 11 | "github.com/emersion/go-message" 12 | "github.com/emersion/go-message/mail" 13 | "github.com/emersion/go-message/textproto" 14 | ) 15 | 16 | func matchString(s, substr string) bool { 17 | return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) 18 | } 19 | 20 | func bufferBody(e *message.Entity) (*bytes.Buffer, error) { 21 | b := new(bytes.Buffer) 22 | if _, err := io.Copy(b, e.Body); err != nil { 23 | return nil, err 24 | } 25 | e.Body = b 26 | return b, nil 27 | } 28 | 29 | func matchBody(e *message.Entity, substr string) (bool, error) { 30 | if s, ok := e.Body.(fmt.Stringer); ok { 31 | return matchString(s.String(), substr), nil 32 | } 33 | 34 | b, err := bufferBody(e) 35 | if err != nil { 36 | return false, err 37 | } 38 | return matchString(b.String(), substr), nil 39 | } 40 | 41 | type lengther interface { 42 | Len() int 43 | } 44 | 45 | type countWriter struct { 46 | N int 47 | } 48 | 49 | func (w *countWriter) Write(b []byte) (int, error) { 50 | w.N += len(b) 51 | return len(b), nil 52 | } 53 | 54 | func bodyLen(e *message.Entity) (int, error) { 55 | headerSize := countWriter{} 56 | textproto.WriteHeader(&headerSize, e.Header.Header) 57 | 58 | if l, ok := e.Body.(lengther); ok { 59 | return l.Len() + headerSize.N, nil 60 | } 61 | 62 | b, err := bufferBody(e) 63 | if err != nil { 64 | return 0, err 65 | } 66 | return b.Len() + headerSize.N, nil 67 | } 68 | 69 | // Match returns true if a message and its metadata matches the provided 70 | // criteria. 71 | func Match(e *message.Entity, seqNum, uid uint32, date time.Time, flags []string, c *imap.SearchCriteria) (bool, error) { 72 | // TODO: support encoded header fields for Bcc, Cc, From, To 73 | // TODO: add header size for Larger and Smaller 74 | 75 | h := mail.Header{Header: e.Header} 76 | 77 | if !c.SentBefore.IsZero() || !c.SentSince.IsZero() { 78 | t, err := h.Date() 79 | if err != nil { 80 | return false, err 81 | } 82 | t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC) 83 | 84 | if !c.SentBefore.IsZero() && !t.Before(c.SentBefore) { 85 | return false, nil 86 | } 87 | if !c.SentSince.IsZero() && t.Before(c.SentSince) { 88 | return false, nil 89 | } 90 | } 91 | 92 | for key, wantValues := range c.Header { 93 | ok := e.Header.Has(key) 94 | for _, wantValue := range wantValues { 95 | if wantValue == "" && !ok { 96 | return false, nil 97 | } 98 | if wantValue != "" { 99 | ok := false 100 | values := e.Header.FieldsByKey(key) 101 | for values.Next() { 102 | decoded, _ := values.Text() 103 | if matchString(decoded, wantValue) { 104 | ok = true 105 | break 106 | } 107 | } 108 | if !ok { 109 | return false, nil 110 | } 111 | } 112 | } 113 | } 114 | for _, body := range c.Body { 115 | if ok, err := matchBody(e, body); err != nil || !ok { 116 | return false, err 117 | } 118 | } 119 | for _, text := range c.Text { 120 | headerMatch := false 121 | for f := e.Header.Fields(); f.Next(); { 122 | decoded, err := f.Text() 123 | if err != nil { 124 | continue 125 | } 126 | if strings.Contains(f.Key()+": "+decoded, text) { 127 | headerMatch = true 128 | } 129 | } 130 | if ok, err := matchBody(e, text); err != nil || !ok && !headerMatch { 131 | return false, err 132 | } 133 | } 134 | 135 | if c.Larger > 0 || c.Smaller > 0 { 136 | n, err := bodyLen(e) 137 | if err != nil { 138 | return false, err 139 | } 140 | 141 | if c.Larger > 0 && uint32(n) <= c.Larger { 142 | return false, nil 143 | } 144 | if c.Smaller > 0 && uint32(n) >= c.Smaller { 145 | return false, nil 146 | } 147 | } 148 | 149 | if !c.Since.IsZero() || !c.Before.IsZero() { 150 | if !matchDate(date, c) { 151 | return false, nil 152 | } 153 | } 154 | 155 | if c.WithFlags != nil || c.WithoutFlags != nil { 156 | if !matchFlags(flags, c) { 157 | return false, nil 158 | } 159 | } 160 | 161 | if c.SeqNum != nil || c.Uid != nil { 162 | if !matchSeqNumAndUid(seqNum, uid, c) { 163 | return false, nil 164 | } 165 | } 166 | 167 | for _, not := range c.Not { 168 | ok, err := Match(e, seqNum, uid, date, flags, not) 169 | if err != nil || ok { 170 | return false, err 171 | } 172 | } 173 | for _, or := range c.Or { 174 | ok1, err := Match(e, seqNum, uid, date, flags, or[0]) 175 | if err != nil { 176 | return ok1, err 177 | } 178 | 179 | ok2, err := Match(e, seqNum, uid, date, flags, or[1]) 180 | if err != nil || (!ok1 && !ok2) { 181 | return false, err 182 | } 183 | } 184 | 185 | return true, nil 186 | } 187 | 188 | func matchFlags(flags []string, c *imap.SearchCriteria) bool { 189 | flagsMap := make(map[string]bool) 190 | for _, f := range flags { 191 | flagsMap[f] = true 192 | } 193 | 194 | for _, f := range c.WithFlags { 195 | if !flagsMap[f] { 196 | return false 197 | } 198 | } 199 | for _, f := range c.WithoutFlags { 200 | if flagsMap[f] { 201 | return false 202 | } 203 | } 204 | 205 | return true 206 | } 207 | 208 | func matchSeqNumAndUid(seqNum uint32, uid uint32, c *imap.SearchCriteria) bool { 209 | if c.SeqNum != nil && !c.SeqNum.Contains(seqNum) { 210 | return false 211 | } 212 | if c.Uid != nil && !c.Uid.Contains(uid) { 213 | return false 214 | } 215 | return true 216 | } 217 | 218 | func matchDate(date time.Time, c *imap.SearchCriteria) bool { 219 | // We discard time zone information by setting it to UTC. 220 | // RFC 3501 explicitly requires zone unaware date comparison. 221 | date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC) 222 | 223 | if !c.Since.IsZero() && !date.After(c.Since) { 224 | return false 225 | } 226 | if !c.Before.IsZero() && !date.Before(c.Before) { 227 | return false 228 | } 229 | return true 230 | } 231 | -------------------------------------------------------------------------------- /backend/mailbox.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/emersion/go-imap" 7 | ) 8 | 9 | // Mailbox represents a mailbox belonging to a user in the mail storage system. 10 | // A mailbox operation always deals with messages. 11 | type Mailbox interface { 12 | // Name returns this mailbox name. 13 | Name() string 14 | 15 | // Info returns this mailbox info. 16 | Info() (*imap.MailboxInfo, error) 17 | 18 | // Status returns this mailbox status. The fields Name, Flags, PermanentFlags 19 | // and UnseenSeqNum in the returned MailboxStatus must be always populated. 20 | // This function does not affect the state of any messages in the mailbox. See 21 | // RFC 3501 section 6.3.10 for a list of items that can be requested. 22 | Status(items []imap.StatusItem) (*imap.MailboxStatus, error) 23 | 24 | // SetSubscribed adds or removes the mailbox to the server's set of "active" 25 | // or "subscribed" mailboxes. 26 | SetSubscribed(subscribed bool) error 27 | 28 | // Check requests a checkpoint of the currently selected mailbox. A checkpoint 29 | // refers to any implementation-dependent housekeeping associated with the 30 | // mailbox (e.g., resolving the server's in-memory state of the mailbox with 31 | // the state on its disk). A checkpoint MAY take a non-instantaneous amount of 32 | // real time to complete. If a server implementation has no such housekeeping 33 | // considerations, CHECK is equivalent to NOOP. 34 | Check() error 35 | 36 | // ListMessages returns a list of messages. seqset must be interpreted as UIDs 37 | // if uid is set to true and as message sequence numbers otherwise. See RFC 38 | // 3501 section 6.4.5 for a list of items that can be requested. 39 | // 40 | // Messages must be sent to ch. When the function returns, ch must be closed. 41 | ListMessages(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error 42 | 43 | // SearchMessages searches messages. The returned list must contain UIDs if 44 | // uid is set to true, or sequence numbers otherwise. 45 | SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) 46 | 47 | // CreateMessage appends a new message to this mailbox. The \Recent flag will 48 | // be added no matter flags is empty or not. If date is nil, the current time 49 | // will be used. 50 | // 51 | // If the Backend implements Updater, it must notify the client immediately 52 | // via a mailbox update. 53 | CreateMessage(flags []string, date time.Time, body imap.Literal) error 54 | 55 | // UpdateMessagesFlags alters flags for the specified message(s). 56 | // 57 | // If the Backend implements Updater, it must notify the client immediately 58 | // via a message update. 59 | UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, operation imap.FlagsOp, flags []string) error 60 | 61 | // CopyMessages copies the specified message(s) to the end of the specified 62 | // destination mailbox. The flags and internal date of the message(s) SHOULD 63 | // be preserved, and the Recent flag SHOULD be set, in the copy. 64 | // 65 | // If the destination mailbox does not exist, a server SHOULD return an error. 66 | // It SHOULD NOT automatically create the mailbox. 67 | // 68 | // If the Backend implements Updater, it must notify the client immediately 69 | // via a mailbox update. 70 | CopyMessages(uid bool, seqset *imap.SeqSet, dest string) error 71 | 72 | // Expunge permanently removes all messages that have the \Deleted flag set 73 | // from the currently selected mailbox. 74 | // 75 | // If the Backend implements Updater, it must notify the client immediately 76 | // via an expunge update. 77 | Expunge() error 78 | } 79 | -------------------------------------------------------------------------------- /backend/memory/backend.go: -------------------------------------------------------------------------------- 1 | // A memory backend. 2 | package memory 3 | 4 | import ( 5 | "errors" 6 | "time" 7 | 8 | "github.com/emersion/go-imap" 9 | "github.com/emersion/go-imap/backend" 10 | ) 11 | 12 | type Backend struct { 13 | users map[string]*User 14 | } 15 | 16 | func (be *Backend) Login(_ *imap.ConnInfo, username, password string) (backend.User, error) { 17 | user, ok := be.users[username] 18 | if ok && user.password == password { 19 | return user, nil 20 | } 21 | 22 | return nil, errors.New("Bad username or password") 23 | } 24 | 25 | func New() *Backend { 26 | user := &User{username: "username", password: "password"} 27 | 28 | body := "From: contact@example.org\r\n" + 29 | "To: contact@example.org\r\n" + 30 | "Subject: A little message, just for you\r\n" + 31 | "Date: Wed, 11 May 2016 14:31:59 +0000\r\n" + 32 | "Message-ID: <0000000@localhost/>\r\n" + 33 | "Content-Type: text/plain\r\n" + 34 | "\r\n" + 35 | "Hi there :)" 36 | 37 | user.mailboxes = map[string]*Mailbox{ 38 | "INBOX": { 39 | name: "INBOX", 40 | user: user, 41 | Messages: []*Message{ 42 | { 43 | Uid: 6, 44 | Date: time.Now(), 45 | Flags: []string{"\\Seen"}, 46 | Size: uint32(len(body)), 47 | Body: []byte(body), 48 | }, 49 | }, 50 | }, 51 | } 52 | 53 | return &Backend{ 54 | users: map[string]*User{user.username: user}, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /backend/memory/mailbox.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "io/ioutil" 5 | "time" 6 | 7 | "github.com/emersion/go-imap" 8 | "github.com/emersion/go-imap/backend" 9 | "github.com/emersion/go-imap/backend/backendutil" 10 | ) 11 | 12 | var Delimiter = "/" 13 | 14 | type Mailbox struct { 15 | Subscribed bool 16 | Messages []*Message 17 | 18 | name string 19 | user *User 20 | } 21 | 22 | func (mbox *Mailbox) Name() string { 23 | return mbox.name 24 | } 25 | 26 | func (mbox *Mailbox) Info() (*imap.MailboxInfo, error) { 27 | info := &imap.MailboxInfo{ 28 | Delimiter: Delimiter, 29 | Name: mbox.name, 30 | } 31 | return info, nil 32 | } 33 | 34 | func (mbox *Mailbox) uidNext() uint32 { 35 | var uid uint32 36 | for _, msg := range mbox.Messages { 37 | if msg.Uid > uid { 38 | uid = msg.Uid 39 | } 40 | } 41 | uid++ 42 | return uid 43 | } 44 | 45 | func (mbox *Mailbox) flags() []string { 46 | flagsMap := make(map[string]bool) 47 | for _, msg := range mbox.Messages { 48 | for _, f := range msg.Flags { 49 | if !flagsMap[f] { 50 | flagsMap[f] = true 51 | } 52 | } 53 | } 54 | 55 | var flags []string 56 | for f := range flagsMap { 57 | flags = append(flags, f) 58 | } 59 | return flags 60 | } 61 | 62 | func (mbox *Mailbox) unseenSeqNum() uint32 { 63 | for i, msg := range mbox.Messages { 64 | seqNum := uint32(i + 1) 65 | 66 | seen := false 67 | for _, flag := range msg.Flags { 68 | if flag == imap.SeenFlag { 69 | seen = true 70 | break 71 | } 72 | } 73 | 74 | if !seen { 75 | return seqNum 76 | } 77 | } 78 | return 0 79 | } 80 | 81 | func (mbox *Mailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) { 82 | status := imap.NewMailboxStatus(mbox.name, items) 83 | status.Flags = mbox.flags() 84 | status.PermanentFlags = []string{"\\*"} 85 | status.UnseenSeqNum = mbox.unseenSeqNum() 86 | 87 | for _, name := range items { 88 | switch name { 89 | case imap.StatusMessages: 90 | status.Messages = uint32(len(mbox.Messages)) 91 | case imap.StatusUidNext: 92 | status.UidNext = mbox.uidNext() 93 | case imap.StatusUidValidity: 94 | status.UidValidity = 1 95 | case imap.StatusRecent: 96 | status.Recent = 0 // TODO 97 | case imap.StatusUnseen: 98 | status.Unseen = 0 // TODO 99 | } 100 | } 101 | 102 | return status, nil 103 | } 104 | 105 | func (mbox *Mailbox) SetSubscribed(subscribed bool) error { 106 | mbox.Subscribed = subscribed 107 | return nil 108 | } 109 | 110 | func (mbox *Mailbox) Check() error { 111 | return nil 112 | } 113 | 114 | func (mbox *Mailbox) ListMessages(uid bool, seqSet *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error { 115 | defer close(ch) 116 | 117 | for i, msg := range mbox.Messages { 118 | seqNum := uint32(i + 1) 119 | 120 | var id uint32 121 | if uid { 122 | id = msg.Uid 123 | } else { 124 | id = seqNum 125 | } 126 | if !seqSet.Contains(id) { 127 | continue 128 | } 129 | 130 | m, err := msg.Fetch(seqNum, items) 131 | if err != nil { 132 | continue 133 | } 134 | 135 | ch <- m 136 | } 137 | 138 | return nil 139 | } 140 | 141 | func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) { 142 | var ids []uint32 143 | for i, msg := range mbox.Messages { 144 | seqNum := uint32(i + 1) 145 | 146 | ok, err := msg.Match(seqNum, criteria) 147 | if err != nil || !ok { 148 | continue 149 | } 150 | 151 | var id uint32 152 | if uid { 153 | id = msg.Uid 154 | } else { 155 | id = seqNum 156 | } 157 | ids = append(ids, id) 158 | } 159 | return ids, nil 160 | } 161 | 162 | func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error { 163 | if date.IsZero() { 164 | date = time.Now() 165 | } 166 | 167 | b, err := ioutil.ReadAll(body) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | mbox.Messages = append(mbox.Messages, &Message{ 173 | Uid: mbox.uidNext(), 174 | Date: date, 175 | Size: uint32(len(b)), 176 | Flags: flags, 177 | Body: b, 178 | }) 179 | return nil 180 | } 181 | 182 | func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, flags []string) error { 183 | for i, msg := range mbox.Messages { 184 | var id uint32 185 | if uid { 186 | id = msg.Uid 187 | } else { 188 | id = uint32(i + 1) 189 | } 190 | if !seqset.Contains(id) { 191 | continue 192 | } 193 | 194 | msg.Flags = backendutil.UpdateFlags(msg.Flags, op, flags) 195 | } 196 | 197 | return nil 198 | } 199 | 200 | func (mbox *Mailbox) CopyMessages(uid bool, seqset *imap.SeqSet, destName string) error { 201 | dest, ok := mbox.user.mailboxes[destName] 202 | if !ok { 203 | return backend.ErrNoSuchMailbox 204 | } 205 | 206 | for i, msg := range mbox.Messages { 207 | var id uint32 208 | if uid { 209 | id = msg.Uid 210 | } else { 211 | id = uint32(i + 1) 212 | } 213 | if !seqset.Contains(id) { 214 | continue 215 | } 216 | 217 | msgCopy := *msg 218 | msgCopy.Uid = dest.uidNext() 219 | dest.Messages = append(dest.Messages, &msgCopy) 220 | } 221 | 222 | return nil 223 | } 224 | 225 | func (mbox *Mailbox) Expunge() error { 226 | for i := len(mbox.Messages) - 1; i >= 0; i-- { 227 | msg := mbox.Messages[i] 228 | 229 | deleted := false 230 | for _, flag := range msg.Flags { 231 | if flag == imap.DeletedFlag { 232 | deleted = true 233 | break 234 | } 235 | } 236 | 237 | if deleted { 238 | mbox.Messages = append(mbox.Messages[:i], mbox.Messages[i+1:]...) 239 | } 240 | } 241 | 242 | return nil 243 | } 244 | -------------------------------------------------------------------------------- /backend/memory/message.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "time" 8 | 9 | "github.com/emersion/go-imap" 10 | "github.com/emersion/go-imap/backend/backendutil" 11 | "github.com/emersion/go-message" 12 | "github.com/emersion/go-message/textproto" 13 | ) 14 | 15 | type Message struct { 16 | Uid uint32 17 | Date time.Time 18 | Size uint32 19 | Flags []string 20 | Body []byte 21 | } 22 | 23 | func (m *Message) entity() (*message.Entity, error) { 24 | return message.Read(bytes.NewReader(m.Body)) 25 | } 26 | 27 | func (m *Message) headerAndBody() (textproto.Header, io.Reader, error) { 28 | body := bufio.NewReader(bytes.NewReader(m.Body)) 29 | hdr, err := textproto.ReadHeader(body) 30 | return hdr, body, err 31 | } 32 | 33 | func (m *Message) Fetch(seqNum uint32, items []imap.FetchItem) (*imap.Message, error) { 34 | fetched := imap.NewMessage(seqNum, items) 35 | for _, item := range items { 36 | switch item { 37 | case imap.FetchEnvelope: 38 | hdr, _, _ := m.headerAndBody() 39 | fetched.Envelope, _ = backendutil.FetchEnvelope(hdr) 40 | case imap.FetchBody, imap.FetchBodyStructure: 41 | hdr, body, _ := m.headerAndBody() 42 | fetched.BodyStructure, _ = backendutil.FetchBodyStructure(hdr, body, item == imap.FetchBodyStructure) 43 | case imap.FetchFlags: 44 | fetched.Flags = m.Flags 45 | case imap.FetchInternalDate: 46 | fetched.InternalDate = m.Date 47 | case imap.FetchRFC822Size: 48 | fetched.Size = m.Size 49 | case imap.FetchUid: 50 | fetched.Uid = m.Uid 51 | default: 52 | section, err := imap.ParseBodySectionName(item) 53 | if err != nil { 54 | break 55 | } 56 | 57 | body := bufio.NewReader(bytes.NewReader(m.Body)) 58 | hdr, err := textproto.ReadHeader(body) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | l, _ := backendutil.FetchBodySection(hdr, body, section) 64 | fetched.Body[section] = l 65 | } 66 | } 67 | 68 | return fetched, nil 69 | } 70 | 71 | func (m *Message) Match(seqNum uint32, c *imap.SearchCriteria) (bool, error) { 72 | e, _ := m.entity() 73 | return backendutil.Match(e, seqNum, m.Uid, m.Date, m.Flags, c) 74 | } 75 | -------------------------------------------------------------------------------- /backend/memory/user.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-imap/backend" 7 | ) 8 | 9 | type User struct { 10 | username string 11 | password string 12 | mailboxes map[string]*Mailbox 13 | } 14 | 15 | func (u *User) Username() string { 16 | return u.username 17 | } 18 | 19 | func (u *User) ListMailboxes(subscribed bool) (mailboxes []backend.Mailbox, err error) { 20 | for _, mailbox := range u.mailboxes { 21 | if subscribed && !mailbox.Subscribed { 22 | continue 23 | } 24 | 25 | mailboxes = append(mailboxes, mailbox) 26 | } 27 | return 28 | } 29 | 30 | func (u *User) GetMailbox(name string) (mailbox backend.Mailbox, err error) { 31 | mailbox, ok := u.mailboxes[name] 32 | if !ok { 33 | err = errors.New("No such mailbox") 34 | } 35 | return 36 | } 37 | 38 | func (u *User) CreateMailbox(name string) error { 39 | if _, ok := u.mailboxes[name]; ok { 40 | return errors.New("Mailbox already exists") 41 | } 42 | 43 | u.mailboxes[name] = &Mailbox{name: name, user: u} 44 | return nil 45 | } 46 | 47 | func (u *User) DeleteMailbox(name string) error { 48 | if name == "INBOX" { 49 | return errors.New("Cannot delete INBOX") 50 | } 51 | if _, ok := u.mailboxes[name]; !ok { 52 | return errors.New("No such mailbox") 53 | } 54 | 55 | delete(u.mailboxes, name) 56 | return nil 57 | } 58 | 59 | func (u *User) RenameMailbox(existingName, newName string) error { 60 | mbox, ok := u.mailboxes[existingName] 61 | if !ok { 62 | return errors.New("No such mailbox") 63 | } 64 | 65 | u.mailboxes[newName] = &Mailbox{ 66 | name: newName, 67 | Messages: mbox.Messages, 68 | user: u, 69 | } 70 | 71 | mbox.Messages = nil 72 | 73 | if existingName != "INBOX" { 74 | delete(u.mailboxes, existingName) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (u *User) Logout() error { 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /backend/updates.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | // Update contains user and mailbox information about an unilateral backend 8 | // update. 9 | type Update interface { 10 | // The user targeted by this update. If empty, all connected users will 11 | // be notified. 12 | Username() string 13 | // The mailbox targeted by this update. If empty, the update targets all 14 | // mailboxes. 15 | Mailbox() string 16 | // Done returns a channel that is closed when the update has been broadcast to 17 | // all clients. 18 | Done() chan struct{} 19 | } 20 | 21 | // NewUpdate creates a new update. 22 | func NewUpdate(username, mailbox string) Update { 23 | return &update{ 24 | username: username, 25 | mailbox: mailbox, 26 | } 27 | } 28 | 29 | type update struct { 30 | username string 31 | mailbox string 32 | done chan struct{} 33 | } 34 | 35 | func (u *update) Username() string { 36 | return u.username 37 | } 38 | 39 | func (u *update) Mailbox() string { 40 | return u.mailbox 41 | } 42 | 43 | func (u *update) Done() chan struct{} { 44 | if u.done == nil { 45 | u.done = make(chan struct{}) 46 | } 47 | return u.done 48 | } 49 | 50 | // StatusUpdate is a status update. See RFC 3501 section 7.1 for a list of 51 | // status responses. 52 | type StatusUpdate struct { 53 | Update 54 | *imap.StatusResp 55 | } 56 | 57 | // MailboxUpdate is a mailbox update. 58 | type MailboxUpdate struct { 59 | Update 60 | *imap.MailboxStatus 61 | } 62 | 63 | // MailboxInfoUpdate is a maiblox info update. 64 | type MailboxInfoUpdate struct { 65 | Update 66 | *imap.MailboxInfo 67 | } 68 | 69 | // MessageUpdate is a message update. 70 | type MessageUpdate struct { 71 | Update 72 | *imap.Message 73 | } 74 | 75 | // ExpungeUpdate is an expunge update. 76 | type ExpungeUpdate struct { 77 | Update 78 | SeqNum uint32 79 | } 80 | 81 | // BackendUpdater is a Backend that implements Updater is able to send 82 | // unilateral backend updates. Backends not implementing this interface don't 83 | // correctly send unilateral updates, for instance if a user logs in from two 84 | // connections and deletes a message from one of them, the over is not aware 85 | // that such a mesage has been deleted. More importantly, backends implementing 86 | // Updater can notify the user for external updates such as new message 87 | // notifications. 88 | type BackendUpdater interface { 89 | // Updates returns a set of channels where updates are sent to. 90 | Updates() <-chan Update 91 | } 92 | 93 | // MailboxPoller is a Mailbox that is able to poll updates for new messages or 94 | // message status updates during a period of inactivity. 95 | type MailboxPoller interface { 96 | // Poll requests mailbox updates. 97 | Poll() error 98 | } 99 | -------------------------------------------------------------------------------- /backend/user.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNoSuchMailbox is returned by User.GetMailbox, User.DeleteMailbox and 7 | // User.RenameMailbox when retrieving, deleting or renaming a mailbox that 8 | // doesn't exist. 9 | ErrNoSuchMailbox = errors.New("No such mailbox") 10 | // ErrMailboxAlreadyExists is returned by User.CreateMailbox and 11 | // User.RenameMailbox when creating or renaming mailbox that already exists. 12 | ErrMailboxAlreadyExists = errors.New("Mailbox already exists") 13 | ) 14 | 15 | // User represents a user in the mail storage system. A user operation always 16 | // deals with mailboxes. 17 | type User interface { 18 | // Username returns this user's username. 19 | Username() string 20 | 21 | // ListMailboxes returns a list of mailboxes belonging to this user. If 22 | // subscribed is set to true, only returns subscribed mailboxes. 23 | ListMailboxes(subscribed bool) ([]Mailbox, error) 24 | 25 | // GetMailbox returns a mailbox. If it doesn't exist, it returns 26 | // ErrNoSuchMailbox. 27 | GetMailbox(name string) (Mailbox, error) 28 | 29 | // CreateMailbox creates a new mailbox. 30 | // 31 | // If the mailbox already exists, an error must be returned. If the mailbox 32 | // name is suffixed with the server's hierarchy separator character, this is a 33 | // declaration that the client intends to create mailbox names under this name 34 | // in the hierarchy. 35 | // 36 | // If the server's hierarchy separator character appears elsewhere in the 37 | // name, the server SHOULD create any superior hierarchical names that are 38 | // needed for the CREATE command to be successfully completed. In other 39 | // words, an attempt to create "foo/bar/zap" on a server in which "/" is the 40 | // hierarchy separator character SHOULD create foo/ and foo/bar/ if they do 41 | // not already exist. 42 | // 43 | // If a new mailbox is created with the same name as a mailbox which was 44 | // deleted, its unique identifiers MUST be greater than any unique identifiers 45 | // used in the previous incarnation of the mailbox UNLESS the new incarnation 46 | // has a different unique identifier validity value. 47 | CreateMailbox(name string) error 48 | 49 | // DeleteMailbox permanently remove the mailbox with the given name. It is an 50 | // error to // attempt to delete INBOX or a mailbox name that does not exist. 51 | // 52 | // The DELETE command MUST NOT remove inferior hierarchical names. For 53 | // example, if a mailbox "foo" has an inferior "foo.bar" (assuming "." is the 54 | // hierarchy delimiter character), removing "foo" MUST NOT remove "foo.bar". 55 | // 56 | // The value of the highest-used unique identifier of the deleted mailbox MUST 57 | // be preserved so that a new mailbox created with the same name will not 58 | // reuse the identifiers of the former incarnation, UNLESS the new incarnation 59 | // has a different unique identifier validity value. 60 | DeleteMailbox(name string) error 61 | 62 | // RenameMailbox changes the name of a mailbox. It is an error to attempt to 63 | // rename from a mailbox name that does not exist or to a mailbox name that 64 | // already exists. 65 | // 66 | // If the name has inferior hierarchical names, then the inferior hierarchical 67 | // names MUST also be renamed. For example, a rename of "foo" to "zap" will 68 | // rename "foo/bar" (assuming "/" is the hierarchy delimiter character) to 69 | // "zap/bar". 70 | // 71 | // If the server's hierarchy separator character appears in the name, the 72 | // server SHOULD create any superior hierarchical names that are needed for 73 | // the RENAME command to complete successfully. In other words, an attempt to 74 | // rename "foo/bar/zap" to baz/rag/zowie on a server in which "/" is the 75 | // hierarchy separator character SHOULD create baz/ and baz/rag/ if they do 76 | // not already exist. 77 | // 78 | // The value of the highest-used unique identifier of the old mailbox name 79 | // MUST be preserved so that a new mailbox created with the same name will not 80 | // reuse the identifiers of the former incarnation, UNLESS the new incarnation 81 | // has a different unique identifier validity value. 82 | // 83 | // Renaming INBOX is permitted, and has special behavior. It moves all 84 | // messages in INBOX to a new mailbox with the given name, leaving INBOX 85 | // empty. If the server implementation supports inferior hierarchical names 86 | // of INBOX, these are unaffected by a rename of INBOX. 87 | RenameMailbox(existingName, newName string) error 88 | 89 | // Logout is called when this User will no longer be used, likely because the 90 | // client closed the connection. 91 | Logout() error 92 | } 93 | -------------------------------------------------------------------------------- /client/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "net" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/emersion/go-imap" 12 | ) 13 | 14 | type cmdScanner struct { 15 | scanner *bufio.Scanner 16 | } 17 | 18 | func (s *cmdScanner) ScanLine() string { 19 | s.scanner.Scan() 20 | return s.scanner.Text() 21 | } 22 | 23 | func (s *cmdScanner) ScanCmd() (tag string, cmd string) { 24 | parts := strings.SplitN(s.ScanLine(), " ", 2) 25 | return parts[0], parts[1] 26 | } 27 | 28 | func newCmdScanner(r io.Reader) *cmdScanner { 29 | return &cmdScanner{ 30 | scanner: bufio.NewScanner(r), 31 | } 32 | } 33 | 34 | type serverConn struct { 35 | *cmdScanner 36 | net.Conn 37 | net.Listener 38 | } 39 | 40 | func (c *serverConn) Close() error { 41 | if err := c.Conn.Close(); err != nil { 42 | return err 43 | } 44 | return c.Listener.Close() 45 | } 46 | 47 | func (c *serverConn) WriteString(s string) (n int, err error) { 48 | return io.WriteString(c.Conn, s) 49 | } 50 | 51 | func newTestClient(t *testing.T) (c *Client, s *serverConn) { 52 | return newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN] Server ready.\r\n") 53 | } 54 | 55 | func newTestClientWithGreeting(t *testing.T, greeting string) (c *Client, s *serverConn) { 56 | l, err := net.Listen("tcp", "127.0.0.1:0") 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | done := make(chan struct{}) 62 | go func() { 63 | conn, err := l.Accept() 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | if _, err := io.WriteString(conn, greeting); err != nil { 69 | panic(err) 70 | } 71 | 72 | s = &serverConn{newCmdScanner(conn), conn, l} 73 | close(done) 74 | }() 75 | 76 | c, err = Dial(l.Addr().String()) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | <-done 82 | return 83 | } 84 | 85 | func setClientState(c *Client, state imap.ConnState, mailbox *imap.MailboxStatus) { 86 | c.locker.Lock() 87 | c.state = state 88 | c.mailbox = mailbox 89 | c.locker.Unlock() 90 | } 91 | 92 | func TestClient(t *testing.T) { 93 | c, s := newTestClient(t) 94 | defer s.Close() 95 | 96 | if ok, err := c.Support("IMAP4rev1"); err != nil { 97 | t.Fatal("c.Support(IMAP4rev1) =", err) 98 | } else if !ok { 99 | t.Fatal("c.Support(IMAP4rev1) = false, want true") 100 | } 101 | } 102 | 103 | func TestClient_SetDebug(t *testing.T) { 104 | c, s := newTestClient(t) 105 | defer s.Close() 106 | 107 | var b bytes.Buffer 108 | done := make(chan error) 109 | 110 | go func() { 111 | c.SetDebug(&b) 112 | done <- nil 113 | }() 114 | if tag, cmd := s.ScanCmd(); cmd != "NOOP" { 115 | t.Fatal("Bad command:", cmd) 116 | } else { 117 | s.WriteString(tag + " OK NOOP completed.\r\n") 118 | } 119 | // wait for SetDebug to finish. 120 | <-done 121 | 122 | go func() { 123 | _, err := c.Capability() 124 | done <- err 125 | }() 126 | 127 | tag, cmd := s.ScanCmd() 128 | if cmd != "CAPABILITY" { 129 | t.Fatal("Bad command:", cmd) 130 | } 131 | 132 | s.WriteString("* CAPABILITY IMAP4rev1\r\n") 133 | s.WriteString(tag + " OK CAPABILITY completed.\r\n") 134 | 135 | if err := <-done; err != nil { 136 | t.Fatal("c.Capability() =", err) 137 | } 138 | 139 | if b.Len() == 0 { 140 | t.Error("empty debug buffer") 141 | } 142 | } 143 | 144 | func TestClient_unilateral(t *testing.T) { 145 | c, s := newTestClient(t) 146 | defer s.Close() 147 | 148 | setClientState(c, imap.SelectedState, imap.NewMailboxStatus("INBOX", nil)) 149 | 150 | updates := make(chan Update, 1) 151 | c.Updates = updates 152 | 153 | s.WriteString("* 42 EXISTS\r\n") 154 | if update, ok := (<-updates).(*MailboxUpdate); !ok || update.Mailbox.Messages != 42 { 155 | t.Errorf("Invalid messages count: expected %v but got %v", 42, update.Mailbox.Messages) 156 | } 157 | 158 | s.WriteString("* 587 RECENT\r\n") 159 | if update, ok := (<-updates).(*MailboxUpdate); !ok || update.Mailbox.Recent != 587 { 160 | t.Errorf("Invalid recent count: expected %v but got %v", 587, update.Mailbox.Recent) 161 | } 162 | 163 | s.WriteString("* 65535 EXPUNGE\r\n") 164 | if update, ok := (<-updates).(*ExpungeUpdate); !ok || update.SeqNum != 65535 { 165 | t.Errorf("Invalid expunged sequence number: expected %v but got %v", 65535, update.SeqNum) 166 | } 167 | 168 | s.WriteString("* 431 FETCH (FLAGS (\\Seen))\r\n") 169 | if update, ok := (<-updates).(*MessageUpdate); !ok || update.Message.SeqNum != 431 { 170 | t.Errorf("Invalid expunged sequence number: expected %v but got %v", 431, update.Message.SeqNum) 171 | } 172 | 173 | s.WriteString("* OK Reticulating splines...\r\n") 174 | if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Reticulating splines..." { 175 | t.Errorf("Invalid info: got %v", update.Status.Info) 176 | } 177 | 178 | s.WriteString("* NO Kansai band competition is in 30 seconds !\r\n") 179 | if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Kansai band competition is in 30 seconds !" { 180 | t.Errorf("Invalid warning: got %v", update.Status.Info) 181 | } 182 | 183 | s.WriteString("* BAD Battery level too low, shutting down.\r\n") 184 | if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Battery level too low, shutting down." { 185 | t.Errorf("Invalid error: got %v", update.Status.Info) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /client/cmd_any.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-imap" 7 | "github.com/emersion/go-imap/commands" 8 | ) 9 | 10 | // ErrAlreadyLoggedOut is returned if Logout is called when the client is 11 | // already logged out. 12 | var ErrAlreadyLoggedOut = errors.New("Already logged out") 13 | 14 | // Capability requests a listing of capabilities that the server supports. 15 | // Capabilities are often returned by the server with the greeting or with the 16 | // STARTTLS and LOGIN responses, so usually explicitly requesting capabilities 17 | // isn't needed. 18 | // 19 | // Most of the time, Support should be used instead. 20 | func (c *Client) Capability() (map[string]bool, error) { 21 | cmd := &commands.Capability{} 22 | 23 | if status, err := c.execute(cmd, nil); err != nil { 24 | return nil, err 25 | } else if err := status.Err(); err != nil { 26 | return nil, err 27 | } 28 | 29 | c.locker.Lock() 30 | caps := c.caps 31 | c.locker.Unlock() 32 | return caps, nil 33 | } 34 | 35 | // Support checks if cap is a capability supported by the server. If the server 36 | // hasn't sent its capabilities yet, Support requests them. 37 | func (c *Client) Support(cap string) (bool, error) { 38 | c.locker.Lock() 39 | ok := c.caps != nil 40 | c.locker.Unlock() 41 | 42 | // If capabilities are not cached, request them 43 | if !ok { 44 | if _, err := c.Capability(); err != nil { 45 | return false, err 46 | } 47 | } 48 | 49 | c.locker.Lock() 50 | supported := c.caps[cap] 51 | c.locker.Unlock() 52 | return supported, nil 53 | } 54 | 55 | // Noop always succeeds and does nothing. 56 | // 57 | // It can be used as a periodic poll for new messages or message status updates 58 | // during a period of inactivity. It can also be used to reset any inactivity 59 | // autologout timer on the server. 60 | func (c *Client) Noop() error { 61 | cmd := new(commands.Noop) 62 | 63 | status, err := c.execute(cmd, nil) 64 | if err != nil { 65 | return err 66 | } 67 | return status.Err() 68 | } 69 | 70 | // Logout gracefully closes the connection. 71 | func (c *Client) Logout() error { 72 | if c.State() == imap.LogoutState { 73 | return ErrAlreadyLoggedOut 74 | } 75 | 76 | cmd := new(commands.Logout) 77 | 78 | if status, err := c.execute(cmd, nil); err == errClosed { 79 | // Server closed connection, that's what we want anyway 80 | return nil 81 | } else if err != nil { 82 | return err 83 | } else if status != nil { 84 | return status.Err() 85 | } 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /client/cmd_any_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/emersion/go-imap" 7 | ) 8 | 9 | func TestClient_Capability(t *testing.T) { 10 | c, s := newTestClient(t) 11 | defer s.Close() 12 | 13 | var caps map[string]bool 14 | done := make(chan error, 1) 15 | go func() { 16 | var err error 17 | caps, err = c.Capability() 18 | done <- err 19 | }() 20 | 21 | tag, cmd := s.ScanCmd() 22 | if cmd != "CAPABILITY" { 23 | t.Fatalf("client sent command %v, want CAPABILITY", cmd) 24 | } 25 | s.WriteString("* CAPABILITY IMAP4rev1 XTEST\r\n") 26 | s.WriteString(tag + " OK CAPABILITY completed.\r\n") 27 | 28 | if err := <-done; err != nil { 29 | t.Error("c.Capability() = ", err) 30 | } 31 | 32 | if !caps["XTEST"] { 33 | t.Error("XTEST capability missing") 34 | } 35 | } 36 | 37 | func TestClient_Noop(t *testing.T) { 38 | c, s := newTestClient(t) 39 | defer s.Close() 40 | 41 | done := make(chan error, 1) 42 | go func() { 43 | done <- c.Noop() 44 | }() 45 | 46 | tag, cmd := s.ScanCmd() 47 | if cmd != "NOOP" { 48 | t.Fatalf("client sent command %v, want NOOP", cmd) 49 | } 50 | s.WriteString(tag + " OK NOOP completed\r\n") 51 | 52 | if err := <-done; err != nil { 53 | t.Error("c.Noop() = ", err) 54 | } 55 | } 56 | 57 | func TestClient_Logout(t *testing.T) { 58 | c, s := newTestClient(t) 59 | defer s.Close() 60 | 61 | done := make(chan error, 1) 62 | go func() { 63 | done <- c.Logout() 64 | }() 65 | 66 | tag, cmd := s.ScanCmd() 67 | if cmd != "LOGOUT" { 68 | t.Fatalf("client sent command %v, want LOGOUT", cmd) 69 | } 70 | s.WriteString("* BYE Client asked to close the connection.\r\n") 71 | s.WriteString(tag + " OK LOGOUT completed\r\n") 72 | 73 | if err := <-done; err != nil { 74 | t.Error("c.Logout() =", err) 75 | } 76 | 77 | if state := c.State(); state != imap.LogoutState { 78 | t.Errorf("c.State() = %v, want %v", state, imap.LogoutState) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/cmd_auth.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/emersion/go-imap" 8 | "github.com/emersion/go-imap/commands" 9 | "github.com/emersion/go-imap/responses" 10 | ) 11 | 12 | // ErrNotLoggedIn is returned if a function that requires the client to be 13 | // logged in is called then the client isn't. 14 | var ErrNotLoggedIn = errors.New("Not logged in") 15 | 16 | func (c *Client) ensureAuthenticated() error { 17 | state := c.State() 18 | if state != imap.AuthenticatedState && state != imap.SelectedState { 19 | return ErrNotLoggedIn 20 | } 21 | return nil 22 | } 23 | 24 | // Select selects a mailbox so that messages in the mailbox can be accessed. Any 25 | // currently selected mailbox is deselected before attempting the new selection. 26 | // Even if the readOnly parameter is set to false, the server can decide to open 27 | // the mailbox in read-only mode. 28 | func (c *Client) Select(name string, readOnly bool) (*imap.MailboxStatus, error) { 29 | if err := c.ensureAuthenticated(); err != nil { 30 | return nil, err 31 | } 32 | 33 | cmd := &commands.Select{ 34 | Mailbox: name, 35 | ReadOnly: readOnly, 36 | } 37 | 38 | mbox := &imap.MailboxStatus{Name: name, Items: make(map[imap.StatusItem]interface{})} 39 | res := &responses.Select{ 40 | Mailbox: mbox, 41 | } 42 | c.locker.Lock() 43 | c.mailbox = mbox 44 | c.locker.Unlock() 45 | 46 | status, err := c.execute(cmd, res) 47 | if err != nil { 48 | c.locker.Lock() 49 | c.mailbox = nil 50 | c.locker.Unlock() 51 | return nil, err 52 | } 53 | if err := status.Err(); err != nil { 54 | c.locker.Lock() 55 | c.mailbox = nil 56 | c.locker.Unlock() 57 | return nil, err 58 | } 59 | 60 | c.locker.Lock() 61 | mbox.ReadOnly = (status.Code == imap.CodeReadOnly) 62 | c.state = imap.SelectedState 63 | c.locker.Unlock() 64 | return mbox, nil 65 | } 66 | 67 | // Create creates a mailbox with the given name. 68 | func (c *Client) Create(name string) error { 69 | if err := c.ensureAuthenticated(); err != nil { 70 | return err 71 | } 72 | 73 | cmd := &commands.Create{ 74 | Mailbox: name, 75 | } 76 | 77 | status, err := c.execute(cmd, nil) 78 | if err != nil { 79 | return err 80 | } 81 | return status.Err() 82 | } 83 | 84 | // Delete permanently removes the mailbox with the given name. 85 | func (c *Client) Delete(name string) error { 86 | if err := c.ensureAuthenticated(); err != nil { 87 | return err 88 | } 89 | 90 | cmd := &commands.Delete{ 91 | Mailbox: name, 92 | } 93 | 94 | status, err := c.execute(cmd, nil) 95 | if err != nil { 96 | return err 97 | } 98 | return status.Err() 99 | } 100 | 101 | // Rename changes the name of a mailbox. 102 | func (c *Client) Rename(existingName, newName string) error { 103 | if err := c.ensureAuthenticated(); err != nil { 104 | return err 105 | } 106 | 107 | cmd := &commands.Rename{ 108 | Existing: existingName, 109 | New: newName, 110 | } 111 | 112 | status, err := c.execute(cmd, nil) 113 | if err != nil { 114 | return err 115 | } 116 | return status.Err() 117 | } 118 | 119 | // Subscribe adds the specified mailbox name to the server's set of "active" or 120 | // "subscribed" mailboxes. 121 | func (c *Client) Subscribe(name string) error { 122 | if err := c.ensureAuthenticated(); err != nil { 123 | return err 124 | } 125 | 126 | cmd := &commands.Subscribe{ 127 | Mailbox: name, 128 | } 129 | 130 | status, err := c.execute(cmd, nil) 131 | if err != nil { 132 | return err 133 | } 134 | return status.Err() 135 | } 136 | 137 | // Unsubscribe removes the specified mailbox name from the server's set of 138 | // "active" or "subscribed" mailboxes. 139 | func (c *Client) Unsubscribe(name string) error { 140 | if err := c.ensureAuthenticated(); err != nil { 141 | return err 142 | } 143 | 144 | cmd := &commands.Unsubscribe{ 145 | Mailbox: name, 146 | } 147 | 148 | status, err := c.execute(cmd, nil) 149 | if err != nil { 150 | return err 151 | } 152 | return status.Err() 153 | } 154 | 155 | // List returns a subset of names from the complete set of all names available 156 | // to the client. 157 | // 158 | // An empty name argument is a special request to return the hierarchy delimiter 159 | // and the root name of the name given in the reference. The character "*" is a 160 | // wildcard, and matches zero or more characters at this position. The 161 | // character "%" is similar to "*", but it does not match a hierarchy delimiter. 162 | func (c *Client) List(ref, name string, ch chan *imap.MailboxInfo) error { 163 | defer close(ch) 164 | 165 | if err := c.ensureAuthenticated(); err != nil { 166 | return err 167 | } 168 | 169 | cmd := &commands.List{ 170 | Reference: ref, 171 | Mailbox: name, 172 | } 173 | res := &responses.List{Mailboxes: ch} 174 | 175 | status, err := c.execute(cmd, res) 176 | if err != nil { 177 | return err 178 | } 179 | return status.Err() 180 | } 181 | 182 | // Lsub returns a subset of names from the set of names that the user has 183 | // declared as being "active" or "subscribed". 184 | func (c *Client) Lsub(ref, name string, ch chan *imap.MailboxInfo) error { 185 | defer close(ch) 186 | 187 | if err := c.ensureAuthenticated(); err != nil { 188 | return err 189 | } 190 | 191 | cmd := &commands.List{ 192 | Reference: ref, 193 | Mailbox: name, 194 | Subscribed: true, 195 | } 196 | res := &responses.List{ 197 | Mailboxes: ch, 198 | Subscribed: true, 199 | } 200 | 201 | status, err := c.execute(cmd, res) 202 | if err != nil { 203 | return err 204 | } 205 | return status.Err() 206 | } 207 | 208 | // Status requests the status of the indicated mailbox. It does not change the 209 | // currently selected mailbox, nor does it affect the state of any messages in 210 | // the queried mailbox. 211 | // 212 | // See RFC 3501 section 6.3.10 for a list of items that can be requested. 213 | func (c *Client) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) { 214 | if err := c.ensureAuthenticated(); err != nil { 215 | return nil, err 216 | } 217 | 218 | cmd := &commands.Status{ 219 | Mailbox: name, 220 | Items: items, 221 | } 222 | res := &responses.Status{ 223 | Mailbox: new(imap.MailboxStatus), 224 | } 225 | 226 | status, err := c.execute(cmd, res) 227 | if err != nil { 228 | return nil, err 229 | } 230 | return res.Mailbox, status.Err() 231 | } 232 | 233 | // Append appends the literal argument as a new message to the end of the 234 | // specified destination mailbox. This argument SHOULD be in the format of an 235 | // RFC 2822 message. flags and date are optional arguments and can be set to 236 | // nil. 237 | func (c *Client) Append(mbox string, flags []string, date time.Time, msg imap.Literal) error { 238 | if err := c.ensureAuthenticated(); err != nil { 239 | return err 240 | } 241 | 242 | cmd := &commands.Append{ 243 | Mailbox: mbox, 244 | Flags: flags, 245 | Date: date, 246 | Message: msg, 247 | } 248 | 249 | status, err := c.execute(cmd, nil) 250 | if err != nil { 251 | return err 252 | } 253 | return status.Err() 254 | } 255 | -------------------------------------------------------------------------------- /client/cmd_noauth.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "net" 7 | 8 | "github.com/emersion/go-imap" 9 | "github.com/emersion/go-imap/commands" 10 | "github.com/emersion/go-imap/responses" 11 | "github.com/emersion/go-sasl" 12 | ) 13 | 14 | var ( 15 | // ErrAlreadyLoggedIn is returned if Login or Authenticate is called when the 16 | // client is already logged in. 17 | ErrAlreadyLoggedIn = errors.New("Already logged in") 18 | // ErrTLSAlreadyEnabled is returned if StartTLS is called when TLS is already 19 | // enabled. 20 | ErrTLSAlreadyEnabled = errors.New("TLS is already enabled") 21 | // ErrLoginDisabled is returned if Login or Authenticate is called when the 22 | // server has disabled authentication. Most of the time, calling enabling TLS 23 | // solves the problem. 24 | ErrLoginDisabled = errors.New("Login is disabled in current state") 25 | ) 26 | 27 | // SupportStartTLS checks if the server supports STARTTLS. 28 | func (c *Client) SupportStartTLS() (bool, error) { 29 | return c.Support("STARTTLS") 30 | } 31 | 32 | // StartTLS starts TLS negotiation. 33 | func (c *Client) StartTLS(tlsConfig *tls.Config) error { 34 | if c.isTLS { 35 | return ErrTLSAlreadyEnabled 36 | } 37 | 38 | if tlsConfig == nil { 39 | tlsConfig = new(tls.Config) 40 | } 41 | if tlsConfig.ServerName == "" { 42 | tlsConfig = tlsConfig.Clone() 43 | tlsConfig.ServerName = c.serverName 44 | } 45 | 46 | cmd := new(commands.StartTLS) 47 | 48 | err := c.Upgrade(func(conn net.Conn) (net.Conn, error) { 49 | // Flag connection as in upgrading 50 | c.upgrading = true 51 | if status, err := c.execute(cmd, nil); err != nil { 52 | return nil, err 53 | } else if err := status.Err(); err != nil { 54 | return nil, err 55 | } 56 | 57 | // Wait for reader to block. 58 | c.conn.WaitReady() 59 | tlsConn := tls.Client(conn, tlsConfig) 60 | if err := tlsConn.Handshake(); err != nil { 61 | return nil, err 62 | } 63 | 64 | // Capabilities change when TLS is enabled 65 | c.locker.Lock() 66 | c.caps = nil 67 | c.locker.Unlock() 68 | 69 | return tlsConn, nil 70 | }) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | c.isTLS = true 76 | return nil 77 | } 78 | 79 | // SupportAuth checks if the server supports a given authentication mechanism. 80 | func (c *Client) SupportAuth(mech string) (bool, error) { 81 | return c.Support("AUTH=" + mech) 82 | } 83 | 84 | // Authenticate indicates a SASL authentication mechanism to the server. If the 85 | // server supports the requested authentication mechanism, it performs an 86 | // authentication protocol exchange to authenticate and identify the client. 87 | func (c *Client) Authenticate(auth sasl.Client) error { 88 | if c.State() != imap.NotAuthenticatedState { 89 | return ErrAlreadyLoggedIn 90 | } 91 | 92 | mech, ir, err := auth.Start() 93 | if err != nil { 94 | return err 95 | } 96 | 97 | cmd := &commands.Authenticate{ 98 | Mechanism: mech, 99 | } 100 | 101 | irOk, err := c.Support("SASL-IR") 102 | if err != nil { 103 | return err 104 | } 105 | if irOk { 106 | cmd.InitialResponse = ir 107 | } 108 | 109 | res := &responses.Authenticate{ 110 | Mechanism: auth, 111 | InitialResponse: ir, 112 | RepliesCh: make(chan []byte, 10), 113 | } 114 | if irOk { 115 | res.InitialResponse = nil 116 | } 117 | 118 | status, err := c.execute(cmd, res) 119 | if err != nil { 120 | return err 121 | } 122 | if err = status.Err(); err != nil { 123 | return err 124 | } 125 | 126 | c.locker.Lock() 127 | c.state = imap.AuthenticatedState 128 | c.caps = nil // Capabilities change when user is logged in 129 | c.locker.Unlock() 130 | 131 | if status.Code == "CAPABILITY" { 132 | c.gotStatusCaps(status.Arguments) 133 | } 134 | 135 | return nil 136 | } 137 | 138 | // Login identifies the client to the server and carries the plaintext password 139 | // authenticating this user. 140 | func (c *Client) Login(username, password string) error { 141 | if state := c.State(); state == imap.AuthenticatedState || state == imap.SelectedState { 142 | return ErrAlreadyLoggedIn 143 | } 144 | 145 | c.locker.Lock() 146 | loginDisabled := c.caps != nil && c.caps["LOGINDISABLED"] 147 | c.locker.Unlock() 148 | if loginDisabled { 149 | return ErrLoginDisabled 150 | } 151 | 152 | cmd := &commands.Login{ 153 | Username: username, 154 | Password: password, 155 | } 156 | 157 | status, err := c.execute(cmd, nil) 158 | if err != nil { 159 | return err 160 | } 161 | if err = status.Err(); err != nil { 162 | return err 163 | } 164 | 165 | c.locker.Lock() 166 | c.state = imap.AuthenticatedState 167 | c.caps = nil // Capabilities change when user is logged in 168 | c.locker.Unlock() 169 | 170 | if status.Code == "CAPABILITY" { 171 | c.gotStatusCaps(status.Arguments) 172 | } 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /client/example_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "io/ioutil" 7 | "log" 8 | "net/mail" 9 | "time" 10 | 11 | "github.com/emersion/go-imap" 12 | "github.com/emersion/go-imap/client" 13 | ) 14 | 15 | func ExampleClient() { 16 | log.Println("Connecting to server...") 17 | 18 | // Connect to server 19 | c, err := client.DialTLS("mail.example.org:993", nil) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | log.Println("Connected") 24 | 25 | // Don't forget to logout 26 | defer c.Logout() 27 | 28 | // Login 29 | if err := c.Login("username", "password"); err != nil { 30 | log.Fatal(err) 31 | } 32 | log.Println("Logged in") 33 | 34 | // List mailboxes 35 | mailboxes := make(chan *imap.MailboxInfo, 10) 36 | done := make(chan error, 1) 37 | go func() { 38 | done <- c.List("", "*", mailboxes) 39 | }() 40 | 41 | log.Println("Mailboxes:") 42 | for m := range mailboxes { 43 | log.Println("* " + m.Name) 44 | } 45 | 46 | if err := <-done; err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | // Select INBOX 51 | mbox, err := c.Select("INBOX", false) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | log.Println("Flags for INBOX:", mbox.Flags) 56 | 57 | // Get the last 4 messages 58 | from := uint32(1) 59 | to := mbox.Messages 60 | if mbox.Messages > 3 { 61 | // We're using unsigned integers here, only substract if the result is > 0 62 | from = mbox.Messages - 3 63 | } 64 | seqset := new(imap.SeqSet) 65 | seqset.AddRange(from, to) 66 | items := []imap.FetchItem{imap.FetchEnvelope} 67 | 68 | messages := make(chan *imap.Message, 10) 69 | done = make(chan error, 1) 70 | go func() { 71 | done <- c.Fetch(seqset, items, messages) 72 | }() 73 | 74 | log.Println("Last 4 messages:") 75 | for msg := range messages { 76 | log.Println("* " + msg.Envelope.Subject) 77 | } 78 | 79 | if err := <-done; err != nil { 80 | log.Fatal(err) 81 | } 82 | 83 | log.Println("Done!") 84 | } 85 | 86 | func ExampleClient_Fetch() { 87 | // Let's assume c is a client 88 | var c *client.Client 89 | 90 | // Select INBOX 91 | mbox, err := c.Select("INBOX", false) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | 96 | // Get the last message 97 | if mbox.Messages == 0 { 98 | log.Fatal("No message in mailbox") 99 | } 100 | seqset := new(imap.SeqSet) 101 | seqset.AddRange(mbox.Messages, mbox.Messages) 102 | 103 | // Get the whole message body 104 | section := &imap.BodySectionName{} 105 | items := []imap.FetchItem{section.FetchItem()} 106 | 107 | messages := make(chan *imap.Message, 1) 108 | done := make(chan error, 1) 109 | go func() { 110 | done <- c.Fetch(seqset, items, messages) 111 | }() 112 | 113 | log.Println("Last message:") 114 | msg := <-messages 115 | r := msg.GetBody(section) 116 | if r == nil { 117 | log.Fatal("Server didn't returned message body") 118 | } 119 | 120 | if err := <-done; err != nil { 121 | log.Fatal(err) 122 | } 123 | 124 | m, err := mail.ReadMessage(r) 125 | if err != nil { 126 | log.Fatal(err) 127 | } 128 | 129 | header := m.Header 130 | log.Println("Date:", header.Get("Date")) 131 | log.Println("From:", header.Get("From")) 132 | log.Println("To:", header.Get("To")) 133 | log.Println("Subject:", header.Get("Subject")) 134 | 135 | body, err := ioutil.ReadAll(m.Body) 136 | if err != nil { 137 | log.Fatal(err) 138 | } 139 | log.Println(body) 140 | } 141 | 142 | func ExampleClient_Append() { 143 | // Let's assume c is a client 144 | var c *client.Client 145 | 146 | // Write the message to a buffer 147 | var b bytes.Buffer 148 | b.WriteString("From: \r\n") 149 | b.WriteString("To: \r\n") 150 | b.WriteString("Subject: Hey there\r\n") 151 | b.WriteString("\r\n") 152 | b.WriteString("Hey <3") 153 | 154 | // Append it to INBOX, with two flags 155 | flags := []string{imap.FlaggedFlag, "foobar"} 156 | if err := c.Append("INBOX", flags, time.Now(), &b); err != nil { 157 | log.Fatal(err) 158 | } 159 | } 160 | 161 | func ExampleClient_Expunge() { 162 | // Let's assume c is a client 163 | var c *client.Client 164 | 165 | // Select INBOX 166 | mbox, err := c.Select("INBOX", false) 167 | if err != nil { 168 | log.Fatal(err) 169 | } 170 | 171 | // We will delete the last message 172 | if mbox.Messages == 0 { 173 | log.Fatal("No message in mailbox") 174 | } 175 | seqset := new(imap.SeqSet) 176 | seqset.AddNum(mbox.Messages) 177 | 178 | // First mark the message as deleted 179 | item := imap.FormatFlagsOp(imap.AddFlags, true) 180 | flags := []interface{}{imap.DeletedFlag} 181 | if err := c.Store(seqset, item, flags, nil); err != nil { 182 | log.Fatal(err) 183 | } 184 | 185 | // Then delete it 186 | if err := c.Expunge(nil); err != nil { 187 | log.Fatal(err) 188 | } 189 | 190 | log.Println("Last message has been deleted") 191 | } 192 | 193 | func ExampleClient_StartTLS() { 194 | log.Println("Connecting to server...") 195 | 196 | // Connect to server 197 | c, err := client.Dial("mail.example.org:143") 198 | if err != nil { 199 | log.Fatal(err) 200 | } 201 | log.Println("Connected") 202 | 203 | // Don't forget to logout 204 | defer c.Logout() 205 | 206 | // Start a TLS session 207 | tlsConfig := &tls.Config{ServerName: "mail.example.org"} 208 | if err := c.StartTLS(tlsConfig); err != nil { 209 | log.Fatal(err) 210 | } 211 | log.Println("TLS started") 212 | 213 | // Now we can login 214 | if err := c.Login("username", "password"); err != nil { 215 | log.Fatal(err) 216 | } 217 | log.Println("Logged in") 218 | } 219 | 220 | func ExampleClient_Store() { 221 | // Let's assume c is a client 222 | var c *client.Client 223 | 224 | // Select INBOX 225 | _, err := c.Select("INBOX", false) 226 | if err != nil { 227 | log.Fatal(err) 228 | } 229 | 230 | // Mark message 42 as seen 231 | seqSet := new(imap.SeqSet) 232 | seqSet.AddNum(42) 233 | item := imap.FormatFlagsOp(imap.AddFlags, true) 234 | flags := []interface{}{imap.SeenFlag} 235 | err = c.Store(seqSet, item, flags, nil) 236 | if err != nil { 237 | log.Fatal(err) 238 | } 239 | 240 | log.Println("Message has been marked as seen") 241 | } 242 | 243 | func ExampleClient_Search() { 244 | // Let's assume c is a client 245 | var c *client.Client 246 | 247 | // Select INBOX 248 | _, err := c.Select("INBOX", false) 249 | if err != nil { 250 | log.Fatal(err) 251 | } 252 | 253 | // Set search criteria 254 | criteria := imap.NewSearchCriteria() 255 | criteria.WithoutFlags = []string{imap.SeenFlag} 256 | ids, err := c.Search(criteria) 257 | if err != nil { 258 | log.Fatal(err) 259 | } 260 | log.Println("IDs found:", ids) 261 | 262 | if len(ids) > 0 { 263 | seqset := new(imap.SeqSet) 264 | seqset.AddNum(ids...) 265 | 266 | messages := make(chan *imap.Message, 10) 267 | done := make(chan error, 1) 268 | go func() { 269 | done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages) 270 | }() 271 | 272 | log.Println("Unseen messages:") 273 | for msg := range messages { 274 | log.Println("* " + msg.Envelope.Subject) 275 | } 276 | 277 | if err := <-done; err != nil { 278 | log.Fatal(err) 279 | } 280 | } 281 | 282 | log.Println("Done!") 283 | } 284 | -------------------------------------------------------------------------------- /client/tag.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | ) 7 | 8 | func randomString(n int) (string, error) { 9 | b := make([]byte, n) 10 | _, err := rand.Read(b) 11 | if err != nil { 12 | return "", err 13 | } 14 | 15 | return base64.RawURLEncoding.EncodeToString(b), nil 16 | } 17 | 18 | func generateTag() string { 19 | tag, err := randomString(4) 20 | if err != nil { 21 | panic(err) 22 | } 23 | return tag 24 | } 25 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package imap 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | // A value that can be converted to a command. 9 | type Commander interface { 10 | Command() *Command 11 | } 12 | 13 | // A command. 14 | type Command struct { 15 | // The command tag. It acts as a unique identifier for this command. If empty, 16 | // the command is untagged. 17 | Tag string 18 | // The command name. 19 | Name string 20 | // The command arguments. 21 | Arguments []interface{} 22 | } 23 | 24 | // Implements the Commander interface. 25 | func (cmd *Command) Command() *Command { 26 | return cmd 27 | } 28 | 29 | func (cmd *Command) WriteTo(w *Writer) error { 30 | tag := cmd.Tag 31 | if tag == "" { 32 | tag = "*" 33 | } 34 | 35 | fields := []interface{}{RawString(tag), RawString(cmd.Name)} 36 | fields = append(fields, cmd.Arguments...) 37 | return w.writeLine(fields...) 38 | } 39 | 40 | // Parse a command from fields. 41 | func (cmd *Command) Parse(fields []interface{}) error { 42 | if len(fields) < 2 { 43 | return errors.New("imap: cannot parse command: no enough fields") 44 | } 45 | 46 | var ok bool 47 | if cmd.Tag, ok = fields[0].(string); !ok { 48 | return errors.New("imap: cannot parse command: invalid tag") 49 | } 50 | if cmd.Name, ok = fields[1].(string); !ok { 51 | return errors.New("imap: cannot parse command: invalid name") 52 | } 53 | cmd.Name = strings.ToUpper(cmd.Name) // Command names are case-insensitive 54 | 55 | cmd.Arguments = fields[2:] 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package imap_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/emersion/go-imap" 8 | ) 9 | 10 | func TestCommand_Command(t *testing.T) { 11 | cmd := &imap.Command{ 12 | Tag: "A001", 13 | Name: "NOOP", 14 | } 15 | 16 | if cmd.Command() != cmd { 17 | t.Error("Command should return itself") 18 | } 19 | } 20 | 21 | func TestCommand_WriteTo_NoArgs(t *testing.T) { 22 | var b bytes.Buffer 23 | w := imap.NewWriter(&b) 24 | 25 | cmd := &imap.Command{ 26 | Tag: "A001", 27 | Name: "NOOP", 28 | } 29 | 30 | if err := cmd.WriteTo(w); err != nil { 31 | t.Fatal(err) 32 | } 33 | if b.String() != "A001 NOOP\r\n" { 34 | t.Fatal("Not the expected command: ", b.String()) 35 | } 36 | } 37 | 38 | func TestCommand_WriteTo_WithArgs(t *testing.T) { 39 | var b bytes.Buffer 40 | w := imap.NewWriter(&b) 41 | 42 | cmd := &imap.Command{ 43 | Tag: "A002", 44 | Name: "LOGIN", 45 | Arguments: []interface{}{"username", "password"}, 46 | } 47 | 48 | if err := cmd.WriteTo(w); err != nil { 49 | t.Fatal(err) 50 | } 51 | if b.String() != "A002 LOGIN \"username\" \"password\"\r\n" { 52 | t.Fatal("Not the expected command: ", b.String()) 53 | } 54 | } 55 | 56 | func TestCommand_Parse_NoArgs(t *testing.T) { 57 | fields := []interface{}{"a", "NOOP"} 58 | 59 | cmd := &imap.Command{} 60 | 61 | if err := cmd.Parse(fields); err != nil { 62 | t.Fatal(err) 63 | } 64 | if cmd.Tag != "a" { 65 | t.Error("Invalid tag:", cmd.Tag) 66 | } 67 | if cmd.Name != "NOOP" { 68 | t.Error("Invalid name:", cmd.Name) 69 | } 70 | if len(cmd.Arguments) != 0 { 71 | t.Error("Invalid arguments:", cmd.Arguments) 72 | } 73 | } 74 | 75 | func TestCommand_Parse_WithArgs(t *testing.T) { 76 | fields := []interface{}{"a", "LOGIN", "username", "password"} 77 | 78 | cmd := &imap.Command{} 79 | 80 | if err := cmd.Parse(fields); err != nil { 81 | t.Fatal(err) 82 | } 83 | if cmd.Tag != "a" { 84 | t.Error("Invalid tag:", cmd.Tag) 85 | } 86 | if cmd.Name != "LOGIN" { 87 | t.Error("Invalid name:", cmd.Name) 88 | } 89 | if len(cmd.Arguments) != 2 { 90 | t.Error("Invalid arguments:", cmd.Arguments) 91 | } 92 | if username, ok := cmd.Arguments[0].(string); !ok || username != "username" { 93 | t.Error("Invalid first argument:", cmd.Arguments[0]) 94 | } 95 | if password, ok := cmd.Arguments[1].(string); !ok || password != "password" { 96 | t.Error("Invalid second argument:", cmd.Arguments[1]) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /commands/append.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/emersion/go-imap" 8 | "github.com/emersion/go-imap/utf7" 9 | ) 10 | 11 | // Append is an APPEND command, as defined in RFC 3501 section 6.3.11. 12 | type Append struct { 13 | Mailbox string 14 | Flags []string 15 | Date time.Time 16 | Message imap.Literal 17 | } 18 | 19 | func (cmd *Append) Command() *imap.Command { 20 | var args []interface{} 21 | 22 | mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) 23 | args = append(args, imap.FormatMailboxName(mailbox)) 24 | 25 | if cmd.Flags != nil { 26 | flags := make([]interface{}, len(cmd.Flags)) 27 | for i, flag := range cmd.Flags { 28 | flags[i] = imap.RawString(flag) 29 | } 30 | args = append(args, flags) 31 | } 32 | 33 | if !cmd.Date.IsZero() { 34 | args = append(args, cmd.Date) 35 | } 36 | 37 | args = append(args, cmd.Message) 38 | 39 | return &imap.Command{ 40 | Name: "APPEND", 41 | Arguments: args, 42 | } 43 | } 44 | 45 | func (cmd *Append) Parse(fields []interface{}) (err error) { 46 | if len(fields) < 2 { 47 | return errors.New("No enough arguments") 48 | } 49 | 50 | // Parse mailbox name 51 | if mailbox, err := imap.ParseString(fields[0]); err != nil { 52 | return err 53 | } else if mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil { 54 | return err 55 | } else { 56 | cmd.Mailbox = imap.CanonicalMailboxName(mailbox) 57 | } 58 | 59 | // Parse message literal 60 | litIndex := len(fields) - 1 61 | var ok bool 62 | if cmd.Message, ok = fields[litIndex].(imap.Literal); !ok { 63 | return errors.New("Message must be a literal") 64 | } 65 | 66 | // Remaining fields a optional 67 | fields = fields[1:litIndex] 68 | if len(fields) > 0 { 69 | // Parse flags list 70 | if flags, ok := fields[0].([]interface{}); ok { 71 | if cmd.Flags, err = imap.ParseStringList(flags); err != nil { 72 | return err 73 | } 74 | 75 | for i, flag := range cmd.Flags { 76 | cmd.Flags[i] = imap.CanonicalFlag(flag) 77 | } 78 | 79 | fields = fields[1:] 80 | } 81 | 82 | // Parse date 83 | if len(fields) > 0 { 84 | if date, ok := fields[0].(string); !ok { 85 | return errors.New("Date must be a string") 86 | } else if cmd.Date, err = time.Parse(imap.DateTimeLayout, date); err != nil { 87 | return err 88 | } 89 | } 90 | } 91 | 92 | return 93 | } 94 | -------------------------------------------------------------------------------- /commands/authenticate.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bufio" 5 | "encoding/base64" 6 | "errors" 7 | "io" 8 | "strings" 9 | 10 | "github.com/emersion/go-imap" 11 | "github.com/emersion/go-sasl" 12 | ) 13 | 14 | // AuthenticateConn is a connection that supports IMAP authentication. 15 | type AuthenticateConn interface { 16 | io.Reader 17 | 18 | // WriteResp writes an IMAP response to this connection. 19 | WriteResp(res imap.WriterTo) error 20 | } 21 | 22 | // Authenticate is an AUTHENTICATE command, as defined in RFC 3501 section 23 | // 6.2.2. 24 | type Authenticate struct { 25 | Mechanism string 26 | InitialResponse []byte 27 | } 28 | 29 | func (cmd *Authenticate) Command() *imap.Command { 30 | args := []interface{}{imap.RawString(cmd.Mechanism)} 31 | if cmd.InitialResponse != nil { 32 | var encodedResponse string 33 | if len(cmd.InitialResponse) == 0 { 34 | // Empty initial response should be encoded as "=", not empty 35 | // string. 36 | encodedResponse = "=" 37 | } else { 38 | encodedResponse = base64.StdEncoding.EncodeToString(cmd.InitialResponse) 39 | } 40 | 41 | args = append(args, imap.RawString(encodedResponse)) 42 | } 43 | return &imap.Command{ 44 | Name: "AUTHENTICATE", 45 | Arguments: args, 46 | } 47 | } 48 | 49 | func (cmd *Authenticate) Parse(fields []interface{}) error { 50 | if len(fields) < 1 { 51 | return errors.New("Not enough arguments") 52 | } 53 | 54 | var ok bool 55 | if cmd.Mechanism, ok = fields[0].(string); !ok { 56 | return errors.New("Mechanism must be a string") 57 | } 58 | cmd.Mechanism = strings.ToUpper(cmd.Mechanism) 59 | 60 | if len(fields) != 2 { 61 | return nil 62 | } 63 | 64 | encodedResponse, ok := fields[1].(string) 65 | if !ok { 66 | return errors.New("Initial response must be a string") 67 | } 68 | if encodedResponse == "=" { 69 | cmd.InitialResponse = []byte{} 70 | return nil 71 | } 72 | 73 | var err error 74 | cmd.InitialResponse, err = base64.StdEncoding.DecodeString(encodedResponse) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func (cmd *Authenticate) Handle(mechanisms map[string]sasl.Server, conn AuthenticateConn) error { 83 | sasl, ok := mechanisms[cmd.Mechanism] 84 | if !ok { 85 | return errors.New("Unsupported mechanism") 86 | } 87 | 88 | scanner := bufio.NewScanner(conn) 89 | 90 | response := cmd.InitialResponse 91 | for { 92 | challenge, done, err := sasl.Next(response) 93 | if err != nil || done { 94 | return err 95 | } 96 | 97 | encoded := base64.StdEncoding.EncodeToString(challenge) 98 | cont := &imap.ContinuationReq{Info: encoded} 99 | if err := conn.WriteResp(cont); err != nil { 100 | return err 101 | } 102 | 103 | if !scanner.Scan() { 104 | if err := scanner.Err(); err != nil { 105 | return err 106 | } 107 | return errors.New("unexpected EOF") 108 | } 109 | 110 | encoded = scanner.Text() 111 | if encoded != "" { 112 | if encoded == "*" { 113 | return &imap.ErrStatusResp{Resp: &imap.StatusResp{ 114 | Type: imap.StatusRespBad, 115 | Info: "negotiation cancelled", 116 | }} 117 | } 118 | response, err = base64.StdEncoding.DecodeString(encoded) 119 | if err != nil { 120 | return err 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /commands/capability.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | // Capability is a CAPABILITY command, as defined in RFC 3501 section 6.1.1. 8 | type Capability struct{} 9 | 10 | func (c *Capability) Command() *imap.Command { 11 | return &imap.Command{ 12 | Name: "CAPABILITY", 13 | } 14 | } 15 | 16 | func (c *Capability) Parse(fields []interface{}) error { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /commands/check.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | // Check is a CHECK command, as defined in RFC 3501 section 6.4.1. 8 | type Check struct{} 9 | 10 | func (cmd *Check) Command() *imap.Command { 11 | return &imap.Command{ 12 | Name: "CHECK", 13 | } 14 | } 15 | 16 | func (cmd *Check) Parse(fields []interface{}) error { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /commands/close.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | // Close is a CLOSE command, as defined in RFC 3501 section 6.4.2. 8 | type Close struct{} 9 | 10 | func (cmd *Close) Command() *imap.Command { 11 | return &imap.Command{ 12 | Name: "CLOSE", 13 | } 14 | } 15 | 16 | func (cmd *Close) Parse(fields []interface{}) error { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /commands/commands.go: -------------------------------------------------------------------------------- 1 | // Package commands implements IMAP commands defined in RFC 3501. 2 | package commands 3 | -------------------------------------------------------------------------------- /commands/copy.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-imap" 7 | "github.com/emersion/go-imap/utf7" 8 | ) 9 | 10 | // Copy is a COPY command, as defined in RFC 3501 section 6.4.7. 11 | type Copy struct { 12 | SeqSet *imap.SeqSet 13 | Mailbox string 14 | } 15 | 16 | func (cmd *Copy) Command() *imap.Command { 17 | mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) 18 | 19 | return &imap.Command{ 20 | Name: "COPY", 21 | Arguments: []interface{}{cmd.SeqSet, imap.FormatMailboxName(mailbox)}, 22 | } 23 | } 24 | 25 | func (cmd *Copy) Parse(fields []interface{}) error { 26 | if len(fields) < 2 { 27 | return errors.New("No enough arguments") 28 | } 29 | 30 | if seqSet, ok := fields[0].(string); !ok { 31 | return errors.New("Invalid sequence set") 32 | } else if seqSet, err := imap.ParseSeqSet(seqSet); err != nil { 33 | return err 34 | } else { 35 | cmd.SeqSet = seqSet 36 | } 37 | 38 | if mailbox, err := imap.ParseString(fields[1]); err != nil { 39 | return err 40 | } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { 41 | return err 42 | } else { 43 | cmd.Mailbox = imap.CanonicalMailboxName(mailbox) 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /commands/create.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-imap" 7 | "github.com/emersion/go-imap/utf7" 8 | ) 9 | 10 | // Create is a CREATE command, as defined in RFC 3501 section 6.3.3. 11 | type Create struct { 12 | Mailbox string 13 | } 14 | 15 | func (cmd *Create) Command() *imap.Command { 16 | mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) 17 | 18 | return &imap.Command{ 19 | Name: "CREATE", 20 | Arguments: []interface{}{mailbox}, 21 | } 22 | } 23 | 24 | func (cmd *Create) Parse(fields []interface{}) error { 25 | if len(fields) < 1 { 26 | return errors.New("No enough arguments") 27 | } 28 | 29 | if mailbox, err := imap.ParseString(fields[0]); err != nil { 30 | return err 31 | } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { 32 | return err 33 | } else { 34 | cmd.Mailbox = imap.CanonicalMailboxName(mailbox) 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /commands/delete.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-imap" 7 | "github.com/emersion/go-imap/utf7" 8 | ) 9 | 10 | // Delete is a DELETE command, as defined in RFC 3501 section 6.3.3. 11 | type Delete struct { 12 | Mailbox string 13 | } 14 | 15 | func (cmd *Delete) Command() *imap.Command { 16 | mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) 17 | 18 | return &imap.Command{ 19 | Name: "DELETE", 20 | Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, 21 | } 22 | } 23 | 24 | func (cmd *Delete) Parse(fields []interface{}) error { 25 | if len(fields) < 1 { 26 | return errors.New("No enough arguments") 27 | } 28 | 29 | if mailbox, err := imap.ParseString(fields[0]); err != nil { 30 | return err 31 | } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { 32 | return err 33 | } else { 34 | cmd.Mailbox = imap.CanonicalMailboxName(mailbox) 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /commands/expunge.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | // Expunge is an EXPUNGE command, as defined in RFC 3501 section 6.4.3. 8 | type Expunge struct{} 9 | 10 | func (cmd *Expunge) Command() *imap.Command { 11 | return &imap.Command{Name: "EXPUNGE"} 12 | } 13 | 14 | func (cmd *Expunge) Parse(fields []interface{}) error { 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /commands/fetch.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/emersion/go-imap" 8 | ) 9 | 10 | // Fetch is a FETCH command, as defined in RFC 3501 section 6.4.5. 11 | type Fetch struct { 12 | SeqSet *imap.SeqSet 13 | Items []imap.FetchItem 14 | } 15 | 16 | func (cmd *Fetch) Command() *imap.Command { 17 | items := make([]interface{}, len(cmd.Items)) 18 | for i, item := range cmd.Items { 19 | items[i] = imap.RawString(item) 20 | } 21 | 22 | return &imap.Command{ 23 | Name: "FETCH", 24 | Arguments: []interface{}{cmd.SeqSet, items}, 25 | } 26 | } 27 | 28 | func (cmd *Fetch) Parse(fields []interface{}) error { 29 | if len(fields) < 2 { 30 | return errors.New("No enough arguments") 31 | } 32 | 33 | var err error 34 | if seqset, ok := fields[0].(string); !ok { 35 | return errors.New("Sequence set must be an atom") 36 | } else if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil { 37 | return err 38 | } 39 | 40 | switch items := fields[1].(type) { 41 | case string: // A macro or a single item 42 | cmd.Items = imap.FetchItem(strings.ToUpper(items)).Expand() 43 | case []interface{}: // A list of items 44 | cmd.Items = make([]imap.FetchItem, 0, len(items)) 45 | for _, v := range items { 46 | itemStr, _ := v.(string) 47 | item := imap.FetchItem(strings.ToUpper(itemStr)) 48 | cmd.Items = append(cmd.Items, item.Expand()...) 49 | } 50 | default: 51 | return errors.New("Items must be either a string or a list") 52 | } 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /commands/list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-imap" 7 | "github.com/emersion/go-imap/utf7" 8 | ) 9 | 10 | // List is a LIST command, as defined in RFC 3501 section 6.3.8. If Subscribed 11 | // is set to true, LSUB will be used instead. 12 | type List struct { 13 | Reference string 14 | Mailbox string 15 | 16 | Subscribed bool 17 | } 18 | 19 | func (cmd *List) Command() *imap.Command { 20 | name := "LIST" 21 | if cmd.Subscribed { 22 | name = "LSUB" 23 | } 24 | 25 | enc := utf7.Encoding.NewEncoder() 26 | ref, _ := enc.String(cmd.Reference) 27 | mailbox, _ := enc.String(cmd.Mailbox) 28 | 29 | return &imap.Command{ 30 | Name: name, 31 | Arguments: []interface{}{ref, mailbox}, 32 | } 33 | } 34 | 35 | func (cmd *List) Parse(fields []interface{}) error { 36 | if len(fields) < 2 { 37 | return errors.New("No enough arguments") 38 | } 39 | 40 | dec := utf7.Encoding.NewDecoder() 41 | 42 | if mailbox, err := imap.ParseString(fields[0]); err != nil { 43 | return err 44 | } else if mailbox, err := dec.String(mailbox); err != nil { 45 | return err 46 | } else { 47 | // TODO: canonical mailbox path 48 | cmd.Reference = imap.CanonicalMailboxName(mailbox) 49 | } 50 | 51 | if mailbox, err := imap.ParseString(fields[1]); err != nil { 52 | return err 53 | } else if mailbox, err := dec.String(mailbox); err != nil { 54 | return err 55 | } else { 56 | cmd.Mailbox = imap.CanonicalMailboxName(mailbox) 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /commands/login.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-imap" 7 | ) 8 | 9 | // Login is a LOGIN command, as defined in RFC 3501 section 6.2.2. 10 | type Login struct { 11 | Username string 12 | Password string 13 | } 14 | 15 | func (cmd *Login) Command() *imap.Command { 16 | return &imap.Command{ 17 | Name: "LOGIN", 18 | Arguments: []interface{}{cmd.Username, cmd.Password}, 19 | } 20 | } 21 | 22 | func (cmd *Login) Parse(fields []interface{}) error { 23 | if len(fields) < 2 { 24 | return errors.New("Not enough arguments") 25 | } 26 | 27 | var err error 28 | if cmd.Username, err = imap.ParseString(fields[0]); err != nil { 29 | return err 30 | } 31 | if cmd.Password, err = imap.ParseString(fields[1]); err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /commands/logout.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | // Logout is a LOGOUT command, as defined in RFC 3501 section 6.1.3. 8 | type Logout struct{} 9 | 10 | func (c *Logout) Command() *imap.Command { 11 | return &imap.Command{ 12 | Name: "LOGOUT", 13 | } 14 | } 15 | 16 | func (c *Logout) Parse(fields []interface{}) error { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /commands/noop.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | // Noop is a NOOP command, as defined in RFC 3501 section 6.1.2. 8 | type Noop struct{} 9 | 10 | func (c *Noop) Command() *imap.Command { 11 | return &imap.Command{ 12 | Name: "NOOP", 13 | } 14 | } 15 | 16 | func (c *Noop) Parse(fields []interface{}) error { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /commands/rename.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-imap" 7 | "github.com/emersion/go-imap/utf7" 8 | ) 9 | 10 | // Rename is a RENAME command, as defined in RFC 3501 section 6.3.5. 11 | type Rename struct { 12 | Existing string 13 | New string 14 | } 15 | 16 | func (cmd *Rename) Command() *imap.Command { 17 | enc := utf7.Encoding.NewEncoder() 18 | existingName, _ := enc.String(cmd.Existing) 19 | newName, _ := enc.String(cmd.New) 20 | 21 | return &imap.Command{ 22 | Name: "RENAME", 23 | Arguments: []interface{}{imap.FormatMailboxName(existingName), imap.FormatMailboxName(newName)}, 24 | } 25 | } 26 | 27 | func (cmd *Rename) Parse(fields []interface{}) error { 28 | if len(fields) < 2 { 29 | return errors.New("No enough arguments") 30 | } 31 | 32 | dec := utf7.Encoding.NewDecoder() 33 | 34 | if existingName, err := imap.ParseString(fields[0]); err != nil { 35 | return err 36 | } else if existingName, err := dec.String(existingName); err != nil { 37 | return err 38 | } else { 39 | cmd.Existing = imap.CanonicalMailboxName(existingName) 40 | } 41 | 42 | if newName, err := imap.ParseString(fields[1]); err != nil { 43 | return err 44 | } else if newName, err := dec.String(newName); err != nil { 45 | return err 46 | } else { 47 | cmd.New = imap.CanonicalMailboxName(newName) 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /commands/search.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "strings" 7 | 8 | "github.com/emersion/go-imap" 9 | ) 10 | 11 | // Search is a SEARCH command, as defined in RFC 3501 section 6.4.4. 12 | type Search struct { 13 | Charset string 14 | Criteria *imap.SearchCriteria 15 | } 16 | 17 | func (cmd *Search) Command() *imap.Command { 18 | var args []interface{} 19 | if cmd.Charset != "" { 20 | args = append(args, imap.RawString("CHARSET"), imap.RawString(cmd.Charset)) 21 | } 22 | args = append(args, cmd.Criteria.Format()...) 23 | 24 | return &imap.Command{ 25 | Name: "SEARCH", 26 | Arguments: args, 27 | } 28 | } 29 | 30 | func (cmd *Search) Parse(fields []interface{}) error { 31 | if len(fields) == 0 { 32 | return errors.New("Missing search criteria") 33 | } 34 | 35 | // Parse charset 36 | if f, ok := fields[0].(string); ok && strings.EqualFold(f, "CHARSET") { 37 | if len(fields) < 2 { 38 | return errors.New("Missing CHARSET value") 39 | } 40 | if cmd.Charset, ok = fields[1].(string); !ok { 41 | return errors.New("Charset must be a string") 42 | } 43 | fields = fields[2:] 44 | } 45 | 46 | var charsetReader func(io.Reader) io.Reader 47 | charset := strings.ToLower(cmd.Charset) 48 | if charset != "utf-8" && charset != "us-ascii" && charset != "" { 49 | charsetReader = func(r io.Reader) io.Reader { 50 | r, _ = imap.CharsetReader(charset, r) 51 | return r 52 | } 53 | } 54 | 55 | cmd.Criteria = new(imap.SearchCriteria) 56 | return cmd.Criteria.ParseWithCharset(fields, charsetReader) 57 | } 58 | -------------------------------------------------------------------------------- /commands/select.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-imap" 7 | "github.com/emersion/go-imap/utf7" 8 | ) 9 | 10 | // Select is a SELECT command, as defined in RFC 3501 section 6.3.1. If ReadOnly 11 | // is set to true, the EXAMINE command will be used instead. 12 | type Select struct { 13 | Mailbox string 14 | ReadOnly bool 15 | } 16 | 17 | func (cmd *Select) Command() *imap.Command { 18 | name := "SELECT" 19 | if cmd.ReadOnly { 20 | name = "EXAMINE" 21 | } 22 | 23 | mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) 24 | 25 | return &imap.Command{ 26 | Name: name, 27 | Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, 28 | } 29 | } 30 | 31 | func (cmd *Select) Parse(fields []interface{}) error { 32 | if len(fields) < 1 { 33 | return errors.New("No enough arguments") 34 | } 35 | 36 | if mailbox, err := imap.ParseString(fields[0]); err != nil { 37 | return err 38 | } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { 39 | return err 40 | } else { 41 | cmd.Mailbox = imap.CanonicalMailboxName(mailbox) 42 | } 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /commands/starttls.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | // StartTLS is a STARTTLS command, as defined in RFC 3501 section 6.2.1. 8 | type StartTLS struct{} 9 | 10 | func (cmd *StartTLS) Command() *imap.Command { 11 | return &imap.Command{ 12 | Name: "STARTTLS", 13 | } 14 | } 15 | 16 | func (cmd *StartTLS) Parse(fields []interface{}) error { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /commands/status.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/emersion/go-imap" 8 | "github.com/emersion/go-imap/utf7" 9 | ) 10 | 11 | // Status is a STATUS command, as defined in RFC 3501 section 6.3.10. 12 | type Status struct { 13 | Mailbox string 14 | Items []imap.StatusItem 15 | } 16 | 17 | func (cmd *Status) Command() *imap.Command { 18 | mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) 19 | 20 | items := make([]interface{}, len(cmd.Items)) 21 | for i, item := range cmd.Items { 22 | items[i] = imap.RawString(item) 23 | } 24 | 25 | return &imap.Command{ 26 | Name: "STATUS", 27 | Arguments: []interface{}{imap.FormatMailboxName(mailbox), items}, 28 | } 29 | } 30 | 31 | func (cmd *Status) Parse(fields []interface{}) error { 32 | if len(fields) < 2 { 33 | return errors.New("No enough arguments") 34 | } 35 | 36 | if mailbox, err := imap.ParseString(fields[0]); err != nil { 37 | return err 38 | } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { 39 | return err 40 | } else { 41 | cmd.Mailbox = imap.CanonicalMailboxName(mailbox) 42 | } 43 | 44 | items, ok := fields[1].([]interface{}) 45 | if !ok { 46 | return errors.New("STATUS command parameter is not a list") 47 | } 48 | cmd.Items = make([]imap.StatusItem, len(items)) 49 | for i, f := range items { 50 | if s, ok := f.(string); !ok { 51 | return errors.New("Got a non-string field in a STATUS command parameter") 52 | } else { 53 | cmd.Items[i] = imap.StatusItem(strings.ToUpper(s)) 54 | } 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /commands/store.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/emersion/go-imap" 8 | ) 9 | 10 | // Store is a STORE command, as defined in RFC 3501 section 6.4.6. 11 | type Store struct { 12 | SeqSet *imap.SeqSet 13 | Item imap.StoreItem 14 | Value interface{} 15 | } 16 | 17 | func (cmd *Store) Command() *imap.Command { 18 | return &imap.Command{ 19 | Name: "STORE", 20 | Arguments: []interface{}{cmd.SeqSet, imap.RawString(cmd.Item), cmd.Value}, 21 | } 22 | } 23 | 24 | func (cmd *Store) Parse(fields []interface{}) error { 25 | if len(fields) < 3 { 26 | return errors.New("No enough arguments") 27 | } 28 | 29 | seqset, ok := fields[0].(string) 30 | if !ok { 31 | return errors.New("Invalid sequence set") 32 | } 33 | var err error 34 | if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil { 35 | return err 36 | } 37 | 38 | if item, ok := fields[1].(string); !ok { 39 | return errors.New("Item name must be a string") 40 | } else { 41 | cmd.Item = imap.StoreItem(strings.ToUpper(item)) 42 | } 43 | 44 | if len(fields[2:]) == 1 { 45 | cmd.Value = fields[2] 46 | } else { 47 | cmd.Value = fields[2:] 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /commands/subscribe.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-imap" 7 | "github.com/emersion/go-imap/utf7" 8 | ) 9 | 10 | // Subscribe is a SUBSCRIBE command, as defined in RFC 3501 section 6.3.6. 11 | type Subscribe struct { 12 | Mailbox string 13 | } 14 | 15 | func (cmd *Subscribe) Command() *imap.Command { 16 | mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) 17 | 18 | return &imap.Command{ 19 | Name: "SUBSCRIBE", 20 | Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, 21 | } 22 | } 23 | 24 | func (cmd *Subscribe) Parse(fields []interface{}) error { 25 | if len(fields) < 0 { 26 | return errors.New("No enough arguments") 27 | } 28 | 29 | if mailbox, err := imap.ParseString(fields[0]); err != nil { 30 | return err 31 | } else if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil { 32 | return err 33 | } 34 | return nil 35 | } 36 | 37 | // An UNSUBSCRIBE command. 38 | // See RFC 3501 section 6.3.7 39 | type Unsubscribe struct { 40 | Mailbox string 41 | } 42 | 43 | func (cmd *Unsubscribe) Command() *imap.Command { 44 | mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) 45 | 46 | return &imap.Command{ 47 | Name: "UNSUBSCRIBE", 48 | Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, 49 | } 50 | } 51 | 52 | func (cmd *Unsubscribe) Parse(fields []interface{}) error { 53 | if len(fields) < 0 { 54 | return errors.New("No enogh arguments") 55 | } 56 | 57 | if mailbox, err := imap.ParseString(fields[0]); err != nil { 58 | return err 59 | } else if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil { 60 | return err 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /commands/uid.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/emersion/go-imap" 8 | ) 9 | 10 | // Uid is a UID command, as defined in RFC 3501 section 6.4.8. It wraps another 11 | // command (e.g. wrapping a Fetch command will result in a UID FETCH). 12 | type Uid struct { 13 | Cmd imap.Commander 14 | } 15 | 16 | func (cmd *Uid) Command() *imap.Command { 17 | inner := cmd.Cmd.Command() 18 | 19 | args := []interface{}{imap.RawString(inner.Name)} 20 | args = append(args, inner.Arguments...) 21 | 22 | return &imap.Command{ 23 | Name: "UID", 24 | Arguments: args, 25 | } 26 | } 27 | 28 | func (cmd *Uid) Parse(fields []interface{}) error { 29 | if len(fields) < 0 { 30 | return errors.New("No command name specified") 31 | } 32 | 33 | name, ok := fields[0].(string) 34 | if !ok { 35 | return errors.New("Command name must be a string") 36 | } 37 | 38 | cmd.Cmd = &imap.Command{ 39 | Name: strings.ToUpper(name), // Command names are case-insensitive 40 | Arguments: fields[1:], 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /conn_test.go: -------------------------------------------------------------------------------- 1 | package imap_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | "testing" 8 | 9 | "github.com/emersion/go-imap" 10 | ) 11 | 12 | func TestNewConn(t *testing.T) { 13 | b := &bytes.Buffer{} 14 | c, s := net.Pipe() 15 | 16 | done := make(chan error) 17 | go (func() { 18 | _, err := io.Copy(b, s) 19 | done <- err 20 | })() 21 | 22 | r := imap.NewReader(nil) 23 | w := imap.NewWriter(nil) 24 | 25 | ic := imap.NewConn(c, r, w) 26 | 27 | sent := []byte("hi") 28 | ic.Write(sent) 29 | ic.Flush() 30 | ic.Close() 31 | 32 | if err := <-done; err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | s.Close() 37 | 38 | received := b.Bytes() 39 | if string(sent) != string(received) { 40 | t.Errorf("Sent %v but received %v", sent, received) 41 | } 42 | } 43 | 44 | func transform(b []byte) []byte { 45 | bb := make([]byte, len(b)) 46 | 47 | for i, c := range b { 48 | if rune(c) == 'c' { 49 | bb[i] = byte('d') 50 | } else { 51 | bb[i] = c 52 | } 53 | } 54 | 55 | return bb 56 | } 57 | 58 | type upgraded struct { 59 | net.Conn 60 | } 61 | 62 | func (c *upgraded) Write(b []byte) (int, error) { 63 | return c.Conn.Write(transform(b)) 64 | } 65 | 66 | func TestConn_Upgrade(t *testing.T) { 67 | b := &bytes.Buffer{} 68 | c, s := net.Pipe() 69 | 70 | done := make(chan error) 71 | go (func() { 72 | _, err := io.Copy(b, s) 73 | done <- err 74 | })() 75 | 76 | r := imap.NewReader(nil) 77 | w := imap.NewWriter(nil) 78 | 79 | ic := imap.NewConn(c, r, w) 80 | 81 | began := make(chan struct{}) 82 | go ic.Upgrade(func(conn net.Conn) (net.Conn, error) { 83 | began <- struct{}{} 84 | ic.WaitReady() 85 | return &upgraded{conn}, nil 86 | }) 87 | <-began 88 | 89 | ic.Wait() 90 | 91 | sent := []byte("abcd") 92 | expected := transform(sent) 93 | ic.Write(sent) 94 | ic.Flush() 95 | ic.Close() 96 | 97 | if err := <-done; err != nil { 98 | t.Fatal(err) 99 | } 100 | 101 | s.Close() 102 | 103 | received := b.Bytes() 104 | if string(expected) != string(received) { 105 | t.Errorf("Expected %v but received %v", expected, received) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /date.go: -------------------------------------------------------------------------------- 1 | package imap 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "time" 7 | ) 8 | 9 | // Date and time layouts. 10 | // Dovecot adds a leading zero to dates: 11 | // https://github.com/dovecot/core/blob/4fbd5c5e113078e72f29465ccc96d44955ceadc2/src/lib-imap/imap-date.c#L166 12 | // Cyrus adds a leading space to dates: 13 | // https://github.com/cyrusimap/cyrus-imapd/blob/1cb805a3bffbdf829df0964f3b802cdc917e76db/lib/times.c#L543 14 | // GMail doesn't support leading spaces in dates used in SEARCH commands. 15 | const ( 16 | // Defined in RFC 3501 as date-text on page 83. 17 | DateLayout = "_2-Jan-2006" 18 | // Defined in RFC 3501 as date-time on page 83. 19 | DateTimeLayout = "_2-Jan-2006 15:04:05 -0700" 20 | // Defined in RFC 5322 section 3.3, mentioned as env-date in RFC 3501 page 84. 21 | envelopeDateTimeLayout = "Mon, 02 Jan 2006 15:04:05 -0700" 22 | // Use as an example in RFC 3501 page 54. 23 | searchDateLayout = "2-Jan-2006" 24 | ) 25 | 26 | // time.Time with a specific layout. 27 | type ( 28 | Date time.Time 29 | DateTime time.Time 30 | envelopeDateTime time.Time 31 | searchDate time.Time 32 | ) 33 | 34 | // Permutations of the layouts defined in RFC 5322, section 3.3. 35 | var envelopeDateTimeLayouts = [...]string{ 36 | envelopeDateTimeLayout, // popular, try it first 37 | "_2 Jan 2006 15:04:05 -0700", 38 | "_2 Jan 2006 15:04:05 MST", 39 | "_2 Jan 2006 15:04 -0700", 40 | "_2 Jan 2006 15:04 MST", 41 | "_2 Jan 06 15:04:05 -0700", 42 | "_2 Jan 06 15:04:05 MST", 43 | "_2 Jan 06 15:04 -0700", 44 | "_2 Jan 06 15:04 MST", 45 | "Mon, _2 Jan 2006 15:04:05 -0700", 46 | "Mon, _2 Jan 2006 15:04:05 MST", 47 | "Mon, _2 Jan 2006 15:04 -0700", 48 | "Mon, _2 Jan 2006 15:04 MST", 49 | "Mon, _2 Jan 06 15:04:05 -0700", 50 | "Mon, _2 Jan 06 15:04:05 MST", 51 | "Mon, _2 Jan 06 15:04 -0700", 52 | "Mon, _2 Jan 06 15:04 MST", 53 | } 54 | 55 | // TODO: this is a blunt way to strip any trailing CFWS (comment). A sharper 56 | // one would strip multiple CFWS, and only if really valid according to 57 | // RFC5322. 58 | var commentRE = regexp.MustCompile(`[ \t]+\(.*\)$`) 59 | 60 | // Try parsing the date based on the layouts defined in RFC 5322, section 3.3. 61 | // Inspired by https://github.com/golang/go/blob/master/src/net/mail/message.go 62 | func parseMessageDateTime(maybeDate string) (time.Time, error) { 63 | maybeDate = commentRE.ReplaceAllString(maybeDate, "") 64 | for _, layout := range envelopeDateTimeLayouts { 65 | parsed, err := time.Parse(layout, maybeDate) 66 | if err == nil { 67 | return parsed, nil 68 | } 69 | } 70 | return time.Time{}, fmt.Errorf("date %s could not be parsed", maybeDate) 71 | } 72 | -------------------------------------------------------------------------------- /date_test.go: -------------------------------------------------------------------------------- 1 | package imap 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | var expectedDateTime = time.Date(2009, time.November, 2, 23, 0, 0, 0, time.FixedZone("", -6*60*60)) 9 | var expectedDate = time.Date(2009, time.November, 2, 0, 0, 0, 0, time.FixedZone("", 0)) 10 | 11 | func TestParseMessageDateTime(t *testing.T) { 12 | tests := []struct { 13 | in string 14 | out time.Time 15 | ok bool 16 | }{ 17 | // some permutations 18 | {"2 Nov 2009 23:00 -0600", expectedDateTime, true}, 19 | {"Tue, 2 Nov 2009 23:00:00 -0600", expectedDateTime, true}, 20 | {"Tue, 2 Nov 2009 23:00:00 -0600 (MST)", expectedDateTime, true}, 21 | 22 | // whitespace 23 | {" 2 Nov 2009 23:00 -0600", expectedDateTime, true}, 24 | {"Tue, 2 Nov 2009 23:00:00 -0600", expectedDateTime, true}, 25 | {"Tue, 2 Nov 2009 23:00:00 -0600 (MST)", expectedDateTime, true}, 26 | 27 | // invalid 28 | {"abc10 Nov 2009 23:00 -0600123", expectedDateTime, false}, 29 | {"10.Nov.2009 11:00:00 -9900", expectedDateTime, false}, 30 | } 31 | for _, test := range tests { 32 | out, err := parseMessageDateTime(test.in) 33 | if !test.ok { 34 | if err == nil { 35 | t.Errorf("ParseMessageDateTime(%q) expected error; got %q", test.in, out) 36 | } 37 | } else if err != nil { 38 | t.Errorf("ParseMessageDateTime(%q) expected %q; got %v", test.in, test.out, err) 39 | } else if !out.Equal(test.out) { 40 | t.Errorf("ParseMessageDateTime(%q) expected %q; got %q", test.in, test.out, out) 41 | } 42 | } 43 | } 44 | 45 | func TestParseDateTime(t *testing.T) { 46 | tests := []struct { 47 | in string 48 | out time.Time 49 | ok bool 50 | }{ 51 | {"2-Nov-2009 23:00:00 -0600", expectedDateTime, true}, 52 | 53 | // whitespace 54 | {" 2-Nov-2009 23:00:00 -0600", expectedDateTime, true}, 55 | 56 | // invalid or incorrect 57 | {"10-Nov-2009", time.Time{}, false}, 58 | {"abc10-Nov-2009 23:00:00 -0600123", time.Time{}, false}, 59 | } 60 | for _, test := range tests { 61 | out, err := time.Parse(DateTimeLayout, test.in) 62 | if !test.ok { 63 | if err == nil { 64 | t.Errorf("ParseDateTime(%q) expected error; got %q", test.in, out) 65 | } 66 | } else if err != nil { 67 | t.Errorf("ParseDateTime(%q) expected %q; got %v", test.in, test.out, err) 68 | } else if !out.Equal(test.out) { 69 | t.Errorf("ParseDateTime(%q) expected %q; got %q", test.in, test.out, out) 70 | } 71 | } 72 | } 73 | 74 | func TestParseDate(t *testing.T) { 75 | tests := []struct { 76 | in string 77 | out time.Time 78 | ok bool 79 | }{ 80 | {"2-Nov-2009", expectedDate, true}, 81 | {" 2-Nov-2009", expectedDate, true}, 82 | } 83 | for _, test := range tests { 84 | out, err := time.Parse(DateLayout, test.in) 85 | if !test.ok { 86 | if err == nil { 87 | t.Errorf("ParseDate(%q) expected error; got %q", test.in, out) 88 | } 89 | } else if err != nil { 90 | t.Errorf("ParseDate(%q) expected %q; got %v", test.in, test.out, err) 91 | } else if !out.Equal(test.out) { 92 | t.Errorf("ParseDate(%q) expected %q; got %q", test.in, test.out, out) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emersion/go-imap 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/emersion/go-message v0.11.1 7 | github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b 8 | golang.org/x/text v0.3.2 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac= 6 | github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= 7 | github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs= 8 | github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= 9 | github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg= 10 | github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= 11 | github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A= 12 | github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 17 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 18 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 19 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 20 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 21 | -------------------------------------------------------------------------------- /imap.go: -------------------------------------------------------------------------------- 1 | // Package imap implements IMAP4rev1 (RFC 3501). 2 | package imap 3 | 4 | import ( 5 | "errors" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | // A StatusItem is a mailbox status data item that can be retrieved with a 11 | // STATUS command. See RFC 3501 section 6.3.10. 12 | type StatusItem string 13 | 14 | const ( 15 | StatusMessages StatusItem = "MESSAGES" 16 | StatusRecent StatusItem = "RECENT" 17 | StatusUidNext StatusItem = "UIDNEXT" 18 | StatusUidValidity StatusItem = "UIDVALIDITY" 19 | StatusUnseen StatusItem = "UNSEEN" 20 | ) 21 | 22 | // A FetchItem is a message data item that can be fetched. 23 | type FetchItem string 24 | 25 | // List of items that can be fetched. 26 | const ( 27 | // Macros 28 | FetchAll FetchItem = "ALL" 29 | FetchFast FetchItem = "FAST" 30 | FetchFull FetchItem = "FULL" 31 | 32 | // Items 33 | FetchBody FetchItem = "BODY" 34 | FetchBodyStructure FetchItem = "BODYSTRUCTURE" 35 | FetchEnvelope FetchItem = "ENVELOPE" 36 | FetchFlags FetchItem = "FLAGS" 37 | FetchInternalDate FetchItem = "INTERNALDATE" 38 | FetchRFC822 FetchItem = "RFC822" 39 | FetchRFC822Header FetchItem = "RFC822.HEADER" 40 | FetchRFC822Size FetchItem = "RFC822.SIZE" 41 | FetchRFC822Text FetchItem = "RFC822.TEXT" 42 | FetchUid FetchItem = "UID" 43 | ) 44 | 45 | // Expand expands the item if it's a macro. 46 | func (item FetchItem) Expand() []FetchItem { 47 | switch item { 48 | case FetchAll: 49 | return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope} 50 | case FetchFast: 51 | return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size} 52 | case FetchFull: 53 | return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope, FetchBody} 54 | default: 55 | return []FetchItem{item} 56 | } 57 | } 58 | 59 | // FlagsOp is an operation that will be applied on message flags. 60 | type FlagsOp string 61 | 62 | const ( 63 | // SetFlags replaces existing flags by new ones. 64 | SetFlags FlagsOp = "FLAGS" 65 | // AddFlags adds new flags. 66 | AddFlags = "+FLAGS" 67 | // RemoveFlags removes existing flags. 68 | RemoveFlags = "-FLAGS" 69 | ) 70 | 71 | // silentOp can be appended to a FlagsOp to prevent the operation from 72 | // triggering unilateral message updates. 73 | const silentOp = ".SILENT" 74 | 75 | // A StoreItem is a message data item that can be updated. 76 | type StoreItem string 77 | 78 | // FormatFlagsOp returns the StoreItem that executes the flags operation op. 79 | func FormatFlagsOp(op FlagsOp, silent bool) StoreItem { 80 | s := string(op) 81 | if silent { 82 | s += silentOp 83 | } 84 | return StoreItem(s) 85 | } 86 | 87 | // ParseFlagsOp parses a flags operation from StoreItem. 88 | func ParseFlagsOp(item StoreItem) (op FlagsOp, silent bool, err error) { 89 | itemStr := string(item) 90 | silent = strings.HasSuffix(itemStr, silentOp) 91 | if silent { 92 | itemStr = strings.TrimSuffix(itemStr, silentOp) 93 | } 94 | op = FlagsOp(itemStr) 95 | 96 | if op != SetFlags && op != AddFlags && op != RemoveFlags { 97 | err = errors.New("Unsupported STORE operation") 98 | } 99 | return 100 | } 101 | 102 | // CharsetReader, if non-nil, defines a function to generate charset-conversion 103 | // readers, converting from the provided charset into UTF-8. Charsets are always 104 | // lower-case. utf-8 and us-ascii charsets are handled by default. One of the 105 | // the CharsetReader's result values must be non-nil. 106 | var CharsetReader func(charset string, r io.Reader) (io.Reader, error) 107 | -------------------------------------------------------------------------------- /internal/testcert.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | // LocalhostCert is a PEM-encoded TLS cert with SAN IPs 4 | // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. 5 | // generated from src/crypto/tls: 6 | // go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h 7 | var LocalhostCert = []byte(`-----BEGIN CERTIFICATE----- 8 | MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS 9 | MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw 10 | MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB 11 | iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4 12 | iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul 13 | rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO 14 | BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw 15 | AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA 16 | AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9 17 | tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs 18 | h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM 19 | fblo6RBxUQ== 20 | -----END CERTIFICATE-----`) 21 | 22 | // LocalhostKey is the private key for localhostCert. 23 | var LocalhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- 24 | MIICXgIBAAKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9 25 | SjY1bIw4iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZB 26 | l2+XsDulrKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQAB 27 | AoGAGRzwwir7XvBOAy5tM/uV6e+Zf6anZzus1s1Y1ClbjbE6HXbnWWF/wbZGOpet 28 | 3Zm4vD6MXc7jpTLryzTQIvVdfQbRc6+MUVeLKwZatTXtdZrhu+Jk7hx0nTPy8Jcb 29 | uJqFk541aEw+mMogY/xEcfbWd6IOkp+4xqjlFLBEDytgbIECQQDvH/E6nk+hgN4H 30 | qzzVtxxr397vWrjrIgPbJpQvBsafG7b0dA4AFjwVbFLmQcj2PprIMmPcQrooz8vp 31 | jy4SHEg1AkEA/v13/5M47K9vCxmb8QeD/asydfsgS5TeuNi8DoUBEmiSJwma7FXY 32 | fFUtxuvL7XvjwjN5B30pNEbc6Iuyt7y4MQJBAIt21su4b3sjXNueLKH85Q+phy2U 33 | fQtuUE9txblTu14q3N7gHRZB4ZMhFYyDy8CKrN2cPg/Fvyt0Xlp/DoCzjA0CQQDU 34 | y2ptGsuSmgUtWj3NM9xuwYPm+Z/F84K6+ARYiZ6PYj013sovGKUFfYAqVXVlxtIX 35 | qyUBnu3X9ps8ZfjLZO7BAkEAlT4R5Yl6cGhaJQYZHOde3JEMhNRcVFMO8dJDaFeo 36 | f9Oeos0UUothgiDktdQHxdNEwLjQf7lJJBzV+5OtwswCWA== 37 | -----END RSA PRIVATE KEY-----`) 38 | -------------------------------------------------------------------------------- /internal/testutil.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | type MapListSorter []interface{} 4 | 5 | func (s MapListSorter) Len() int { 6 | return len(s) / 2 7 | } 8 | 9 | func (s MapListSorter) Less(i, j int) bool { 10 | return s[i*2].(string) < s[j*2].(string) 11 | } 12 | 13 | func (s MapListSorter) Swap(i, j int) { 14 | s[i*2], s[j*2] = s[j*2], s[i*2] 15 | s[i*2+1], s[j*2+1] = s[j*2+1], s[i*2+1] 16 | } 17 | -------------------------------------------------------------------------------- /literal.go: -------------------------------------------------------------------------------- 1 | package imap 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // A literal, as defined in RFC 3501 section 4.3. 8 | type Literal interface { 9 | io.Reader 10 | 11 | // Len returns the number of bytes of the literal. 12 | Len() int 13 | } 14 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package imap 2 | 3 | // Logger is the behaviour used by server/client to 4 | // report errors for accepting connections and unexpected behavior from handlers. 5 | type Logger interface { 6 | Printf(format string, v ...interface{}) 7 | Println(v ...interface{}) 8 | } 9 | -------------------------------------------------------------------------------- /mailbox_test.go: -------------------------------------------------------------------------------- 1 | package imap_test 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sort" 7 | "testing" 8 | 9 | "github.com/emersion/go-imap" 10 | "github.com/emersion/go-imap/internal" 11 | ) 12 | 13 | func TestCanonicalMailboxName(t *testing.T) { 14 | if got := imap.CanonicalMailboxName("Inbox"); got != imap.InboxName { 15 | t.Errorf("Invalid canonical mailbox name: expected %q but got %q", imap.InboxName, got) 16 | } 17 | if got := imap.CanonicalMailboxName("Drafts"); got != "Drafts" { 18 | t.Errorf("Invalid canonical mailbox name: expected %q but got %q", "Drafts", got) 19 | } 20 | } 21 | 22 | var mailboxInfoTests = []struct { 23 | fields []interface{} 24 | info *imap.MailboxInfo 25 | }{ 26 | { 27 | fields: []interface{}{ 28 | []interface{}{"\\Noselect", "\\Recent", "\\Unseen"}, 29 | "/", 30 | "INBOX", 31 | }, 32 | info: &imap.MailboxInfo{ 33 | Attributes: []string{"\\Noselect", "\\Recent", "\\Unseen"}, 34 | Delimiter: "/", 35 | Name: "INBOX", 36 | }, 37 | }, 38 | } 39 | 40 | func TestMailboxInfo_Parse(t *testing.T) { 41 | for _, test := range mailboxInfoTests { 42 | info := &imap.MailboxInfo{} 43 | if err := info.Parse(test.fields); err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | if fmt.Sprint(info.Attributes) != fmt.Sprint(test.info.Attributes) { 48 | t.Fatal("Invalid flags:", info.Attributes) 49 | } 50 | if info.Delimiter != test.info.Delimiter { 51 | t.Fatal("Invalid delimiter:", info.Delimiter) 52 | } 53 | if info.Name != test.info.Name { 54 | t.Fatal("Invalid name:", info.Name) 55 | } 56 | } 57 | } 58 | 59 | func TestMailboxInfo_Format(t *testing.T) { 60 | for _, test := range mailboxInfoTests { 61 | fields := test.info.Format() 62 | 63 | if fmt.Sprint(fields) != fmt.Sprint(test.fields) { 64 | t.Fatal("Invalid fields:", fields) 65 | } 66 | } 67 | } 68 | 69 | var mailboxInfoMatchTests = []struct { 70 | name, ref, pattern string 71 | result bool 72 | }{ 73 | {name: "INBOX", pattern: "INBOX", result: true}, 74 | {name: "INBOX", pattern: "Asuka", result: false}, 75 | {name: "INBOX", pattern: "*", result: true}, 76 | {name: "INBOX", pattern: "%", result: true}, 77 | {name: "Neon Genesis Evangelion/Misato", pattern: "*", result: true}, 78 | {name: "Neon Genesis Evangelion/Misato", pattern: "%", result: false}, 79 | {name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/*", result: true}, 80 | {name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/%", result: true}, 81 | {name: "Neon Genesis Evangelion/Misato", pattern: "Neo* Evangelion/Misato", result: true}, 82 | {name: "Neon Genesis Evangelion/Misato", pattern: "Neo% Evangelion/Misato", result: true}, 83 | {name: "Neon Genesis Evangelion/Misato", pattern: "*Eva*/Misato", result: true}, 84 | {name: "Neon Genesis Evangelion/Misato", pattern: "%Eva%/Misato", result: true}, 85 | {name: "Neon Genesis Evangelion/Misato", pattern: "*X*/Misato", result: false}, 86 | {name: "Neon Genesis Evangelion/Misato", pattern: "%X%/Misato", result: false}, 87 | {name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/Mi%o", result: true}, 88 | {name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/Mi%too", result: false}, 89 | {name: "Misato/Misato", pattern: "Mis*to/Misato", result: true}, 90 | {name: "Misato/Misato", pattern: "Mis*to", result: true}, 91 | {name: "Misato/Misato/Misato", pattern: "Mis*to/Mis%to", result: true}, 92 | {name: "Misato/Misato", pattern: "Mis**to/Misato", result: true}, 93 | {name: "Misato/Misato", pattern: "Misat%/Misato", result: true}, 94 | {name: "Misato/Misato", pattern: "Misat%Misato", result: false}, 95 | {name: "Misato/Misato", ref: "Misato", pattern: "Misato", result: true}, 96 | {name: "Misato/Misato", ref: "Misato/", pattern: "Misato", result: true}, 97 | {name: "Misato/Misato", ref: "Shinji", pattern: "/Misato/*", result: true}, 98 | {name: "Misato/Misato", ref: "Misato", pattern: "/Misato", result: false}, 99 | {name: "Misato/Misato", ref: "Misato", pattern: "Shinji", result: false}, 100 | {name: "Misato/Misato", ref: "Shinji", pattern: "Misato", result: false}, 101 | } 102 | 103 | func TestMailboxInfo_Match(t *testing.T) { 104 | for _, test := range mailboxInfoMatchTests { 105 | info := &imap.MailboxInfo{Name: test.name, Delimiter: "/"} 106 | result := info.Match(test.ref, test.pattern) 107 | if result != test.result { 108 | t.Errorf("Matching name %q with pattern %q and reference %q returns %v, but expected %v", test.name, test.pattern, test.ref, result, test.result) 109 | } 110 | } 111 | } 112 | 113 | func TestNewMailboxStatus(t *testing.T) { 114 | status := imap.NewMailboxStatus("INBOX", []imap.StatusItem{imap.StatusMessages, imap.StatusUnseen}) 115 | 116 | expected := &imap.MailboxStatus{ 117 | Name: "INBOX", 118 | Items: map[imap.StatusItem]interface{}{imap.StatusMessages: nil, imap.StatusUnseen: nil}, 119 | } 120 | 121 | if !reflect.DeepEqual(expected, status) { 122 | t.Errorf("Invalid mailbox status: expected \n%+v\n but got \n%+v", expected, status) 123 | } 124 | } 125 | 126 | var mailboxStatusTests = [...]struct { 127 | fields []interface{} 128 | status *imap.MailboxStatus 129 | }{ 130 | { 131 | fields: []interface{}{ 132 | "MESSAGES", uint32(42), 133 | "RECENT", uint32(1), 134 | "UNSEEN", uint32(6), 135 | "UIDNEXT", uint32(65536), 136 | "UIDVALIDITY", uint32(4242), 137 | }, 138 | status: &imap.MailboxStatus{ 139 | Items: map[imap.StatusItem]interface{}{ 140 | imap.StatusMessages: nil, 141 | imap.StatusRecent: nil, 142 | imap.StatusUnseen: nil, 143 | imap.StatusUidNext: nil, 144 | imap.StatusUidValidity: nil, 145 | }, 146 | Messages: 42, 147 | Recent: 1, 148 | Unseen: 6, 149 | UidNext: 65536, 150 | UidValidity: 4242, 151 | }, 152 | }, 153 | } 154 | 155 | func TestMailboxStatus_Parse(t *testing.T) { 156 | for i, test := range mailboxStatusTests { 157 | status := &imap.MailboxStatus{} 158 | if err := status.Parse(test.fields); err != nil { 159 | t.Errorf("Expected no error while parsing mailbox status #%v, got: %v", i, err) 160 | continue 161 | } 162 | 163 | if !reflect.DeepEqual(status, test.status) { 164 | t.Errorf("Invalid parsed mailbox status for #%v: got \n%+v\n but expected \n%+v", i, status, test.status) 165 | } 166 | } 167 | } 168 | 169 | func TestMailboxStatus_Format(t *testing.T) { 170 | for i, test := range mailboxStatusTests { 171 | fields := test.status.Format() 172 | 173 | // MapListSorter does not know about RawString and will panic. 174 | stringFields := make([]interface{}, 0, len(fields)) 175 | for _, field := range fields { 176 | if s, ok := field.(imap.RawString); ok { 177 | stringFields = append(stringFields, string(s)) 178 | } else { 179 | stringFields = append(stringFields, field) 180 | } 181 | } 182 | 183 | sort.Sort(internal.MapListSorter(stringFields)) 184 | 185 | sort.Sort(internal.MapListSorter(test.fields)) 186 | 187 | if !reflect.DeepEqual(stringFields, test.fields) { 188 | t.Errorf("Invalid mailbox status fields for #%v: got \n%+v\n but expected \n%+v", i, fields, test.fields) 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package imap 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Resp is an IMAP response. It is either a *DataResp, a 8 | // *ContinuationReq or a *StatusResp. 9 | type Resp interface { 10 | resp() 11 | } 12 | 13 | // ReadResp reads a single response from a Reader. 14 | func ReadResp(r *Reader) (Resp, error) { 15 | atom, err := r.ReadAtom() 16 | if err != nil { 17 | return nil, err 18 | } 19 | tag, ok := atom.(string) 20 | if !ok { 21 | return nil, newParseError("response tag is not an atom") 22 | } 23 | 24 | if tag == "+" { 25 | if err := r.ReadSp(); err != nil { 26 | r.UnreadRune() 27 | } 28 | 29 | resp := &ContinuationReq{} 30 | resp.Info, err = r.ReadInfo() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return resp, nil 36 | } 37 | 38 | if err := r.ReadSp(); err != nil { 39 | return nil, err 40 | } 41 | 42 | // Can be either data or status 43 | // Try to parse a status 44 | var fields []interface{} 45 | if atom, err := r.ReadAtom(); err == nil { 46 | fields = append(fields, atom) 47 | 48 | if err := r.ReadSp(); err == nil { 49 | if name, ok := atom.(string); ok { 50 | status := StatusRespType(name) 51 | switch status { 52 | case StatusRespOk, StatusRespNo, StatusRespBad, StatusRespPreauth, StatusRespBye: 53 | resp := &StatusResp{ 54 | Tag: tag, 55 | Type: status, 56 | } 57 | 58 | char, _, err := r.ReadRune() 59 | if err != nil { 60 | return nil, err 61 | } 62 | r.UnreadRune() 63 | 64 | if char == '[' { 65 | // Contains code & arguments 66 | resp.Code, resp.Arguments, err = r.ReadRespCode() 67 | if err != nil { 68 | return nil, err 69 | } 70 | } 71 | 72 | resp.Info, err = r.ReadInfo() 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | return resp, nil 78 | } 79 | } 80 | } else { 81 | r.UnreadRune() 82 | } 83 | } else { 84 | r.UnreadRune() 85 | } 86 | 87 | // Not a status so it's data 88 | resp := &DataResp{Tag: tag} 89 | 90 | var remaining []interface{} 91 | remaining, err = r.ReadLine() 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | resp.Fields = append(fields, remaining...) 97 | return resp, nil 98 | } 99 | 100 | // DataResp is an IMAP response containing data. 101 | type DataResp struct { 102 | // The response tag. Can be either "" for untagged responses, "+" for continuation 103 | // requests or a previous command's tag. 104 | Tag string 105 | // The parsed response fields. 106 | Fields []interface{} 107 | } 108 | 109 | // NewUntaggedResp creates a new untagged response. 110 | func NewUntaggedResp(fields []interface{}) *DataResp { 111 | return &DataResp{ 112 | Tag: "*", 113 | Fields: fields, 114 | } 115 | } 116 | 117 | func (r *DataResp) resp() {} 118 | 119 | func (r *DataResp) WriteTo(w *Writer) error { 120 | tag := RawString(r.Tag) 121 | if tag == "" { 122 | tag = RawString("*") 123 | } 124 | 125 | fields := []interface{}{RawString(tag)} 126 | fields = append(fields, r.Fields...) 127 | return w.writeLine(fields...) 128 | } 129 | 130 | // ContinuationReq is a continuation request response. 131 | type ContinuationReq struct { 132 | // The info message sent with the continuation request. 133 | Info string 134 | } 135 | 136 | func (r *ContinuationReq) resp() {} 137 | 138 | func (r *ContinuationReq) WriteTo(w *Writer) error { 139 | if err := w.writeString("+"); err != nil { 140 | return err 141 | } 142 | 143 | if r.Info != "" { 144 | if err := w.writeString(string(sp) + r.Info); err != nil { 145 | return err 146 | } 147 | } 148 | 149 | return w.writeCrlf() 150 | } 151 | 152 | // ParseNamedResp attempts to parse a named data response. 153 | func ParseNamedResp(resp Resp) (name string, fields []interface{}, ok bool) { 154 | data, ok := resp.(*DataResp) 155 | if !ok || len(data.Fields) == 0 { 156 | return 157 | } 158 | 159 | // Some responses (namely EXISTS and RECENT) are formatted like so: 160 | // [num] [name] [...] 161 | // Which is fucking stupid. But we handle that here by checking if the 162 | // response name is a number and then rearranging it. 163 | if len(data.Fields) > 1 { 164 | name, ok := data.Fields[1].(string) 165 | if ok { 166 | if _, err := ParseNumber(data.Fields[0]); err == nil { 167 | fields := []interface{}{data.Fields[0]} 168 | fields = append(fields, data.Fields[2:]...) 169 | return strings.ToUpper(name), fields, true 170 | } 171 | } 172 | } 173 | 174 | // IMAP commands are formatted like this: 175 | // [name] [...] 176 | name, ok = data.Fields[0].(string) 177 | if !ok { 178 | return 179 | } 180 | return strings.ToUpper(name), data.Fields[1:], true 181 | } 182 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package imap_test 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/emersion/go-imap" 9 | ) 10 | 11 | func TestResp_WriteTo(t *testing.T) { 12 | var b bytes.Buffer 13 | w := imap.NewWriter(&b) 14 | 15 | resp := imap.NewUntaggedResp([]interface{}{imap.RawString("76"), imap.RawString("FETCH"), []interface{}{imap.RawString("UID"), 783}}) 16 | if err := resp.WriteTo(w); err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | if b.String() != "* 76 FETCH (UID 783)\r\n" { 21 | t.Error("Invalid response:", b.String()) 22 | } 23 | } 24 | 25 | func TestContinuationReq_WriteTo(t *testing.T) { 26 | var b bytes.Buffer 27 | w := imap.NewWriter(&b) 28 | 29 | resp := &imap.ContinuationReq{} 30 | 31 | if err := resp.WriteTo(w); err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | if b.String() != "+\r\n" { 36 | t.Error("Invalid continuation:", b.String()) 37 | } 38 | } 39 | 40 | func TestContinuationReq_WriteTo_WithInfo(t *testing.T) { 41 | var b bytes.Buffer 42 | w := imap.NewWriter(&b) 43 | 44 | resp := &imap.ContinuationReq{Info: "send literal"} 45 | 46 | if err := resp.WriteTo(w); err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | if b.String() != "+ send literal\r\n" { 51 | t.Error("Invalid continuation:", b.String()) 52 | } 53 | } 54 | 55 | func TestReadResp_ContinuationReq(t *testing.T) { 56 | b := bytes.NewBufferString("+ send literal\r\n") 57 | r := imap.NewReader(b) 58 | 59 | resp, err := imap.ReadResp(r) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | cont, ok := resp.(*imap.ContinuationReq) 65 | if !ok { 66 | t.Fatal("Response is not a continuation request") 67 | } 68 | 69 | if cont.Info != "send literal" { 70 | t.Error("Invalid info:", cont.Info) 71 | } 72 | } 73 | 74 | func TestReadResp_ContinuationReq_NoInfo(t *testing.T) { 75 | b := bytes.NewBufferString("+\r\n") 76 | r := imap.NewReader(b) 77 | 78 | resp, err := imap.ReadResp(r) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | cont, ok := resp.(*imap.ContinuationReq) 84 | if !ok { 85 | t.Fatal("Response is not a continuation request") 86 | } 87 | 88 | if cont.Info != "" { 89 | t.Error("Invalid info:", cont.Info) 90 | } 91 | } 92 | 93 | func TestReadResp_Resp(t *testing.T) { 94 | b := bytes.NewBufferString("* 1 EXISTS\r\n") 95 | r := imap.NewReader(b) 96 | 97 | resp, err := imap.ReadResp(r) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | data, ok := resp.(*imap.DataResp) 103 | if !ok { 104 | t.Fatal("Invalid response type") 105 | } 106 | 107 | if data.Tag != "*" { 108 | t.Error("Invalid tag:", data.Tag) 109 | } 110 | if len(data.Fields) != 2 { 111 | t.Error("Invalid fields:", data.Fields) 112 | } 113 | } 114 | 115 | func TestReadResp_Resp_NoArgs(t *testing.T) { 116 | b := bytes.NewBufferString("* SEARCH\r\n") 117 | r := imap.NewReader(b) 118 | 119 | resp, err := imap.ReadResp(r) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | data, ok := resp.(*imap.DataResp) 125 | if !ok { 126 | t.Fatal("Invalid response type") 127 | } 128 | 129 | if data.Tag != "*" { 130 | t.Error("Invalid tag:", data.Tag) 131 | } 132 | if len(data.Fields) != 1 || data.Fields[0] != "SEARCH" { 133 | t.Error("Invalid fields:", data.Fields) 134 | } 135 | } 136 | 137 | func TestReadResp_StatusResp(t *testing.T) { 138 | tests := []struct { 139 | input string 140 | expected *imap.StatusResp 141 | }{ 142 | { 143 | input: "* OK IMAP4rev1 Service Ready\r\n", 144 | expected: &imap.StatusResp{ 145 | Tag: "*", 146 | Type: imap.StatusRespOk, 147 | Info: "IMAP4rev1 Service Ready", 148 | }, 149 | }, 150 | { 151 | input: "* PREAUTH Welcome Pauline!\r\n", 152 | expected: &imap.StatusResp{ 153 | Tag: "*", 154 | Type: imap.StatusRespPreauth, 155 | Info: "Welcome Pauline!", 156 | }, 157 | }, 158 | { 159 | input: "a001 OK NOOP completed\r\n", 160 | expected: &imap.StatusResp{ 161 | Tag: "a001", 162 | Type: imap.StatusRespOk, 163 | Info: "NOOP completed", 164 | }, 165 | }, 166 | { 167 | input: "a001 OK [READ-ONLY] SELECT completed\r\n", 168 | expected: &imap.StatusResp{ 169 | Tag: "a001", 170 | Type: imap.StatusRespOk, 171 | Code: "READ-ONLY", 172 | Info: "SELECT completed", 173 | }, 174 | }, 175 | { 176 | input: "a001 OK [CAPABILITY IMAP4rev1 UIDPLUS] LOGIN completed\r\n", 177 | expected: &imap.StatusResp{ 178 | Tag: "a001", 179 | Type: imap.StatusRespOk, 180 | Code: "CAPABILITY", 181 | Arguments: []interface{}{"IMAP4rev1", "UIDPLUS"}, 182 | Info: "LOGIN completed", 183 | }, 184 | }, 185 | } 186 | 187 | for _, test := range tests { 188 | b := bytes.NewBufferString(test.input) 189 | r := imap.NewReader(b) 190 | 191 | resp, err := imap.ReadResp(r) 192 | if err != nil { 193 | t.Fatal(err) 194 | } 195 | 196 | status, ok := resp.(*imap.StatusResp) 197 | if !ok { 198 | t.Fatal("Response is not a status:", resp) 199 | } 200 | 201 | if status.Tag != test.expected.Tag { 202 | t.Errorf("Invalid tag: expected %v but got %v", status.Tag, test.expected.Tag) 203 | } 204 | if status.Type != test.expected.Type { 205 | t.Errorf("Invalid type: expected %v but got %v", status.Type, test.expected.Type) 206 | } 207 | if status.Code != test.expected.Code { 208 | t.Errorf("Invalid code: expected %v but got %v", status.Code, test.expected.Code) 209 | } 210 | if len(status.Arguments) != len(test.expected.Arguments) { 211 | t.Errorf("Invalid arguments: expected %v but got %v", status.Arguments, test.expected.Arguments) 212 | } 213 | if status.Info != test.expected.Info { 214 | t.Errorf("Invalid info: expected %v but got %v", status.Info, test.expected.Info) 215 | } 216 | } 217 | } 218 | 219 | func TestParseNamedResp(t *testing.T) { 220 | tests := []struct { 221 | resp *imap.DataResp 222 | name string 223 | fields []interface{} 224 | }{ 225 | { 226 | resp: &imap.DataResp{Fields: []interface{}{"CAPABILITY", "IMAP4rev1"}}, 227 | name: "CAPABILITY", 228 | fields: []interface{}{"IMAP4rev1"}, 229 | }, 230 | { 231 | resp: &imap.DataResp{Fields: []interface{}{"42", "EXISTS"}}, 232 | name: "EXISTS", 233 | fields: []interface{}{"42"}, 234 | }, 235 | { 236 | resp: &imap.DataResp{Fields: []interface{}{"42", "FETCH", "blah"}}, 237 | name: "FETCH", 238 | fields: []interface{}{"42", "blah"}, 239 | }, 240 | } 241 | 242 | for _, test := range tests { 243 | name, fields, ok := imap.ParseNamedResp(test.resp) 244 | if !ok { 245 | t.Errorf("ParseNamedResp(%v)[2] = false, want true", test.resp) 246 | } else if name != test.name { 247 | t.Errorf("ParseNamedResp(%v)[0] = %v, want %v", test.resp, name, test.name) 248 | } else if !reflect.DeepEqual(fields, test.fields) { 249 | t.Errorf("ParseNamedResp(%v)[1] = %v, want %v", test.resp, fields, test.fields) 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /responses/authenticate.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "github.com/emersion/go-imap" 7 | "github.com/emersion/go-sasl" 8 | ) 9 | 10 | // An AUTHENTICATE response. 11 | type Authenticate struct { 12 | Mechanism sasl.Client 13 | InitialResponse []byte 14 | RepliesCh chan []byte 15 | } 16 | 17 | // Implements 18 | func (r *Authenticate) Replies() <-chan []byte { 19 | return r.RepliesCh 20 | } 21 | 22 | func (r *Authenticate) writeLine(l string) error { 23 | r.RepliesCh <- []byte(l + "\r\n") 24 | return nil 25 | } 26 | 27 | func (r *Authenticate) cancel() error { 28 | return r.writeLine("*") 29 | } 30 | 31 | func (r *Authenticate) Handle(resp imap.Resp) error { 32 | cont, ok := resp.(*imap.ContinuationReq) 33 | if !ok { 34 | return ErrUnhandled 35 | } 36 | 37 | // Empty challenge, send initial response as stated in RFC 2222 section 5.1 38 | if cont.Info == "" && r.InitialResponse != nil { 39 | encoded := base64.StdEncoding.EncodeToString(r.InitialResponse) 40 | if err := r.writeLine(encoded); err != nil { 41 | return err 42 | } 43 | r.InitialResponse = nil 44 | return nil 45 | } 46 | 47 | challenge, err := base64.StdEncoding.DecodeString(cont.Info) 48 | if err != nil { 49 | r.cancel() 50 | return err 51 | } 52 | 53 | reply, err := r.Mechanism.Next(challenge) 54 | if err != nil { 55 | r.cancel() 56 | return err 57 | } 58 | 59 | encoded := base64.StdEncoding.EncodeToString(reply) 60 | return r.writeLine(encoded) 61 | } 62 | -------------------------------------------------------------------------------- /responses/capability.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | // A CAPABILITY response. 8 | // See RFC 3501 section 7.2.1 9 | type Capability struct { 10 | Caps []string 11 | } 12 | 13 | func (r *Capability) WriteTo(w *imap.Writer) error { 14 | fields := []interface{}{imap.RawString("CAPABILITY")} 15 | for _, cap := range r.Caps { 16 | fields = append(fields, imap.RawString(cap)) 17 | } 18 | 19 | return imap.NewUntaggedResp(fields).WriteTo(w) 20 | } 21 | -------------------------------------------------------------------------------- /responses/expunge.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | const expungeName = "EXPUNGE" 8 | 9 | // An EXPUNGE response. 10 | // See RFC 3501 section 7.4.1 11 | type Expunge struct { 12 | SeqNums chan uint32 13 | } 14 | 15 | func (r *Expunge) Handle(resp imap.Resp) error { 16 | name, fields, ok := imap.ParseNamedResp(resp) 17 | if !ok || name != expungeName { 18 | return ErrUnhandled 19 | } 20 | 21 | if len(fields) == 0 { 22 | return errNotEnoughFields 23 | } 24 | 25 | seqNum, err := imap.ParseNumber(fields[0]) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | r.SeqNums <- seqNum 31 | return nil 32 | } 33 | 34 | func (r *Expunge) WriteTo(w *imap.Writer) error { 35 | for seqNum := range r.SeqNums { 36 | resp := imap.NewUntaggedResp([]interface{}{seqNum, imap.RawString(expungeName)}) 37 | if err := resp.WriteTo(w); err != nil { 38 | return err 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /responses/fetch.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | const fetchName = "FETCH" 8 | 9 | // A FETCH response. 10 | // See RFC 3501 section 7.4.2 11 | type Fetch struct { 12 | Messages chan *imap.Message 13 | } 14 | 15 | func (r *Fetch) Handle(resp imap.Resp) error { 16 | name, fields, ok := imap.ParseNamedResp(resp) 17 | if !ok || name != fetchName { 18 | return ErrUnhandled 19 | } else if len(fields) < 1 { 20 | return errNotEnoughFields 21 | } 22 | 23 | seqNum, err := imap.ParseNumber(fields[0]) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | msgFields, _ := fields[1].([]interface{}) 29 | msg := &imap.Message{SeqNum: seqNum} 30 | if err := msg.Parse(msgFields); err != nil { 31 | return err 32 | } 33 | 34 | r.Messages <- msg 35 | return nil 36 | } 37 | 38 | func (r *Fetch) WriteTo(w *imap.Writer) error { 39 | var err error 40 | for msg := range r.Messages { 41 | resp := imap.NewUntaggedResp([]interface{}{msg.SeqNum, imap.RawString(fetchName), msg.Format()}) 42 | if err == nil { 43 | err = resp.WriteTo(w) 44 | } 45 | } 46 | return err 47 | } 48 | -------------------------------------------------------------------------------- /responses/list.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | const ( 8 | listName = "LIST" 9 | lsubName = "LSUB" 10 | ) 11 | 12 | // A LIST response. 13 | // If Subscribed is set to true, LSUB will be used instead. 14 | // See RFC 3501 section 7.2.2 15 | type List struct { 16 | Mailboxes chan *imap.MailboxInfo 17 | Subscribed bool 18 | } 19 | 20 | func (r *List) Name() string { 21 | if r.Subscribed { 22 | return lsubName 23 | } else { 24 | return listName 25 | } 26 | } 27 | 28 | func (r *List) Handle(resp imap.Resp) error { 29 | name, fields, ok := imap.ParseNamedResp(resp) 30 | if !ok || name != r.Name() { 31 | return ErrUnhandled 32 | } 33 | 34 | mbox := &imap.MailboxInfo{} 35 | if err := mbox.Parse(fields); err != nil { 36 | return err 37 | } 38 | 39 | r.Mailboxes <- mbox 40 | return nil 41 | } 42 | 43 | func (r *List) WriteTo(w *imap.Writer) error { 44 | respName := r.Name() 45 | 46 | for mbox := range r.Mailboxes { 47 | fields := []interface{}{imap.RawString(respName)} 48 | fields = append(fields, mbox.Format()...) 49 | 50 | resp := imap.NewUntaggedResp(fields) 51 | if err := resp.WriteTo(w); err != nil { 52 | return err 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /responses/list_test.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/emersion/go-imap" 8 | ) 9 | 10 | func TestListSlashDelimiter(t *testing.T) { 11 | mbox := &imap.MailboxInfo{} 12 | 13 | if err := mbox.Parse([]interface{}{ 14 | []interface{}{"\\Unseen"}, 15 | "/", 16 | "INBOX", 17 | }); err != nil { 18 | t.Error(err) 19 | t.FailNow() 20 | } 21 | 22 | if response := getListResponse(t, mbox); response != `* LIST (\Unseen) "/" INBOX`+"\r\n" { 23 | t.Error("Unexpected response:", response) 24 | } 25 | } 26 | 27 | func TestListNILDelimiter(t *testing.T) { 28 | mbox := &imap.MailboxInfo{} 29 | 30 | if err := mbox.Parse([]interface{}{ 31 | []interface{}{"\\Unseen"}, 32 | nil, 33 | "INBOX", 34 | }); err != nil { 35 | t.Error(err) 36 | t.FailNow() 37 | } 38 | 39 | if response := getListResponse(t, mbox); response != `* LIST (\Unseen) NIL INBOX`+"\r\n" { 40 | t.Error("Unexpected response:", response) 41 | } 42 | } 43 | 44 | func newListResponse(mbox *imap.MailboxInfo) (l *List) { 45 | l = &List{Mailboxes: make(chan *imap.MailboxInfo)} 46 | 47 | go func() { 48 | l.Mailboxes <- mbox 49 | close(l.Mailboxes) 50 | }() 51 | 52 | return 53 | } 54 | 55 | func getListResponse(t *testing.T, mbox *imap.MailboxInfo) string { 56 | b := &bytes.Buffer{} 57 | w := imap.NewWriter(b) 58 | 59 | if err := newListResponse(mbox).WriteTo(w); err != nil { 60 | t.Error(err) 61 | t.FailNow() 62 | } 63 | 64 | return b.String() 65 | } 66 | -------------------------------------------------------------------------------- /responses/responses.go: -------------------------------------------------------------------------------- 1 | // IMAP responses defined in RFC 3501. 2 | package responses 3 | 4 | import ( 5 | "errors" 6 | 7 | "github.com/emersion/go-imap" 8 | ) 9 | 10 | // ErrUnhandled is used when a response hasn't been handled. 11 | var ErrUnhandled = errors.New("imap: unhandled response") 12 | 13 | var errNotEnoughFields = errors.New("imap: not enough fields in response") 14 | 15 | // Handler handles responses. 16 | type Handler interface { 17 | // Handle processes a response. If the response cannot be processed, 18 | // ErrUnhandledResp must be returned. 19 | Handle(resp imap.Resp) error 20 | } 21 | 22 | // HandlerFunc is a function that handles responses. 23 | type HandlerFunc func(resp imap.Resp) error 24 | 25 | // Handle implements Handler. 26 | func (f HandlerFunc) Handle(resp imap.Resp) error { 27 | return f(resp) 28 | } 29 | 30 | // Replier is a Handler that needs to send raw data (for instance 31 | // AUTHENTICATE). 32 | type Replier interface { 33 | Handler 34 | Replies() <-chan []byte 35 | } 36 | -------------------------------------------------------------------------------- /responses/search.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | ) 6 | 7 | const searchName = "SEARCH" 8 | 9 | // A SEARCH response. 10 | // See RFC 3501 section 7.2.5 11 | type Search struct { 12 | Ids []uint32 13 | } 14 | 15 | func (r *Search) Handle(resp imap.Resp) error { 16 | name, fields, ok := imap.ParseNamedResp(resp) 17 | if !ok || name != searchName { 18 | return ErrUnhandled 19 | } 20 | 21 | r.Ids = make([]uint32, len(fields)) 22 | for i, f := range fields { 23 | if id, err := imap.ParseNumber(f); err != nil { 24 | return err 25 | } else { 26 | r.Ids[i] = id 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func (r *Search) WriteTo(w *imap.Writer) (err error) { 34 | fields := []interface{}{imap.RawString(searchName)} 35 | for _, id := range r.Ids { 36 | fields = append(fields, id) 37 | } 38 | 39 | resp := imap.NewUntaggedResp(fields) 40 | return resp.WriteTo(w) 41 | } 42 | -------------------------------------------------------------------------------- /responses/select.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/emersion/go-imap" 7 | ) 8 | 9 | // A SELECT response. 10 | type Select struct { 11 | Mailbox *imap.MailboxStatus 12 | } 13 | 14 | func (r *Select) Handle(resp imap.Resp) error { 15 | if r.Mailbox == nil { 16 | r.Mailbox = &imap.MailboxStatus{Items: make(map[imap.StatusItem]interface{})} 17 | } 18 | mbox := r.Mailbox 19 | 20 | switch resp := resp.(type) { 21 | case *imap.DataResp: 22 | name, fields, ok := imap.ParseNamedResp(resp) 23 | if !ok || name != "FLAGS" { 24 | return ErrUnhandled 25 | } else if len(fields) < 1 { 26 | return errNotEnoughFields 27 | } 28 | 29 | flags, _ := fields[0].([]interface{}) 30 | mbox.Flags, _ = imap.ParseStringList(flags) 31 | case *imap.StatusResp: 32 | if len(resp.Arguments) < 1 { 33 | return ErrUnhandled 34 | } 35 | 36 | var item imap.StatusItem 37 | switch resp.Code { 38 | case "UNSEEN": 39 | mbox.UnseenSeqNum, _ = imap.ParseNumber(resp.Arguments[0]) 40 | case "PERMANENTFLAGS": 41 | flags, _ := resp.Arguments[0].([]interface{}) 42 | mbox.PermanentFlags, _ = imap.ParseStringList(flags) 43 | case "UIDNEXT": 44 | mbox.UidNext, _ = imap.ParseNumber(resp.Arguments[0]) 45 | item = imap.StatusUidNext 46 | case "UIDVALIDITY": 47 | mbox.UidValidity, _ = imap.ParseNumber(resp.Arguments[0]) 48 | item = imap.StatusUidValidity 49 | default: 50 | return ErrUnhandled 51 | } 52 | 53 | if item != "" { 54 | mbox.ItemsLocker.Lock() 55 | mbox.Items[item] = nil 56 | mbox.ItemsLocker.Unlock() 57 | } 58 | default: 59 | return ErrUnhandled 60 | } 61 | return nil 62 | } 63 | 64 | func (r *Select) WriteTo(w *imap.Writer) error { 65 | mbox := r.Mailbox 66 | 67 | if mbox.Flags != nil { 68 | flags := make([]interface{}, len(mbox.Flags)) 69 | for i, f := range mbox.Flags { 70 | flags[i] = imap.RawString(f) 71 | } 72 | res := imap.NewUntaggedResp([]interface{}{imap.RawString("FLAGS"), flags}) 73 | if err := res.WriteTo(w); err != nil { 74 | return err 75 | } 76 | } 77 | 78 | if mbox.PermanentFlags != nil { 79 | flags := make([]interface{}, len(mbox.PermanentFlags)) 80 | for i, f := range mbox.PermanentFlags { 81 | flags[i] = imap.RawString(f) 82 | } 83 | statusRes := &imap.StatusResp{ 84 | Type: imap.StatusRespOk, 85 | Code: imap.CodePermanentFlags, 86 | Arguments: []interface{}{flags}, 87 | Info: "Flags permitted.", 88 | } 89 | if err := statusRes.WriteTo(w); err != nil { 90 | return err 91 | } 92 | } 93 | 94 | if mbox.UnseenSeqNum > 0 { 95 | statusRes := &imap.StatusResp{ 96 | Type: imap.StatusRespOk, 97 | Code: imap.CodeUnseen, 98 | Arguments: []interface{}{mbox.UnseenSeqNum}, 99 | Info: fmt.Sprintf("Message %d is first unseen", mbox.UnseenSeqNum), 100 | } 101 | if err := statusRes.WriteTo(w); err != nil { 102 | return err 103 | } 104 | } 105 | 106 | for k := range r.Mailbox.Items { 107 | switch k { 108 | case imap.StatusMessages: 109 | res := imap.NewUntaggedResp([]interface{}{mbox.Messages, imap.RawString("EXISTS")}) 110 | if err := res.WriteTo(w); err != nil { 111 | return err 112 | } 113 | case imap.StatusRecent: 114 | res := imap.NewUntaggedResp([]interface{}{mbox.Recent, imap.RawString("RECENT")}) 115 | if err := res.WriteTo(w); err != nil { 116 | return err 117 | } 118 | case imap.StatusUidNext: 119 | statusRes := &imap.StatusResp{ 120 | Type: imap.StatusRespOk, 121 | Code: imap.CodeUidNext, 122 | Arguments: []interface{}{mbox.UidNext}, 123 | Info: "Predicted next UID", 124 | } 125 | if err := statusRes.WriteTo(w); err != nil { 126 | return err 127 | } 128 | case imap.StatusUidValidity: 129 | statusRes := &imap.StatusResp{ 130 | Type: imap.StatusRespOk, 131 | Code: imap.CodeUidValidity, 132 | Arguments: []interface{}{mbox.UidValidity}, 133 | Info: "UIDs valid", 134 | } 135 | if err := statusRes.WriteTo(w); err != nil { 136 | return err 137 | } 138 | } 139 | } 140 | 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /responses/status.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-imap" 7 | "github.com/emersion/go-imap/utf7" 8 | ) 9 | 10 | const statusName = "STATUS" 11 | 12 | // A STATUS response. 13 | // See RFC 3501 section 7.2.4 14 | type Status struct { 15 | Mailbox *imap.MailboxStatus 16 | } 17 | 18 | func (r *Status) Handle(resp imap.Resp) error { 19 | if r.Mailbox == nil { 20 | r.Mailbox = &imap.MailboxStatus{} 21 | } 22 | mbox := r.Mailbox 23 | 24 | name, fields, ok := imap.ParseNamedResp(resp) 25 | if !ok || name != statusName { 26 | return ErrUnhandled 27 | } else if len(fields) < 2 { 28 | return errNotEnoughFields 29 | } 30 | 31 | if name, err := imap.ParseString(fields[0]); err != nil { 32 | return err 33 | } else if name, err := utf7.Encoding.NewDecoder().String(name); err != nil { 34 | return err 35 | } else { 36 | mbox.Name = imap.CanonicalMailboxName(name) 37 | } 38 | 39 | var items []interface{} 40 | if items, ok = fields[1].([]interface{}); !ok { 41 | return errors.New("STATUS response expects a list as second argument") 42 | } 43 | 44 | mbox.Items = nil 45 | return mbox.Parse(items) 46 | } 47 | 48 | func (r *Status) WriteTo(w *imap.Writer) error { 49 | mbox := r.Mailbox 50 | name, _ := utf7.Encoding.NewEncoder().String(mbox.Name) 51 | fields := []interface{}{imap.RawString(statusName), imap.FormatMailboxName(name), mbox.Format()} 52 | return imap.NewUntaggedResp(fields).WriteTo(w) 53 | } 54 | -------------------------------------------------------------------------------- /search_test.go: -------------------------------------------------------------------------------- 1 | package imap 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/textproto" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | // Note to myself: writing these boring tests actually fixed 2 bugs :P 14 | 15 | var searchSeqSet1, _ = ParseSeqSet("1:42") 16 | var searchSeqSet2, _ = ParseSeqSet("743:938") 17 | var searchDate1 = time.Date(1997, 11, 21, 0, 0, 0, 0, time.UTC) 18 | var searchDate2 = time.Date(1984, 11, 5, 0, 0, 0, 0, time.UTC) 19 | 20 | var searchCriteriaTests = []struct { 21 | expected string 22 | criteria *SearchCriteria 23 | }{ 24 | { 25 | expected: `(1:42 UID 743:938 ` + 26 | `SINCE "5-Nov-1984" BEFORE "21-Nov-1997" SENTSINCE "5-Nov-1984" SENTBEFORE "21-Nov-1997" ` + 27 | `FROM "root@protonmail.com" BODY "hey there" TEXT "DILLE" ` + 28 | `ANSWERED DELETED KEYWORD cc UNKEYWORD microsoft ` + 29 | `LARGER 4242 SMALLER 4342 ` + 30 | `NOT (SENTON "21-Nov-1997" HEADER "Content-Type" "text/csv") ` + 31 | `OR (ON "5-Nov-1984" DRAFT FLAGGED UNANSWERED UNDELETED OLD) (UNDRAFT UNFLAGGED UNSEEN))`, 32 | criteria: &SearchCriteria{ 33 | SeqNum: searchSeqSet1, 34 | Uid: searchSeqSet2, 35 | Since: searchDate2, 36 | Before: searchDate1, 37 | SentSince: searchDate2, 38 | SentBefore: searchDate1, 39 | Header: textproto.MIMEHeader{ 40 | "From": {"root@protonmail.com"}, 41 | }, 42 | Body: []string{"hey there"}, 43 | Text: []string{"DILLE"}, 44 | WithFlags: []string{AnsweredFlag, DeletedFlag, "cc"}, 45 | WithoutFlags: []string{"microsoft"}, 46 | Larger: 4242, 47 | Smaller: 4342, 48 | Not: []*SearchCriteria{{ 49 | SentSince: searchDate1, 50 | SentBefore: searchDate1.Add(24 * time.Hour), 51 | Header: textproto.MIMEHeader{ 52 | "Content-Type": {"text/csv"}, 53 | }, 54 | }}, 55 | Or: [][2]*SearchCriteria{{ 56 | { 57 | Since: searchDate2, 58 | Before: searchDate2.Add(24 * time.Hour), 59 | WithFlags: []string{DraftFlag, FlaggedFlag}, 60 | WithoutFlags: []string{AnsweredFlag, DeletedFlag, RecentFlag}, 61 | }, 62 | { 63 | WithoutFlags: []string{DraftFlag, FlaggedFlag, SeenFlag}, 64 | }, 65 | }}, 66 | }, 67 | }, 68 | { 69 | expected: "(ALL)", 70 | criteria: &SearchCriteria{}, 71 | }, 72 | } 73 | 74 | func TestSearchCriteria_Format(t *testing.T) { 75 | for i, test := range searchCriteriaTests { 76 | fields := test.criteria.Format() 77 | 78 | got, err := formatFields(fields) 79 | if err != nil { 80 | t.Fatal("Unexpected no error while formatting fields, got:", err) 81 | } 82 | 83 | if got != test.expected { 84 | t.Errorf("Invalid search criteria fields for #%v: got \n%v\n instead of \n%v", i+1, got, test.expected) 85 | } 86 | } 87 | } 88 | 89 | func TestSearchCriteria_Parse(t *testing.T) { 90 | for i, test := range searchCriteriaTests { 91 | criteria := new(SearchCriteria) 92 | 93 | b := bytes.NewBuffer([]byte(test.expected)) 94 | r := NewReader(b) 95 | fields, _ := r.ReadFields() 96 | 97 | if err := criteria.ParseWithCharset(fields[0].([]interface{}), nil); err != nil { 98 | t.Errorf("Cannot parse search criteria for #%v: %v", i+1, err) 99 | } else if !reflect.DeepEqual(criteria, test.criteria) { 100 | t.Errorf("Invalid search criteria for #%v: got \n%+v\n instead of \n%+v", i+1, criteria, test.criteria) 101 | } 102 | } 103 | } 104 | 105 | var searchCriteriaParseTests = []struct { 106 | fields []interface{} 107 | criteria *SearchCriteria 108 | charset func(io.Reader) io.Reader 109 | }{ 110 | { 111 | fields: []interface{}{"ALL"}, 112 | criteria: &SearchCriteria{}, 113 | }, 114 | { 115 | fields: []interface{}{"NEW"}, 116 | criteria: &SearchCriteria{ 117 | WithFlags: []string{RecentFlag}, 118 | WithoutFlags: []string{SeenFlag}, 119 | }, 120 | }, 121 | { 122 | fields: []interface{}{"SUBJECT", strings.NewReader("café")}, 123 | criteria: &SearchCriteria{ 124 | Header: textproto.MIMEHeader{"Subject": {"café"}}, 125 | }, 126 | charset: func(r io.Reader) io.Reader { 127 | return r 128 | }, 129 | }, 130 | } 131 | 132 | func TestSearchCriteria_Parse_others(t *testing.T) { 133 | for i, test := range searchCriteriaParseTests { 134 | criteria := new(SearchCriteria) 135 | if err := criteria.ParseWithCharset(test.fields, test.charset); err != nil { 136 | t.Errorf("Cannot parse search criteria for #%v: %v", i+1, err) 137 | } else if !reflect.DeepEqual(criteria, test.criteria) { 138 | t.Errorf("Invalid search criteria for #%v: got \n%+v\n instead of \n%+v", i+1, criteria, test.criteria) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /server/cmd_any.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/emersion/go-imap" 5 | "github.com/emersion/go-imap/backend" 6 | "github.com/emersion/go-imap/commands" 7 | "github.com/emersion/go-imap/responses" 8 | ) 9 | 10 | type Capability struct { 11 | commands.Capability 12 | } 13 | 14 | func (cmd *Capability) Handle(conn Conn) error { 15 | res := &responses.Capability{Caps: conn.Capabilities()} 16 | return conn.WriteResp(res) 17 | } 18 | 19 | type Noop struct { 20 | commands.Noop 21 | } 22 | 23 | func (cmd *Noop) Handle(conn Conn) error { 24 | ctx := conn.Context() 25 | if ctx.Mailbox != nil { 26 | // If a mailbox is selected, NOOP can be used to poll for server updates 27 | if mbox, ok := ctx.Mailbox.(backend.MailboxPoller); ok { 28 | return mbox.Poll() 29 | } 30 | } 31 | 32 | return nil 33 | } 34 | 35 | type Logout struct { 36 | commands.Logout 37 | } 38 | 39 | func (cmd *Logout) Handle(conn Conn) error { 40 | // RFC3501#section-6.4.2 CLOSE 41 | // The SELECT, EXAMINE, and LOGOUT commands implicitly close the 42 | // currently selected mailbox without doing an expunge. 43 | if err := closeMailbox(conn.Context()); err != nil && err != ErrNoMailboxSelected { 44 | conn.Server().ErrorLog.Printf("CLOSE-LOGOUT failed: %v", err) 45 | } 46 | 47 | res := &imap.StatusResp{ 48 | Type: imap.StatusRespBye, 49 | Info: "Closing connection", 50 | } 51 | 52 | if err := conn.WriteResp(res); err != nil { 53 | return err 54 | } 55 | 56 | // Request to close the connection 57 | conn.Context().State = imap.LogoutState 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /server/cmd_any_test.go: -------------------------------------------------------------------------------- 1 | package server_test 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "net" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/emersion/go-imap/server" 11 | "github.com/emersion/go-sasl" 12 | ) 13 | 14 | func testServerGreeted(t *testing.T) (s *server.Server, c net.Conn, scanner *bufio.Scanner) { 15 | s, c = testServer(t) 16 | scanner = bufio.NewScanner(c) 17 | 18 | scanner.Scan() // Greeting 19 | return 20 | } 21 | 22 | func TestCapability(t *testing.T) { 23 | s, c, scanner := testServerGreeted(t) 24 | defer s.Close() 25 | defer c.Close() 26 | 27 | io.WriteString(c, "a001 CAPABILITY\r\n") 28 | 29 | scanner.Scan() 30 | if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN" { 31 | t.Fatal("Bad capability:", scanner.Text()) 32 | } 33 | 34 | scanner.Scan() 35 | if !strings.HasPrefix(scanner.Text(), "a001 OK ") { 36 | t.Fatal("Bad status response:", scanner.Text()) 37 | } 38 | } 39 | 40 | func TestNoop(t *testing.T) { 41 | s, c, scanner := testServerGreeted(t) 42 | defer s.Close() 43 | defer c.Close() 44 | 45 | io.WriteString(c, "a001 NOOP\r\n") 46 | 47 | scanner.Scan() 48 | if !strings.HasPrefix(scanner.Text(), "a001 OK ") { 49 | t.Fatal("Bad status response:", scanner.Text()) 50 | } 51 | } 52 | 53 | func TestLogout(t *testing.T) { 54 | s, c, scanner := testServerGreeted(t) 55 | defer s.Close() 56 | defer c.Close() 57 | 58 | io.WriteString(c, "a001 LOGOUT\r\n") 59 | 60 | scanner.Scan() 61 | if !strings.HasPrefix(scanner.Text(), "* BYE ") { 62 | t.Fatal("Bad BYE response:", scanner.Text()) 63 | } 64 | 65 | scanner.Scan() 66 | if !strings.HasPrefix(scanner.Text(), "a001 OK ") { 67 | t.Fatal("Bad status response:", scanner.Text()) 68 | } 69 | } 70 | 71 | type xnoop struct{} 72 | 73 | func (ext *xnoop) Capabilities(server.Conn) []string { 74 | return []string{"XNOOP"} 75 | } 76 | 77 | func (ext *xnoop) Command(string) server.HandlerFactory { 78 | return nil 79 | } 80 | 81 | func TestServer_Enable(t *testing.T) { 82 | s, c, scanner := testServerGreeted(t) 83 | defer s.Close() 84 | defer c.Close() 85 | 86 | s.Enable(&xnoop{}) 87 | 88 | io.WriteString(c, "a001 CAPABILITY\r\n") 89 | 90 | scanner.Scan() 91 | if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN XNOOP" { 92 | t.Fatal("Bad capability:", scanner.Text()) 93 | } 94 | 95 | scanner.Scan() 96 | if !strings.HasPrefix(scanner.Text(), "a001 OK ") { 97 | t.Fatal("Bad status response:", scanner.Text()) 98 | } 99 | } 100 | 101 | type xnoopAuth struct{} 102 | 103 | func (ext *xnoopAuth) Next(response []byte) (challenge []byte, done bool, err error) { 104 | done = true 105 | return 106 | } 107 | 108 | func TestServer_EnableAuth(t *testing.T) { 109 | s, c, scanner := testServerGreeted(t) 110 | defer s.Close() 111 | defer c.Close() 112 | 113 | s.EnableAuth("XNOOP", func(server.Conn) sasl.Server { 114 | return &xnoopAuth{} 115 | }) 116 | 117 | io.WriteString(c, "a001 CAPABILITY\r\n") 118 | 119 | scanner.Scan() 120 | if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN AUTH=XNOOP" && 121 | scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=XNOOP AUTH=PLAIN" { 122 | t.Fatal("Bad capability:", scanner.Text()) 123 | } 124 | 125 | scanner.Scan() 126 | if !strings.HasPrefix(scanner.Text(), "a001 OK ") { 127 | t.Fatal("Bad status response:", scanner.Text()) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /server/cmd_noauth.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "net" 7 | 8 | "github.com/emersion/go-imap" 9 | "github.com/emersion/go-imap/commands" 10 | "github.com/emersion/go-sasl" 11 | ) 12 | 13 | // IMAP errors in Not Authenticated state. 14 | var ( 15 | ErrAlreadyAuthenticated = errors.New("Already authenticated") 16 | ErrAuthDisabled = errors.New("Authentication disabled") 17 | ) 18 | 19 | type StartTLS struct { 20 | commands.StartTLS 21 | } 22 | 23 | func (cmd *StartTLS) Handle(conn Conn) error { 24 | ctx := conn.Context() 25 | if ctx.State != imap.NotAuthenticatedState { 26 | return ErrAlreadyAuthenticated 27 | } 28 | if conn.IsTLS() { 29 | return errors.New("TLS is already enabled") 30 | } 31 | if conn.Server().TLSConfig == nil { 32 | return errors.New("TLS support not enabled") 33 | } 34 | 35 | // Send an OK status response to let the client know that the TLS handshake 36 | // can begin 37 | return ErrStatusResp(&imap.StatusResp{ 38 | Type: imap.StatusRespOk, 39 | Info: "Begin TLS negotiation now", 40 | }) 41 | } 42 | 43 | func (cmd *StartTLS) Upgrade(conn Conn) error { 44 | tlsConfig := conn.Server().TLSConfig 45 | 46 | var tlsConn *tls.Conn 47 | err := conn.Upgrade(func(sock net.Conn) (net.Conn, error) { 48 | conn.WaitReady() 49 | tlsConn = tls.Server(sock, tlsConfig) 50 | err := tlsConn.Handshake() 51 | return tlsConn, err 52 | }) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | conn.setTLSConn(tlsConn) 58 | 59 | return nil 60 | } 61 | 62 | func afterAuthStatus(conn Conn) error { 63 | caps := conn.Capabilities() 64 | capAtoms := make([]interface{}, 0, len(caps)) 65 | for _, cap := range caps { 66 | capAtoms = append(capAtoms, imap.RawString(cap)) 67 | } 68 | 69 | return ErrStatusResp(&imap.StatusResp{ 70 | Type: imap.StatusRespOk, 71 | Code: imap.CodeCapability, 72 | Arguments: capAtoms, 73 | }) 74 | } 75 | 76 | func canAuth(conn Conn) bool { 77 | for _, cap := range conn.Capabilities() { 78 | if cap == "AUTH=PLAIN" { 79 | return true 80 | } 81 | } 82 | return false 83 | } 84 | 85 | type Login struct { 86 | commands.Login 87 | } 88 | 89 | func (cmd *Login) Handle(conn Conn) error { 90 | ctx := conn.Context() 91 | if ctx.State != imap.NotAuthenticatedState { 92 | return ErrAlreadyAuthenticated 93 | } 94 | if !canAuth(conn) { 95 | return ErrAuthDisabled 96 | } 97 | 98 | user, err := conn.Server().Backend.Login(conn.Info(), cmd.Username, cmd.Password) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | ctx.State = imap.AuthenticatedState 104 | ctx.User = user 105 | return afterAuthStatus(conn) 106 | } 107 | 108 | type Authenticate struct { 109 | commands.Authenticate 110 | } 111 | 112 | func (cmd *Authenticate) Handle(conn Conn) error { 113 | ctx := conn.Context() 114 | if ctx.State != imap.NotAuthenticatedState { 115 | return ErrAlreadyAuthenticated 116 | } 117 | if !canAuth(conn) { 118 | return ErrAuthDisabled 119 | } 120 | 121 | mechanisms := map[string]sasl.Server{} 122 | for name, newSasl := range conn.Server().auths { 123 | mechanisms[name] = newSasl(conn) 124 | } 125 | 126 | err := cmd.Authenticate.Handle(mechanisms, conn) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | return afterAuthStatus(conn) 132 | } 133 | -------------------------------------------------------------------------------- /server/cmd_noauth_test.go: -------------------------------------------------------------------------------- 1 | package server_test 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "io" 7 | "net" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/emersion/go-imap/internal" 12 | "github.com/emersion/go-imap/server" 13 | ) 14 | 15 | func testServerTLS(t *testing.T) (s *server.Server, c net.Conn, scanner *bufio.Scanner) { 16 | s, c, scanner = testServerGreeted(t) 17 | 18 | cert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | tlsConfig := &tls.Config{ 24 | InsecureSkipVerify: true, 25 | Certificates: []tls.Certificate{cert}, 26 | } 27 | 28 | s.AllowInsecureAuth = false 29 | s.TLSConfig = tlsConfig 30 | 31 | io.WriteString(c, "a001 CAPABILITY\r\n") 32 | scanner.Scan() 33 | if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" STARTTLS LOGINDISABLED" { 34 | t.Fatal("Bad CAPABILITY response:", scanner.Text()) 35 | } 36 | scanner.Scan() 37 | if !strings.HasPrefix(scanner.Text(), "a001 OK ") { 38 | t.Fatal("Bad status response:", scanner.Text()) 39 | } 40 | 41 | io.WriteString(c, "a001 STARTTLS\r\n") 42 | scanner.Scan() 43 | if !strings.HasPrefix(scanner.Text(), "a001 OK ") { 44 | t.Fatal("Bad status response:", scanner.Text()) 45 | } 46 | sc := tls.Client(c, tlsConfig) 47 | if err = sc.Handshake(); err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | c = sc 52 | scanner = bufio.NewScanner(c) 53 | return 54 | } 55 | 56 | func TestStartTLS(t *testing.T) { 57 | s, c, scanner := testServerTLS(t) 58 | defer s.Close() 59 | defer c.Close() 60 | 61 | io.WriteString(c, "a001 CAPABILITY\r\n") 62 | 63 | scanner.Scan() 64 | if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN" { 65 | t.Fatal("Bad CAPABILITY response:", scanner.Text()) 66 | } 67 | } 68 | 69 | func TestStartTLS_AlreadyEnabled(t *testing.T) { 70 | s, c, scanner := testServerTLS(t) 71 | defer s.Close() 72 | defer c.Close() 73 | 74 | io.WriteString(c, "a001 STARTTLS\r\n") 75 | scanner.Scan() 76 | if !strings.HasPrefix(scanner.Text(), "a001 NO ") { 77 | t.Fatal("Bad status response:", scanner.Text()) 78 | } 79 | } 80 | 81 | func TestStartTLS_NotSupported(t *testing.T) { 82 | s, c, scanner := testServerGreeted(t) 83 | defer s.Close() 84 | defer c.Close() 85 | 86 | io.WriteString(c, "a001 STARTTLS\r\n") 87 | scanner.Scan() 88 | if !strings.HasPrefix(scanner.Text(), "a001 NO ") { 89 | t.Fatal("Bad status response:", scanner.Text()) 90 | } 91 | } 92 | 93 | func TestLogin_Ok(t *testing.T) { 94 | s, c, scanner := testServerGreeted(t) 95 | defer s.Close() 96 | defer c.Close() 97 | 98 | io.WriteString(c, "a001 LOGIN username password\r\n") 99 | 100 | scanner.Scan() 101 | if !strings.HasPrefix(scanner.Text(), "a001 OK ") { 102 | t.Fatal("Bad status response:", scanner.Text()) 103 | } 104 | } 105 | 106 | func TestLogin_AlreadyAuthenticated(t *testing.T) { 107 | s, c, scanner := testServerGreeted(t) 108 | defer s.Close() 109 | defer c.Close() 110 | 111 | io.WriteString(c, "a001 LOGIN username password\r\n") 112 | 113 | scanner.Scan() 114 | if !strings.HasPrefix(scanner.Text(), "a001 OK ") { 115 | t.Fatal("Bad status response:", scanner.Text()) 116 | } 117 | 118 | io.WriteString(c, "a001 LOGIN username password\r\n") 119 | 120 | scanner.Scan() 121 | if !strings.HasPrefix(scanner.Text(), "a001 NO ") { 122 | t.Fatal("Bad status response:", scanner.Text()) 123 | } 124 | } 125 | 126 | func TestLogin_No(t *testing.T) { 127 | s, c, scanner := testServerGreeted(t) 128 | defer s.Close() 129 | defer c.Close() 130 | 131 | io.WriteString(c, "a001 LOGIN username wrongpassword\r\n") 132 | 133 | scanner.Scan() 134 | if !strings.HasPrefix(scanner.Text(), "a001 NO ") { 135 | t.Fatal("Bad status response:", scanner.Text()) 136 | } 137 | } 138 | 139 | func TestAuthenticate_Plain_Ok(t *testing.T) { 140 | s, c, scanner := testServerGreeted(t) 141 | defer s.Close() 142 | defer c.Close() 143 | 144 | io.WriteString(c, "a001 AUTHENTICATE PLAIN\r\n") 145 | 146 | scanner.Scan() 147 | if scanner.Text() != "+" { 148 | t.Fatal("Bad continuation request:", scanner.Text()) 149 | } 150 | 151 | // :usename:password 152 | io.WriteString(c, "AHVzZXJuYW1lAHBhc3N3b3Jk\r\n") 153 | 154 | scanner.Scan() 155 | if !strings.HasPrefix(scanner.Text(), "a001 OK ") { 156 | t.Fatal("Bad status response:", scanner.Text()) 157 | } 158 | } 159 | 160 | func TestAuthenticate_Plain_Cancel(t *testing.T) { 161 | s, c, scanner := testServerGreeted(t) 162 | defer s.Close() 163 | defer c.Close() 164 | 165 | io.WriteString(c, "a001 AUTHENTICATE PLAIN\r\n") 166 | 167 | scanner.Scan() 168 | if scanner.Text() != "+" { 169 | t.Fatal("Bad continuation request:", scanner.Text()) 170 | } 171 | 172 | io.WriteString(c, "*\r\n") 173 | 174 | scanner.Scan() 175 | if !strings.HasPrefix(scanner.Text(), "a001 BAD negotiation cancelled") { 176 | t.Fatal("Bad status response:", scanner.Text()) 177 | } 178 | } 179 | 180 | func TestAuthenticate_Plain_InitialResponse(t *testing.T) { 181 | s, c, scanner := testServerGreeted(t) 182 | defer s.Close() 183 | defer c.Close() 184 | 185 | io.WriteString(c, "a001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n") 186 | 187 | scanner.Scan() 188 | if !strings.HasPrefix(scanner.Text(), "a001 OK ") { 189 | t.Fatal("Bad status response:", scanner.Text()) 190 | } 191 | } 192 | 193 | func TestAuthenticate_Plain_No(t *testing.T) { 194 | s, c, scanner := testServerGreeted(t) 195 | defer s.Close() 196 | defer c.Close() 197 | 198 | io.WriteString(c, "a001 AUTHENTICATE PLAIN\r\n") 199 | 200 | scanner.Scan() 201 | if scanner.Text() != "+" { 202 | t.Fatal("Bad continuation request:", scanner.Text()) 203 | } 204 | 205 | // Invalid challenge 206 | io.WriteString(c, "BHVzZXJuYW1lAHBhc3N3b6Jk\r\n") 207 | 208 | scanner.Scan() 209 | if !strings.HasPrefix(scanner.Text(), "a001 NO ") { 210 | t.Fatal("Bad status response:", scanner.Text()) 211 | } 212 | } 213 | 214 | func TestAuthenticate_No(t *testing.T) { 215 | s, c, scanner := testServerGreeted(t) 216 | defer s.Close() 217 | defer c.Close() 218 | 219 | io.WriteString(c, "a001 AUTHENTICATE XIDONTEXIST\r\n") 220 | 221 | scanner.Scan() 222 | if !strings.HasPrefix(scanner.Text(), "a001 NO ") { 223 | t.Fatal("Bad status response:", scanner.Text()) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server_test 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "testing" 7 | 8 | "github.com/emersion/go-imap/backend/memory" 9 | "github.com/emersion/go-imap/server" 10 | ) 11 | 12 | // Extnesions that are always advertised by go-imap server. 13 | const builtinExtensions = "LITERAL+ SASL-IR CHILDREN" 14 | 15 | func testServer(t *testing.T) (s *server.Server, conn net.Conn) { 16 | bkd := memory.New() 17 | 18 | l, err := net.Listen("tcp", "127.0.0.1:0") 19 | if err != nil { 20 | t.Fatal("Cannot listen:", err) 21 | } 22 | 23 | s = server.New(bkd) 24 | s.AllowInsecureAuth = true 25 | 26 | go s.Serve(l) 27 | 28 | conn, err = net.Dial("tcp", l.Addr().String()) 29 | if err != nil { 30 | t.Fatal("Cannot connect to server:", err) 31 | } 32 | 33 | return 34 | } 35 | 36 | func TestServer_greeting(t *testing.T) { 37 | s, conn := testServer(t) 38 | defer s.Close() 39 | defer conn.Close() 40 | 41 | scanner := bufio.NewScanner(conn) 42 | 43 | scanner.Scan() // Wait for greeting 44 | greeting := scanner.Text() 45 | 46 | if greeting != "* OK [CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN] IMAP4rev1 Service Ready" { 47 | t.Fatal("Bad greeting:", greeting) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | package imap 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // A status response type. 8 | type StatusRespType string 9 | 10 | // Status response types defined in RFC 3501 section 7.1. 11 | const ( 12 | // The OK response indicates an information message from the server. When 13 | // tagged, it indicates successful completion of the associated command. 14 | // The untagged form indicates an information-only message. 15 | StatusRespOk StatusRespType = "OK" 16 | 17 | // The NO response indicates an operational error message from the 18 | // server. When tagged, it indicates unsuccessful completion of the 19 | // associated command. The untagged form indicates a warning; the 20 | // command can still complete successfully. 21 | StatusRespNo StatusRespType = "NO" 22 | 23 | // The BAD response indicates an error message from the server. When 24 | // tagged, it reports a protocol-level error in the client's command; 25 | // the tag indicates the command that caused the error. The untagged 26 | // form indicates a protocol-level error for which the associated 27 | // command can not be determined; it can also indicate an internal 28 | // server failure. 29 | StatusRespBad StatusRespType = "BAD" 30 | 31 | // The PREAUTH response is always untagged, and is one of three 32 | // possible greetings at connection startup. It indicates that the 33 | // connection has already been authenticated by external means; thus 34 | // no LOGIN command is needed. 35 | StatusRespPreauth StatusRespType = "PREAUTH" 36 | 37 | // The BYE response is always untagged, and indicates that the server 38 | // is about to close the connection. 39 | StatusRespBye StatusRespType = "BYE" 40 | ) 41 | 42 | type StatusRespCode string 43 | 44 | // Status response codes defined in RFC 3501 section 7.1. 45 | const ( 46 | CodeAlert StatusRespCode = "ALERT" 47 | CodeBadCharset StatusRespCode = "BADCHARSET" 48 | CodeCapability StatusRespCode = "CAPABILITY" 49 | CodeParse StatusRespCode = "PARSE" 50 | CodePermanentFlags StatusRespCode = "PERMANENTFLAGS" 51 | CodeReadOnly StatusRespCode = "READ-ONLY" 52 | CodeReadWrite StatusRespCode = "READ-WRITE" 53 | CodeTryCreate StatusRespCode = "TRYCREATE" 54 | CodeUidNext StatusRespCode = "UIDNEXT" 55 | CodeUidValidity StatusRespCode = "UIDVALIDITY" 56 | CodeUnseen StatusRespCode = "UNSEEN" 57 | ) 58 | 59 | // A status response. 60 | // See RFC 3501 section 7.1 61 | type StatusResp struct { 62 | // The response tag. If empty, it defaults to *. 63 | Tag string 64 | // The status type. 65 | Type StatusRespType 66 | // The status code. 67 | // See https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml 68 | Code StatusRespCode 69 | // Arguments provided with the status code. 70 | Arguments []interface{} 71 | // The status info. 72 | Info string 73 | } 74 | 75 | func (r *StatusResp) resp() {} 76 | 77 | // If this status is NO or BAD, returns an error with the status info. 78 | // Otherwise, returns nil. 79 | func (r *StatusResp) Err() error { 80 | if r == nil { 81 | // No status response, connection closed before we get one 82 | return errors.New("imap: connection closed during command execution") 83 | } 84 | 85 | if r.Type == StatusRespNo || r.Type == StatusRespBad { 86 | return errors.New(r.Info) 87 | } 88 | return nil 89 | } 90 | 91 | func (r *StatusResp) WriteTo(w *Writer) error { 92 | tag := RawString(r.Tag) 93 | if tag == "" { 94 | tag = "*" 95 | } 96 | 97 | if err := w.writeFields([]interface{}{RawString(tag), RawString(r.Type)}); err != nil { 98 | return err 99 | } 100 | 101 | if err := w.writeString(string(sp)); err != nil { 102 | return err 103 | } 104 | 105 | if r.Code != "" { 106 | if err := w.writeRespCode(r.Code, r.Arguments); err != nil { 107 | return err 108 | } 109 | 110 | if err := w.writeString(string(sp)); err != nil { 111 | return err 112 | } 113 | } 114 | 115 | if err := w.writeString(r.Info); err != nil { 116 | return err 117 | } 118 | 119 | return w.writeCrlf() 120 | } 121 | 122 | // ErrStatusResp can be returned by a server.Handler to replace the default status 123 | // response. The response tag must be empty. 124 | // 125 | // To suppress default response, Resp should be set to nil. 126 | type ErrStatusResp struct { 127 | // Response to send instead of default. 128 | Resp *StatusResp 129 | } 130 | 131 | func (err *ErrStatusResp) Error() string { 132 | if err.Resp == nil { 133 | return "imap: suppressed response" 134 | } 135 | return err.Resp.Info 136 | } 137 | -------------------------------------------------------------------------------- /status_test.go: -------------------------------------------------------------------------------- 1 | package imap_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/emersion/go-imap" 8 | ) 9 | 10 | func TestStatusResp_WriteTo(t *testing.T) { 11 | tests := []struct { 12 | input *imap.StatusResp 13 | expected string 14 | }{ 15 | { 16 | input: &imap.StatusResp{ 17 | Tag: "*", 18 | Type: imap.StatusRespOk, 19 | }, 20 | expected: "* OK \r\n", 21 | }, 22 | { 23 | input: &imap.StatusResp{ 24 | Tag: "*", 25 | Type: imap.StatusRespOk, 26 | Info: "LOGIN completed", 27 | }, 28 | expected: "* OK LOGIN completed\r\n", 29 | }, 30 | { 31 | input: &imap.StatusResp{ 32 | Tag: "42", 33 | Type: imap.StatusRespBad, 34 | Info: "Invalid arguments", 35 | }, 36 | expected: "42 BAD Invalid arguments\r\n", 37 | }, 38 | { 39 | input: &imap.StatusResp{ 40 | Tag: "a001", 41 | Type: imap.StatusRespOk, 42 | Code: "READ-ONLY", 43 | Info: "EXAMINE completed", 44 | }, 45 | expected: "a001 OK [READ-ONLY] EXAMINE completed\r\n", 46 | }, 47 | { 48 | input: &imap.StatusResp{ 49 | Tag: "*", 50 | Type: imap.StatusRespOk, 51 | Code: "CAPABILITY", 52 | Arguments: []interface{}{imap.RawString("IMAP4rev1")}, 53 | Info: "IMAP4rev1 service ready", 54 | }, 55 | expected: "* OK [CAPABILITY IMAP4rev1] IMAP4rev1 service ready\r\n", 56 | }, 57 | } 58 | 59 | for i, test := range tests { 60 | b := &bytes.Buffer{} 61 | w := imap.NewWriter(b) 62 | 63 | if err := test.input.WriteTo(w); err != nil { 64 | t.Errorf("Cannot write status #%v, got error: %v", i, err) 65 | continue 66 | } 67 | 68 | o := b.String() 69 | if o != test.expected { 70 | t.Errorf("Invalid output for status #%v: %v", i, o) 71 | } 72 | } 73 | } 74 | 75 | func TestStatus_Err(t *testing.T) { 76 | status := &imap.StatusResp{Type: imap.StatusRespOk, Info: "All green"} 77 | if err := status.Err(); err != nil { 78 | t.Error("OK status returned error:", err) 79 | } 80 | 81 | status = &imap.StatusResp{Type: imap.StatusRespBad, Info: "BAD!"} 82 | if err := status.Err(); err == nil { 83 | t.Error("BAD status didn't returned error:", err) 84 | } else if err.Error() != "BAD!" { 85 | t.Error("BAD status returned incorrect error message:", err) 86 | } 87 | 88 | status = &imap.StatusResp{Type: imap.StatusRespNo, Info: "NO!"} 89 | if err := status.Err(); err == nil { 90 | t.Error("NO status didn't returned error:", err) 91 | } else if err.Error() != "NO!" { 92 | t.Error("NO status returned incorrect error message:", err) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /utf7/decoder.go: -------------------------------------------------------------------------------- 1 | package utf7 2 | 3 | import ( 4 | "errors" 5 | "unicode/utf16" 6 | "unicode/utf8" 7 | 8 | "golang.org/x/text/transform" 9 | ) 10 | 11 | // ErrInvalidUTF7 means that a transformer encountered invalid UTF-7. 12 | var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7") 13 | 14 | type decoder struct { 15 | ascii bool 16 | } 17 | 18 | func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { 19 | for i := 0; i < len(src); i++ { 20 | ch := src[i] 21 | 22 | if ch < min || ch > max { // Illegal code point in ASCII mode 23 | err = ErrInvalidUTF7 24 | return 25 | } 26 | 27 | if ch != '&' { 28 | if nDst+1 > len(dst) { 29 | err = transform.ErrShortDst 30 | return 31 | } 32 | 33 | nSrc++ 34 | 35 | dst[nDst] = ch 36 | nDst++ 37 | 38 | d.ascii = true 39 | continue 40 | } 41 | 42 | // Find the end of the Base64 or "&-" segment 43 | start := i + 1 44 | for i++; i < len(src) && src[i] != '-'; i++ { 45 | if src[i] == '\r' || src[i] == '\n' { // base64 package ignores CR and LF 46 | err = ErrInvalidUTF7 47 | return 48 | } 49 | } 50 | 51 | if i == len(src) { // Implicit shift ("&...") 52 | if atEOF { 53 | err = ErrInvalidUTF7 54 | } else { 55 | err = transform.ErrShortSrc 56 | } 57 | return 58 | } 59 | 60 | var b []byte 61 | if i == start { // Escape sequence "&-" 62 | b = []byte{'&'} 63 | d.ascii = true 64 | } else { // Control or non-ASCII code points in base64 65 | if !d.ascii { // Null shift ("&...-&...-") 66 | err = ErrInvalidUTF7 67 | return 68 | } 69 | 70 | b = decode(src[start:i]) 71 | d.ascii = false 72 | } 73 | 74 | if len(b) == 0 { // Bad encoding 75 | err = ErrInvalidUTF7 76 | return 77 | } 78 | 79 | if nDst+len(b) > len(dst) { 80 | if atEOF { 81 | d.ascii = true 82 | } 83 | err = transform.ErrShortDst 84 | return 85 | } 86 | 87 | nSrc = i + 1 88 | 89 | for _, ch := range b { 90 | dst[nDst] = ch 91 | nDst++ 92 | } 93 | } 94 | 95 | if atEOF { 96 | d.ascii = true 97 | } 98 | 99 | return 100 | } 101 | 102 | func (d *decoder) Reset() { 103 | d.ascii = true 104 | } 105 | 106 | // Extracts UTF-16-BE bytes from base64 data and converts them to UTF-8. 107 | // A nil slice is returned if the encoding is invalid. 108 | func decode(b64 []byte) []byte { 109 | var b []byte 110 | 111 | // Allocate a single block of memory large enough to store the Base64 data 112 | // (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes. 113 | // Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence, 114 | // double the space allocation for UTF-8. 115 | if n := len(b64); b64[n-1] == '=' { 116 | return nil 117 | } else if n&3 == 0 { 118 | b = make([]byte, b64Enc.DecodedLen(n)*3) 119 | } else { 120 | n += 4 - n&3 121 | b = make([]byte, n+b64Enc.DecodedLen(n)*3) 122 | copy(b[copy(b, b64):n], []byte("==")) 123 | b64, b = b[:n], b[n:] 124 | } 125 | 126 | // Decode Base64 into the first 1/3rd of b 127 | n, err := b64Enc.Decode(b, b64) 128 | if err != nil || n&1 == 1 { 129 | return nil 130 | } 131 | 132 | // Decode UTF-16-BE into the remaining 2/3rds of b 133 | b, s := b[:n], b[n:] 134 | j := 0 135 | for i := 0; i < n; i += 2 { 136 | r := rune(b[i])<<8 | rune(b[i+1]) 137 | if utf16.IsSurrogate(r) { 138 | if i += 2; i == n { 139 | return nil 140 | } 141 | r2 := rune(b[i])<<8 | rune(b[i+1]) 142 | if r = utf16.DecodeRune(r, r2); r == repl { 143 | return nil 144 | } 145 | } else if min <= r && r <= max { 146 | return nil 147 | } 148 | j += utf8.EncodeRune(s[j:], r) 149 | } 150 | return s[:j] 151 | } 152 | -------------------------------------------------------------------------------- /utf7/decoder_test.go: -------------------------------------------------------------------------------- 1 | package utf7_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/emersion/go-imap/utf7" 7 | ) 8 | 9 | var decode = []struct { 10 | in string 11 | out string 12 | ok bool 13 | }{ 14 | // Basics (the inverse test on encode checks other valid inputs) 15 | {"", "", true}, 16 | {"abc", "abc", true}, 17 | {"&-abc", "&abc", true}, 18 | {"abc&-", "abc&", true}, 19 | {"a&-b&-c", "a&b&c", true}, 20 | {"&ABk-", "\x19", true}, 21 | {"&AB8-", "\x1F", true}, 22 | {"ABk-", "ABk-", true}, 23 | {"&-,&-&AP8-&-", "&,&\u00FF&", true}, 24 | {"&-&-,&AP8-&-", "&&,\u00FF&", true}, 25 | {"abc &- &AP8A,wD,- &- xyz", "abc & \u00FF\u00FF\u00FF & xyz", true}, 26 | 27 | // Illegal code point in ASCII 28 | {"\x00", "", false}, 29 | {"\x1F", "", false}, 30 | {"abc\n", "", false}, 31 | {"abc\x7Fxyz", "", false}, 32 | {"\uFFFD", "", false}, 33 | {"\u041C", "", false}, 34 | 35 | // Invalid Base64 alphabet 36 | {"&/+8-", "", false}, 37 | {"&*-", "", false}, 38 | {"&ZeVnLIqe -", "", false}, 39 | 40 | // CR and LF in Base64 41 | {"&ZeVnLIqe\r\n-", "", false}, 42 | {"&ZeVnLIqe\r\n\r\n-", "", false}, 43 | {"&ZeVn\r\n\r\nLIqe-", "", false}, 44 | 45 | // Padding not stripped 46 | {"&AAAAHw=-", "", false}, 47 | {"&AAAAHw==-", "", false}, 48 | {"&AAAAHwB,AIA=-", "", false}, 49 | {"&AAAAHwB,AIA==-", "", false}, 50 | 51 | // One byte short 52 | {"&2A-", "", false}, 53 | {"&2ADc-", "", false}, 54 | {"&AAAAHwB,A-", "", false}, 55 | {"&AAAAHwB,A=-", "", false}, 56 | {"&AAAAHwB,A==-", "", false}, 57 | {"&AAAAHwB,A===-", "", false}, 58 | {"&AAAAHwB,AI-", "", false}, 59 | {"&AAAAHwB,AI=-", "", false}, 60 | {"&AAAAHwB,AI==-", "", false}, 61 | 62 | // Implicit shift 63 | {"&", "", false}, 64 | {"&Jjo", "", false}, 65 | {"Jjo&", "", false}, 66 | {"&Jjo&", "", false}, 67 | {"&Jjo!", "", false}, 68 | {"&Jjo+", "", false}, 69 | {"abc&Jjo", "", false}, 70 | 71 | // Null shift 72 | {"&AGE-&Jjo-", "", false}, 73 | {"&U,BTFw-&ZeVnLIqe-", "", false}, 74 | 75 | // Long input with Base64 at the end 76 | {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa &2D3eCg- &2D3eCw- &2D3eDg-", 77 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \U0001f60a \U0001f60b \U0001f60e", true}, 78 | 79 | // ASCII in Base64 80 | {"&AGE-", "", false}, // "a" 81 | {"&ACY-", "", false}, // "&" 82 | {"&AGgAZQBsAGwAbw-", "", false}, // "hello" 83 | {"&JjoAIQ-", "", false}, // "\u263a!" 84 | 85 | // Bad surrogate 86 | {"&2AA-", "", false}, // U+D800 87 | {"&2AD-", "", false}, // U+D800 88 | {"&3AA-", "", false}, // U+DC00 89 | {"&2AAAQQ-", "", false}, // U+D800 'A' 90 | {"&2AD,,w-", "", false}, // U+D800 U+FFFF 91 | {"&3ADYAA-", "", false}, // U+DC00 U+D800 92 | } 93 | 94 | func TestDecoder(t *testing.T) { 95 | dec := utf7.Encoding.NewDecoder() 96 | 97 | for _, test := range decode { 98 | out, err := dec.String(test.in) 99 | if out != test.out { 100 | t.Errorf("UTF7Decode(%+q) expected %+q; got %+q", test.in, test.out, out) 101 | } 102 | if test.ok { 103 | if err != nil { 104 | t.Errorf("UTF7Decode(%+q) unexpected error; %v", test.in, err) 105 | } 106 | } else if err == nil { 107 | t.Errorf("UTF7Decode(%+q) expected error", test.in) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /utf7/encoder.go: -------------------------------------------------------------------------------- 1 | package utf7 2 | 3 | import ( 4 | "unicode/utf16" 5 | "unicode/utf8" 6 | 7 | "golang.org/x/text/transform" 8 | ) 9 | 10 | type encoder struct{} 11 | 12 | func (e *encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { 13 | for i := 0; i < len(src); { 14 | ch := src[i] 15 | 16 | var b []byte 17 | if min <= ch && ch <= max { 18 | b = []byte{ch} 19 | if ch == '&' { 20 | b = append(b, '-') 21 | } 22 | 23 | i++ 24 | } else { 25 | start := i 26 | 27 | // Find the next printable ASCII code point 28 | i++ 29 | for i < len(src) && (src[i] < min || src[i] > max) { 30 | i++ 31 | } 32 | 33 | if !atEOF && i == len(src) { 34 | err = transform.ErrShortSrc 35 | return 36 | } 37 | 38 | b = encode(src[start:i]) 39 | } 40 | 41 | if nDst+len(b) > len(dst) { 42 | err = transform.ErrShortDst 43 | return 44 | } 45 | 46 | nSrc = i 47 | 48 | for _, ch := range b { 49 | dst[nDst] = ch 50 | nDst++ 51 | } 52 | } 53 | 54 | return 55 | } 56 | 57 | func (e *encoder) Reset() {} 58 | 59 | // Converts string s from UTF-8 to UTF-16-BE, encodes the result as base64, 60 | // removes the padding, and adds UTF-7 shifts. 61 | func encode(s []byte) []byte { 62 | // len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no 63 | // control code points (see table below). 64 | b := make([]byte, 0, len(s)+4) 65 | for len(s) > 0 { 66 | r, size := utf8.DecodeRune(s) 67 | if r > utf8.MaxRune { 68 | r, size = utf8.RuneError, 1 // Bug fix (issue 3785) 69 | } 70 | s = s[size:] 71 | if r1, r2 := utf16.EncodeRune(r); r1 != repl { 72 | b = append(b, byte(r1>>8), byte(r1)) 73 | r = r2 74 | } 75 | b = append(b, byte(r>>8), byte(r)) 76 | } 77 | 78 | // Encode as base64 79 | n := b64Enc.EncodedLen(len(b)) + 2 80 | b64 := make([]byte, n) 81 | b64Enc.Encode(b64[1:], b) 82 | 83 | // Strip padding 84 | n -= 2 - (len(b)+2)%3 85 | b64 = b64[:n] 86 | 87 | // Add UTF-7 shifts 88 | b64[0] = '&' 89 | b64[n-1] = '-' 90 | return b64 91 | } 92 | -------------------------------------------------------------------------------- /utf7/encoder_test.go: -------------------------------------------------------------------------------- 1 | package utf7_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/emersion/go-imap/utf7" 7 | ) 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 | func TestEncoder(t *testing.T) { 118 | enc := utf7.Encoding.NewEncoder() 119 | 120 | for _, test := range encode { 121 | out, _ := enc.String(test.in) 122 | if out != test.out { 123 | t.Errorf("UTF7Encode(%+q) expected %+q; got %+q", test.in, test.out, out) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /utf7/utf7.go: -------------------------------------------------------------------------------- 1 | // Package utf7 implements modified UTF-7 encoding defined in RFC 3501 section 5.1.3 2 | package utf7 3 | 4 | import ( 5 | "encoding/base64" 6 | 7 | "golang.org/x/text/encoding" 8 | ) 9 | 10 | const ( 11 | min = 0x20 // Minimum self-representing UTF-7 value 12 | max = 0x7E // Maximum self-representing UTF-7 value 13 | 14 | repl = '\uFFFD' // Unicode replacement code point 15 | ) 16 | 17 | var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,") 18 | 19 | type enc struct{} 20 | 21 | func (e enc) NewDecoder() *encoding.Decoder { 22 | return &encoding.Decoder{ 23 | Transformer: &decoder{true}, 24 | } 25 | } 26 | 27 | func (e enc) NewEncoder() *encoding.Encoder { 28 | return &encoding.Encoder{ 29 | Transformer: &encoder{}, 30 | } 31 | } 32 | 33 | // Encoding is the modified UTF-7 encoding. 34 | var Encoding encoding.Encoding = enc{} 35 | -------------------------------------------------------------------------------- /write.go: -------------------------------------------------------------------------------- 1 | package imap 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "strconv" 9 | "time" 10 | "unicode" 11 | ) 12 | 13 | type flusher interface { 14 | Flush() error 15 | } 16 | 17 | type ( 18 | // A raw string. 19 | RawString string 20 | ) 21 | 22 | type WriterTo interface { 23 | WriteTo(w *Writer) error 24 | } 25 | 26 | func formatNumber(num uint32) string { 27 | return strconv.FormatUint(uint64(num), 10) 28 | } 29 | 30 | // Convert a string list to a field list. 31 | func FormatStringList(list []string) (fields []interface{}) { 32 | fields = make([]interface{}, len(list)) 33 | for i, v := range list { 34 | fields[i] = v 35 | } 36 | return 37 | } 38 | 39 | // Check if a string is 8-bit clean. 40 | func isAscii(s string) bool { 41 | for _, c := range s { 42 | if c > unicode.MaxASCII || unicode.IsControl(c) { 43 | return false 44 | } 45 | } 46 | return true 47 | } 48 | 49 | // An IMAP writer. 50 | type Writer struct { 51 | io.Writer 52 | 53 | AllowAsyncLiterals bool 54 | 55 | continues <-chan bool 56 | } 57 | 58 | // Helper function to write a string to w. 59 | func (w *Writer) writeString(s string) error { 60 | _, err := io.WriteString(w.Writer, s) 61 | return err 62 | } 63 | 64 | func (w *Writer) writeCrlf() error { 65 | if err := w.writeString(crlf); err != nil { 66 | return err 67 | } 68 | 69 | return w.Flush() 70 | } 71 | 72 | func (w *Writer) writeNumber(num uint32) error { 73 | return w.writeString(formatNumber(num)) 74 | } 75 | 76 | func (w *Writer) writeQuoted(s string) error { 77 | return w.writeString(strconv.Quote(s)) 78 | } 79 | 80 | func (w *Writer) writeQuotedOrLiteral(s string) error { 81 | if !isAscii(s) { 82 | // IMAP doesn't allow 8-bit data outside literals 83 | return w.writeLiteral(bytes.NewBufferString(s)) 84 | } 85 | 86 | return w.writeQuoted(s) 87 | } 88 | 89 | func (w *Writer) writeDateTime(t time.Time, layout string) error { 90 | if t.IsZero() { 91 | return w.writeString(nilAtom) 92 | } 93 | return w.writeQuoted(t.Format(layout)) 94 | } 95 | 96 | func (w *Writer) writeFields(fields []interface{}) error { 97 | for i, field := range fields { 98 | if i > 0 { // Write separator 99 | if err := w.writeString(string(sp)); err != nil { 100 | return err 101 | } 102 | } 103 | 104 | if err := w.writeField(field); err != nil { 105 | return err 106 | } 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func (w *Writer) writeList(fields []interface{}) error { 113 | if err := w.writeString(string(listStart)); err != nil { 114 | return err 115 | } 116 | 117 | if err := w.writeFields(fields); err != nil { 118 | return err 119 | } 120 | 121 | return w.writeString(string(listEnd)) 122 | } 123 | 124 | // LiteralLengthErr is returned when the Len() of the Literal object does not 125 | // match the actual length of the byte stream. 126 | type LiteralLengthErr struct { 127 | Actual int 128 | Expected int 129 | } 130 | 131 | func (e LiteralLengthErr) Error() string { 132 | return fmt.Sprintf("imap: size of Literal is not equal to Len() (%d != %d)", e.Expected, e.Actual) 133 | } 134 | 135 | func (w *Writer) writeLiteral(l Literal) error { 136 | if l == nil { 137 | return w.writeString(nilAtom) 138 | } 139 | 140 | unsyncLiteral := w.AllowAsyncLiterals && l.Len() <= 4096 141 | 142 | header := string(literalStart) + strconv.Itoa(l.Len()) 143 | if unsyncLiteral { 144 | header += string('+') 145 | } 146 | header += string(literalEnd) + crlf 147 | if err := w.writeString(header); err != nil { 148 | return err 149 | } 150 | 151 | // If a channel is available, wait for a continuation request before sending data 152 | if !unsyncLiteral && w.continues != nil { 153 | // Make sure to flush the writer, otherwise we may never receive a continuation request 154 | if err := w.Flush(); err != nil { 155 | return err 156 | } 157 | 158 | if !<-w.continues { 159 | return fmt.Errorf("imap: cannot send literal: no continuation request received") 160 | } 161 | } 162 | 163 | // In case of bufio.Buffer, it will be 0 after io.Copy. 164 | literalLen := int64(l.Len()) 165 | 166 | n, err := io.CopyN(w, l, literalLen) 167 | if err != nil { 168 | if err == io.EOF && n != literalLen { 169 | return LiteralLengthErr{int(n), l.Len()} 170 | } 171 | return err 172 | } 173 | extra, _ := io.Copy(ioutil.Discard, l) 174 | if extra != 0 { 175 | return LiteralLengthErr{int(n + extra), l.Len()} 176 | } 177 | 178 | return nil 179 | } 180 | 181 | func (w *Writer) writeField(field interface{}) error { 182 | if field == nil { 183 | return w.writeString(nilAtom) 184 | } 185 | 186 | switch field := field.(type) { 187 | case RawString: 188 | return w.writeString(string(field)) 189 | case string: 190 | return w.writeQuotedOrLiteral(field) 191 | case int: 192 | return w.writeNumber(uint32(field)) 193 | case uint32: 194 | return w.writeNumber(field) 195 | case Literal: 196 | return w.writeLiteral(field) 197 | case []interface{}: 198 | return w.writeList(field) 199 | case envelopeDateTime: 200 | return w.writeDateTime(time.Time(field), envelopeDateTimeLayout) 201 | case searchDate: 202 | return w.writeDateTime(time.Time(field), searchDateLayout) 203 | case Date: 204 | return w.writeDateTime(time.Time(field), DateLayout) 205 | case DateTime: 206 | return w.writeDateTime(time.Time(field), DateTimeLayout) 207 | case time.Time: 208 | return w.writeDateTime(field, DateTimeLayout) 209 | case *SeqSet: 210 | return w.writeString(field.String()) 211 | case *BodySectionName: 212 | // Can contain spaces - that's why we don't just pass it as a string 213 | return w.writeString(string(field.FetchItem())) 214 | } 215 | 216 | return fmt.Errorf("imap: cannot format field: %v", field) 217 | } 218 | 219 | func (w *Writer) writeRespCode(code StatusRespCode, args []interface{}) error { 220 | if err := w.writeString(string(respCodeStart)); err != nil { 221 | return err 222 | } 223 | 224 | fields := []interface{}{RawString(code)} 225 | fields = append(fields, args...) 226 | 227 | if err := w.writeFields(fields); err != nil { 228 | return err 229 | } 230 | 231 | return w.writeString(string(respCodeEnd)) 232 | } 233 | 234 | func (w *Writer) writeLine(fields ...interface{}) error { 235 | if err := w.writeFields(fields); err != nil { 236 | return err 237 | } 238 | 239 | return w.writeCrlf() 240 | } 241 | 242 | func (w *Writer) Flush() error { 243 | if f, ok := w.Writer.(flusher); ok { 244 | return f.Flush() 245 | } 246 | return nil 247 | } 248 | 249 | func NewWriter(w io.Writer) *Writer { 250 | return &Writer{Writer: w} 251 | } 252 | 253 | func NewClientWriter(w io.Writer, continues <-chan bool) *Writer { 254 | return &Writer{Writer: w, continues: continues} 255 | } 256 | --------------------------------------------------------------------------------