pax_global_header00006660000000000000000000000064141272550430014515gustar00rootroot0000000000000052 comment=84d302265aa0f1f7ab6db20a675221f5163d283a go-imap-1.2.0/000077500000000000000000000000001412725504300130465ustar00rootroot00000000000000go-imap-1.2.0/.build.yml000066400000000000000000000005561412725504300147540ustar00rootroot00000000000000image: alpine/edge packages: - go sources: - https://github.com/emersion/go-imap artifacts: - coverage.html tasks: - build: | cd go-imap go build -race -v ./... - test: | cd go-imap go test -coverprofile=coverage.txt -covermode=atomic ./... - coverage: | cd go-imap go tool cover -html=coverage.txt -o ~/coverage.html go-imap-1.2.0/.github/000077500000000000000000000000001412725504300144065ustar00rootroot00000000000000go-imap-1.2.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001412725504300165715ustar00rootroot00000000000000go-imap-1.2.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002571412725504300205650ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Question url: "https://web.libera.chat/gamja/#emersion" about: "Please ask questions in #emersion on Libera Chat" go-imap-1.2.0/.github/ISSUE_TEMPLATE/issue_template.md000066400000000000000000000004711412725504300221400ustar00rootroot00000000000000--- name: Bug report or feature request about: Report a bug or request a new feature --- go-imap-1.2.0/.gitignore000066400000000000000000000004561412725504300150430ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof /client.go /server.go coverage.txt go-imap-1.2.0/LICENSE000066400000000000000000000022041412725504300140510ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 The Go-IMAP Authors Copyright (c) 2016 emersion Copyright (c) 2016 Proton Technologies AG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-imap-1.2.0/README.md000066400000000000000000000114401412725504300143250ustar00rootroot00000000000000# go-imap [![godocs.io](https://godocs.io/github.com/emersion/go-imap?status.svg)](https://godocs.io/github.com/emersion/go-imap) [![builds.sr.ht status](https://builds.sr.ht/~emersion/go-imap/commits.svg)](https://builds.sr.ht/~emersion/go-imap/commits?) An [IMAP4rev1](https://tools.ietf.org/html/rfc3501) library written in Go. It can be used to build a client and/or a server. ## Usage ### Client [![godocs.io](https://godocs.io/github.com/emersion/go-imap/client?status.svg)](https://godocs.io/github.com/emersion/go-imap/client) ```go package main import ( "log" "github.com/emersion/go-imap/client" "github.com/emersion/go-imap" ) func main() { log.Println("Connecting to server...") // Connect to server c, err := client.DialTLS("mail.example.org:993", nil) if err != nil { log.Fatal(err) } log.Println("Connected") // Don't forget to logout defer c.Logout() // Login if err := c.Login("username", "password"); err != nil { log.Fatal(err) } log.Println("Logged in") // List mailboxes mailboxes := make(chan *imap.MailboxInfo, 10) done := make(chan error, 1) go func () { done <- c.List("", "*", mailboxes) }() log.Println("Mailboxes:") for m := range mailboxes { log.Println("* " + m.Name) } if err := <-done; err != nil { log.Fatal(err) } // Select INBOX mbox, err := c.Select("INBOX", false) if err != nil { log.Fatal(err) } log.Println("Flags for INBOX:", mbox.Flags) // Get the last 4 messages from := uint32(1) to := mbox.Messages if mbox.Messages > 3 { // We're using unsigned integers here, only subtract if the result is > 0 from = mbox.Messages - 3 } seqset := new(imap.SeqSet) seqset.AddRange(from, to) messages := make(chan *imap.Message, 10) done = make(chan error, 1) go func() { done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages) }() log.Println("Last 4 messages:") for msg := range messages { log.Println("* " + msg.Envelope.Subject) } if err := <-done; err != nil { log.Fatal(err) } log.Println("Done!") } ``` ### Server [![godocs.io](https://godocs.io/github.com/emersion/go-imap/server?status.svg)](https://godocs.io/github.com/emersion/go-imap/server) ```go package main import ( "log" "github.com/emersion/go-imap/server" "github.com/emersion/go-imap/backend/memory" ) func main() { // Create a memory backend be := memory.New() // Create a new server s := server.New(be) s.Addr = ":1143" // Since we will use this server for testing only, we can allow plain text // authentication over unencrypted connections s.AllowInsecureAuth = true log.Println("Starting IMAP server at localhost:1143") if err := s.ListenAndServe(); err != nil { log.Fatal(err) } } ``` You can now use `telnet localhost 1143` to manually connect to the server. ## Extensions Support for several IMAP extensions is included in go-imap itself. This includes: * [APPENDLIMIT](https://tools.ietf.org/html/rfc7889) * [CHILDREN](https://tools.ietf.org/html/rfc3348) * [ENABLE](https://tools.ietf.org/html/rfc5161) * [IDLE](https://tools.ietf.org/html/rfc2177) * [IMPORTANT](https://tools.ietf.org/html/rfc8457) * [LITERAL+](https://tools.ietf.org/html/rfc7888) * [MOVE](https://tools.ietf.org/html/rfc6851) * [SASL-IR](https://tools.ietf.org/html/rfc4959) * [SPECIAL-USE](https://tools.ietf.org/html/rfc6154) * [UNSELECT](https://tools.ietf.org/html/rfc3691) Support for other extensions is provided via separate packages. See below. ## Extending go-imap ### Extensions Commands defined in IMAP extensions are available in other packages. See [the wiki](https://github.com/emersion/go-imap/wiki/Using-extensions#using-client-extensions) to learn how to use them. * [COMPRESS](https://github.com/emersion/go-imap-compress) * [ID](https://github.com/ProtonMail/go-imap-id) * [METADATA](https://github.com/emersion/go-imap-metadata) * [NAMESPACE](https://github.com/foxcpp/go-imap-namespace) * [QUOTA](https://github.com/emersion/go-imap-quota) * [SORT and THREAD](https://github.com/emersion/go-imap-sortthread) * [UIDPLUS](https://github.com/emersion/go-imap-uidplus) ### Server backends * [Memory](https://github.com/emersion/go-imap/tree/master/backend/memory) (for testing) * [Multi](https://github.com/emersion/go-imap-multi) * [PGP](https://github.com/emersion/go-imap-pgp) * [Proxy](https://github.com/emersion/go-imap-proxy) ### Related projects * [go-message](https://github.com/emersion/go-message) - parsing and formatting MIME and mail messages * [go-msgauth](https://github.com/emersion/go-msgauth) - handle DKIM, DMARC and Authentication-Results * [go-pgpmail](https://github.com/emersion/go-pgpmail) - decrypting and encrypting mails with OpenPGP * [go-sasl](https://github.com/emersion/go-sasl) - sending and receiving SASL authentications * [go-smtp](https://github.com/emersion/go-smtp) - building SMTP clients and servers ## License MIT go-imap-1.2.0/backend/000077500000000000000000000000001412725504300144355ustar00rootroot00000000000000go-imap-1.2.0/backend/appendlimit.go000066400000000000000000000014611412725504300172740ustar00rootroot00000000000000package backend import ( "errors" ) // An error that should be returned by User.CreateMessage when the message size // is too big. var ErrTooBig = errors.New("Message size exceeding limit") // A backend that supports retrieving per-user message size limits. type AppendLimitBackend interface { Backend // Get the fixed maximum message size in octets that the backend will accept // when creating a new message. If there is no limit, return nil. CreateMessageLimit() *uint32 } // A user that supports retrieving per-user message size limits. type AppendLimitUser interface { User // Get the fixed maximum message size in octets that the backend will accept // when creating a new message. If there is no limit, return nil. // // This overrides the global backend limit. CreateMessageLimit() *uint32 } go-imap-1.2.0/backend/backend.go000066400000000000000000000011261412725504300163530ustar00rootroot00000000000000// Package backend defines an IMAP server backend interface. package backend import ( "errors" "github.com/emersion/go-imap" ) // ErrInvalidCredentials is returned by Backend.Login when a username or a // password is incorrect. var ErrInvalidCredentials = errors.New("Invalid credentials") // Backend is an IMAP server backend. A backend operation always deals with // users. type Backend interface { // Login authenticates a user. If the username or the password is incorrect, // it returns ErrInvalidCredentials. Login(connInfo *imap.ConnInfo, username, password string) (User, error) } go-imap-1.2.0/backend/backendutil/000077500000000000000000000000001412725504300167225ustar00rootroot00000000000000go-imap-1.2.0/backend/backendutil/backendutil.go000066400000000000000000000001421412725504300215330ustar00rootroot00000000000000// Package backendutil provides utility functions to implement IMAP backends. package backendutil go-imap-1.2.0/backend/backendutil/backendutil_test.go000066400000000000000000000047151412725504300226040ustar00rootroot00000000000000package backendutil import ( "time" ) var testDate, _ = time.Parse(time.RFC1123Z, "Sat, 18 Jun 2016 12:00:00 +0900") const testHeaderString = "Content-Type: multipart/mixed; boundary=message-boundary\r\n" + "Date: Sat, 18 Jun 2016 12:00:00 +0900\r\n" + "Date: Sat, 19 Jun 2016 12:00:00 +0900\r\n" + "From: Mitsuha Miyamizu \r\n" + "Reply-To: Mitsuha Miyamizu \r\n" + "Message-Id: 42@example.org\r\n" + "Subject: Your Name.\r\n" + "To: Taki Tachibana \r\n" + "\r\n" const testHeaderFromToString = "From: Mitsuha Miyamizu \r\n" + "To: Taki Tachibana \r\n" + "\r\n" const testHeaderDateString = "Date: Sat, 18 Jun 2016 12:00:00 +0900\r\n" + "Date: Sat, 19 Jun 2016 12:00:00 +0900\r\n" + "\r\n" const testHeaderNoFromToString = "Content-Type: multipart/mixed; boundary=message-boundary\r\n" + "Date: Sat, 18 Jun 2016 12:00:00 +0900\r\n" + "Date: Sat, 19 Jun 2016 12:00:00 +0900\r\n" + "Reply-To: Mitsuha Miyamizu \r\n" + "Message-Id: 42@example.org\r\n" + "Subject: Your Name.\r\n" + "\r\n" const testAltHeaderString = "Content-Type: multipart/alternative; boundary=b2\r\n" + "\r\n" const testTextHeaderString = "Content-Disposition: inline\r\n" + "Content-Type: text/plain\r\n" + "\r\n" const testTextContentTypeString = "Content-Type: text/plain\r\n" + "\r\n" const testTextNoContentTypeString = "Content-Disposition: inline\r\n" + "\r\n" const testTextBodyString = "What's your name?" const testTextString = testTextHeaderString + testTextBodyString const testHTMLHeaderString = "Content-Disposition: inline\r\n" + "Content-Type: text/html\r\n" + "\r\n" const testHTMLBodyString = "
What's your\r\n name?
" const testHTMLString = testHTMLHeaderString + testHTMLBodyString const testAttachmentHeaderString = "Content-Disposition: attachment; filename=note.txt\r\n" + "Content-Type: text/plain\r\n" + "\r\n" const testAttachmentBodyString = "My name is Mitsuha." const testAttachmentString = testAttachmentHeaderString + testAttachmentBodyString const testBodyString = "--message-boundary\r\n" + testAltHeaderString + "\r\n--b2\r\n" + testTextString + "\r\n--b2\r\n" + testHTMLString + "\r\n--b2--\r\n" + "\r\n--message-boundary\r\n" + testAttachmentString + "\r\n--message-boundary--\r\n" const testMailString = testHeaderString + testBodyString go-imap-1.2.0/backend/backendutil/body.go000066400000000000000000000053551412725504300202160ustar00rootroot00000000000000package backendutil import ( "bytes" "errors" "io" "mime" nettextproto "net/textproto" "strings" "github.com/emersion/go-imap" "github.com/emersion/go-message/textproto" ) var errNoSuchPart = errors.New("backendutil: no such message body part") func multipartReader(header textproto.Header, body io.Reader) *textproto.MultipartReader { contentType := header.Get("Content-Type") if !strings.HasPrefix(strings.ToLower(contentType), "multipart/") { return nil } _, params, err := mime.ParseMediaType(contentType) if err != nil { return nil } return textproto.NewMultipartReader(body, params["boundary"]) } // FetchBodySection extracts a body section from a message. func FetchBodySection(header textproto.Header, body io.Reader, section *imap.BodySectionName) (imap.Literal, error) { // First, find the requested part using the provided path for i := 0; i < len(section.Path); i++ { n := section.Path[i] mr := multipartReader(header, body) if mr == nil { // First part of non-multipart message refers to the message itself. // See RFC 3501, Page 55. if len(section.Path) == 1 && section.Path[0] == 1 { break } return nil, errNoSuchPart } for j := 1; j <= n; j++ { p, err := mr.NextPart() if err == io.EOF { return nil, errNoSuchPart } else if err != nil { return nil, err } if j == n { body = p header = p.Header break } } } // Then, write the requested data to a buffer b := new(bytes.Buffer) resHeader := header if section.Fields != nil { // Copy header so we will not change value passed to us. resHeader = header.Copy() if section.NotFields { for _, fieldName := range section.Fields { resHeader.Del(fieldName) } } else { fieldsMap := make(map[string]struct{}, len(section.Fields)) for _, field := range section.Fields { fieldsMap[nettextproto.CanonicalMIMEHeaderKey(field)] = struct{}{} } for field := resHeader.Fields(); field.Next(); { if _, ok := fieldsMap[field.Key()]; !ok { field.Del() } } } } // Write the header err := textproto.WriteHeader(b, resHeader) if err != nil { return nil, err } switch section.Specifier { case imap.TextSpecifier: // The header hasn't been requested. Discard it. b.Reset() case imap.EntireSpecifier: if len(section.Path) > 0 { // When selecting a specific part by index, IMAP servers // return only the text, not the associated MIME header. b.Reset() } } // Write the body, if requested switch section.Specifier { case imap.EntireSpecifier, imap.TextSpecifier: if _, err := io.Copy(b, body); err != nil { return nil, err } } var l imap.Literal = b if section.Partial != nil { l = bytes.NewReader(section.ExtractPartial(b.Bytes())) } return l, nil } go-imap-1.2.0/backend/backendutil/body_test.go000066400000000000000000000106171412725504300212520ustar00rootroot00000000000000package backendutil import ( "bufio" "io/ioutil" "strings" "testing" "github.com/emersion/go-imap" "github.com/emersion/go-message/textproto" ) var bodyTests = []struct { section string body string }{ { section: "BODY[]", body: testMailString, }, { section: "BODY[1.1]", body: testTextBodyString, }, { section: "BODY[1.2]", body: testHTMLBodyString, }, { section: "BODY[2]", body: testAttachmentBodyString, }, { section: "BODY[HEADER]", body: testHeaderString, }, { section: "BODY[HEADER.FIELDS (From To)]", body: testHeaderFromToString, }, { section: "BODY[HEADER.FIELDS (FROM to)]", body: testHeaderFromToString, }, { section: "BODY[HEADER.FIELDS.NOT (From To)]", body: testHeaderNoFromToString, }, { section: "BODY[HEADER.FIELDS (Date)]", body: testHeaderDateString, }, { section: "BODY[1.1.HEADER]", body: testTextHeaderString, }, { section: "BODY[1.1.HEADER.FIELDS (Content-Type)]", body: testTextContentTypeString, }, { section: "BODY[1.1.HEADER.FIELDS.NOT (Content-Type)]", body: testTextNoContentTypeString, }, { section: "BODY[2.HEADER]", body: testAttachmentHeaderString, }, { section: "BODY[2.MIME]", body: testAttachmentHeaderString, }, { section: "BODY[TEXT]", body: testBodyString, }, { section: "BODY[1.1.TEXT]", body: testTextBodyString, }, { section: "BODY[2.TEXT]", body: testAttachmentBodyString, }, { section: "BODY[2.1]", body: "", }, { section: "BODY[3]", body: "", }, { section: "BODY[2.TEXT]<0.9>", body: testAttachmentBodyString[:9], }, } func TestFetchBodySection(t *testing.T) { for _, test := range bodyTests { test := test t.Run(test.section, func(t *testing.T) { bufferedBody := bufio.NewReader(strings.NewReader(testMailString)) header, err := textproto.ReadHeader(bufferedBody) if err != nil { t.Fatal("Expected no error while reading mail, got:", err) } section, err := imap.ParseBodySectionName(imap.FetchItem(test.section)) if err != nil { t.Fatal("Expected no error while parsing body section name, got:", err) } r, err := FetchBodySection(header, bufferedBody, section) if test.body == "" { if err == nil { t.Error("Expected an error while extracting non-existing body section") } } else { if err != nil { t.Fatal("Expected no error while extracting body section, got:", err) } b, err := ioutil.ReadAll(r) if err != nil { t.Fatal("Expected no error while reading body section, got:", err) } if s := string(b); s != test.body { t.Errorf("Expected body section %q to be \n%s\n but got \n%s", test.section, test.body, s) } } }) } } func TestFetchBodySection_NonMultipart(t *testing.T) { // https://tools.ietf.org/html/rfc3501#page-55: // Every message has at least one part number. Non-[MIME-IMB] // messages, and non-multipart [MIME-IMB] messages with no // encapsulated message, only have a part 1. testMsgHdr := "From: Mitsuha Miyamizu \r\n" + "To: Taki Tachibana \r\n" + "Subject: Your Name.\r\n" + "Message-Id: 42@example.org\r\n" + "\r\n" testMsgBody := "That's not multipart message. Thought it should be possible to get this text using BODY[1]." testMsg := testMsgHdr + testMsgBody tests := []struct { section string body string }{ { section: "BODY[1.MIME]", body: testMsgHdr, }, { section: "BODY[1]", body: testMsgBody, }, } for _, test := range tests { test := test t.Run(test.section, func(t *testing.T) { bufferedBody := bufio.NewReader(strings.NewReader(testMsg)) header, err := textproto.ReadHeader(bufferedBody) if err != nil { t.Fatal("Expected no error while reading mail, got:", err) } section, err := imap.ParseBodySectionName(imap.FetchItem(test.section)) if err != nil { t.Fatal("Expected no error while parsing body section name, got:", err) } r, err := FetchBodySection(header, bufferedBody, section) if err != nil { t.Fatal("Expected no error while extracting body section, got:", err) } b, err := ioutil.ReadAll(r) if err != nil { t.Fatal("Expected no error while reading body section, got:", err) } if s := string(b); s != test.body { t.Errorf("Expected body section %q to be \n%s\n but got \n%s", test.section, test.body, s) } }) } } go-imap-1.2.0/backend/backendutil/bodystructure.go000066400000000000000000000052601412725504300221720ustar00rootroot00000000000000package backendutil import ( "bufio" "bytes" "io" "io/ioutil" "mime" "strings" "github.com/emersion/go-imap" "github.com/emersion/go-message/textproto" ) type countReader struct { r io.Reader bytes uint32 newlines uint32 endsWithLF bool } func (r *countReader) Read(b []byte) (int, error) { n, err := r.r.Read(b) r.bytes += uint32(n) if n != 0 { r.newlines += uint32(bytes.Count(b[:n], []byte{'\n'})) r.endsWithLF = b[n-1] == '\n' } // If the stream does not end with a newline - count missing newline. if err == io.EOF { if !r.endsWithLF { r.newlines++ } } return n, err } // FetchBodyStructure computes a message's body structure from its content. func FetchBodyStructure(header textproto.Header, body io.Reader, extended bool) (*imap.BodyStructure, error) { bs := new(imap.BodyStructure) mediaType, mediaParams, err := mime.ParseMediaType(header.Get("Content-Type")) if err == nil { typeParts := strings.SplitN(mediaType, "/", 2) bs.MIMEType = typeParts[0] if len(typeParts) == 2 { bs.MIMESubType = typeParts[1] } bs.Params = mediaParams } else { bs.MIMEType = "text" bs.MIMESubType = "plain" } bs.Id = header.Get("Content-Id") bs.Description = header.Get("Content-Description") bs.Encoding = header.Get("Content-Transfer-Encoding") if mr := multipartReader(header, body); mr != nil { var parts []*imap.BodyStructure for { p, err := mr.NextPart() if err == io.EOF { break } else if err != nil { return nil, err } pbs, err := FetchBodyStructure(p.Header, p, extended) if err != nil { return nil, err } parts = append(parts, pbs) } bs.Parts = parts } else { countedBody := countReader{r: body} needLines := false if bs.MIMEType == "message" && bs.MIMESubType == "rfc822" { // This will result in double-buffering if body is already a // bufio.Reader (most likely it is). :\ bufBody := bufio.NewReader(&countedBody) subMsgHdr, err := textproto.ReadHeader(bufBody) if err != nil { return nil, err } bs.Envelope, err = FetchEnvelope(subMsgHdr) if err != nil { return nil, err } bs.BodyStructure, err = FetchBodyStructure(subMsgHdr, bufBody, extended) if err != nil { return nil, err } needLines = true } else if bs.MIMEType == "text" { needLines = true } if _, err := io.Copy(ioutil.Discard, &countedBody); err != nil { return nil, err } bs.Size = countedBody.bytes if needLines { bs.Lines = countedBody.newlines } } if extended { bs.Extended = true bs.Disposition, bs.DispositionParams, _ = mime.ParseMediaType(header.Get("Content-Disposition")) // TODO: bs.Language, bs.Location // TODO: bs.MD5 } return bs, nil } go-imap-1.2.0/backend/backendutil/bodystructure_test.go000066400000000000000000000036351412725504300232350ustar00rootroot00000000000000package backendutil import ( "bufio" "reflect" "strings" "testing" "github.com/emersion/go-imap" "github.com/emersion/go-message/textproto" ) var testBodyStructure = &imap.BodyStructure{ MIMEType: "multipart", MIMESubType: "mixed", Params: map[string]string{"boundary": "message-boundary"}, Parts: []*imap.BodyStructure{ { MIMEType: "multipart", MIMESubType: "alternative", Params: map[string]string{"boundary": "b2"}, Extended: true, Parts: []*imap.BodyStructure{ { MIMEType: "text", MIMESubType: "plain", Params: map[string]string{}, Extended: true, Disposition: "inline", DispositionParams: map[string]string{}, Lines: 1, Size: 17, }, { MIMEType: "text", MIMESubType: "html", Params: map[string]string{}, Extended: true, Disposition: "inline", DispositionParams: map[string]string{}, Lines: 2, Size: 37, }, }, }, { MIMEType: "text", MIMESubType: "plain", Params: map[string]string{}, Extended: true, Disposition: "attachment", DispositionParams: map[string]string{"filename": "note.txt"}, Lines: 1, Size: 19, }, }, Extended: true, } func TestFetchBodyStructure(t *testing.T) { bufferedBody := bufio.NewReader(strings.NewReader(testMailString)) header, err := textproto.ReadHeader(bufferedBody) if err != nil { t.Fatal("Expected no error while reading mail, got:", err) } bs, err := FetchBodyStructure(header, bufferedBody, true) if err != nil { t.Fatal("Expected no error while fetching body structure, got:", err) } if !reflect.DeepEqual(testBodyStructure, bs) { t.Errorf("Expected body structure \n%+v\n but got \n%+v", testBodyStructure, bs) } } go-imap-1.2.0/backend/backendutil/envelope.go000066400000000000000000000025241412725504300210710ustar00rootroot00000000000000package backendutil import ( "net/mail" "strings" "github.com/emersion/go-imap" "github.com/emersion/go-message/textproto" ) func headerAddressList(value string) ([]*imap.Address, error) { addrs, err := mail.ParseAddressList(value) if err != nil { return []*imap.Address{}, err } list := make([]*imap.Address, len(addrs)) for i, a := range addrs { parts := strings.SplitN(a.Address, "@", 2) mailbox := parts[0] var hostname string if len(parts) == 2 { hostname = parts[1] } list[i] = &imap.Address{ PersonalName: a.Name, MailboxName: mailbox, HostName: hostname, } } return list, err } // FetchEnvelope returns a message's envelope from its header. func FetchEnvelope(h textproto.Header) (*imap.Envelope, error) { env := new(imap.Envelope) env.Date, _ = mail.ParseDate(h.Get("Date")) env.Subject = h.Get("Subject") env.From, _ = headerAddressList(h.Get("From")) env.Sender, _ = headerAddressList(h.Get("Sender")) if len(env.Sender) == 0 { env.Sender = env.From } env.ReplyTo, _ = headerAddressList(h.Get("Reply-To")) if len(env.ReplyTo) == 0 { env.ReplyTo = env.From } env.To, _ = headerAddressList(h.Get("To")) env.Cc, _ = headerAddressList(h.Get("Cc")) env.Bcc, _ = headerAddressList(h.Get("Bcc")) env.InReplyTo = h.Get("In-Reply-To") env.MessageId = h.Get("Message-Id") return env, nil } go-imap-1.2.0/backend/backendutil/envelope_test.go000066400000000000000000000024111412725504300221230ustar00rootroot00000000000000package backendutil import ( "bufio" "reflect" "strings" "testing" "github.com/emersion/go-imap" "github.com/emersion/go-message/textproto" ) var testEnvelope = &imap.Envelope{ Date: testDate, Subject: "Your Name.", From: []*imap.Address{{PersonalName: "Mitsuha Miyamizu", MailboxName: "mitsuha.miyamizu", HostName: "example.org"}}, Sender: []*imap.Address{{PersonalName: "Mitsuha Miyamizu", MailboxName: "mitsuha.miyamizu", HostName: "example.org"}}, ReplyTo: []*imap.Address{{PersonalName: "Mitsuha Miyamizu", MailboxName: "mitsuha.miyamizu+replyto", HostName: "example.org"}}, To: []*imap.Address{{PersonalName: "Taki Tachibana", MailboxName: "taki.tachibana", HostName: "example.org"}}, Cc: []*imap.Address{}, Bcc: []*imap.Address{}, InReplyTo: "", MessageId: "42@example.org", } func TestFetchEnvelope(t *testing.T) { hdr, err := textproto.ReadHeader(bufio.NewReader(strings.NewReader(testMailString))) if err != nil { t.Fatal("Expected no error while reading mail, got:", err) } env, err := FetchEnvelope(hdr) if err != nil { t.Fatal("Expected no error while fetching envelope, got:", err) } if !reflect.DeepEqual(env, testEnvelope) { t.Errorf("Expected envelope \n%+v\n but got \n%+v", testEnvelope, env) } } go-imap-1.2.0/backend/backendutil/flags.go000066400000000000000000000032271412725504300203510ustar00rootroot00000000000000package backendutil import ( "github.com/emersion/go-imap" ) // UpdateFlags executes a flag operation on the flag set current. func UpdateFlags(current []string, op imap.FlagsOp, flags []string) []string { // Don't modify contents of 'flags' slice. Only modify 'current'. // See https://github.com/golang/go/wiki/SliceTricks // Re-use current's backing store newFlags := current[:0] switch op { case imap.SetFlags: hasRecent := false // keep recent flag for _, flag := range current { if flag == imap.RecentFlag { newFlags = append(newFlags, imap.RecentFlag) hasRecent = true break } } // append new flags for _, flag := range flags { if flag == imap.RecentFlag { // Make sure we don't add the recent flag multiple times. if hasRecent { // Already have the recent flag, skip. continue } hasRecent = true } // append new flag newFlags = append(newFlags, flag) } case imap.AddFlags: // keep current flags newFlags = current // Only add new flag if it isn't already in current list. for _, addFlag := range flags { found := false for _, flag := range current { if addFlag == flag { found = true break } } // new flag not found, add it. if !found { newFlags = append(newFlags, addFlag) } } case imap.RemoveFlags: // Filter current flags for _, flag := range current { remove := false for _, removeFlag := range flags { if removeFlag == flag { remove = true } } if !remove { newFlags = append(newFlags, flag) } } default: // Unknown operation, return current flags unchanged newFlags = current } return newFlags } go-imap-1.2.0/backend/backendutil/flags_test.go000066400000000000000000000041721412725504300214100ustar00rootroot00000000000000package backendutil import ( "reflect" "testing" "github.com/emersion/go-imap" ) var updateFlagsTests = []struct { op imap.FlagsOp flags []string res []string }{ { op: imap.AddFlags, flags: []string{"d", "e"}, res: []string{"a", "b", "c", "d", "e"}, }, { op: imap.AddFlags, flags: []string{"a", "d", "b"}, res: []string{"a", "b", "c", "d"}, }, { op: imap.RemoveFlags, flags: []string{"b", "v", "e", "a"}, res: []string{"c"}, }, { op: imap.SetFlags, flags: []string{"a", "d", "e"}, res: []string{"a", "d", "e"}, }, // Test unknown op for code coverage. { op: imap.FlagsOp("TestUnknownOp"), flags: []string{"a", "d", "e"}, res: []string{"a", "b", "c"}, }, } func TestUpdateFlags(t *testing.T) { flagsList := []string{"a", "b", "c"} for _, test := range updateFlagsTests { // Make a backup copy of 'test.flags' origFlags := append(test.flags[:0:0], test.flags...) // Copy flags current := append(flagsList[:0:0], flagsList...) got := UpdateFlags(current, test.op, test.flags) if !reflect.DeepEqual(got, test.res) { t.Errorf("Expected result to be \n%v\n but got \n%v", test.res, got) } // Verify that 'test.flags' wasn't modified if !reflect.DeepEqual(origFlags, test.flags) { t.Errorf("Unexpected change to operation flags list changed \nbefore %v\n after \n%v", origFlags, test.flags) } } } func TestUpdateFlags_Recent(t *testing.T) { current := []string{} current = UpdateFlags(current, imap.SetFlags, []string{imap.RecentFlag}) res := []string{imap.RecentFlag} if !reflect.DeepEqual(current, res) { t.Errorf("Expected result to be \n%v\n but got \n%v", res, current) } current = UpdateFlags(current, imap.SetFlags, []string{"something"}) res = []string{imap.RecentFlag, "something"} if !reflect.DeepEqual(current, res) { t.Errorf("Expected result to be \n%v\n but got \n%v", res, current) } current = UpdateFlags(current, imap.SetFlags, []string{"another", imap.RecentFlag}) res = []string{imap.RecentFlag, "another"} if !reflect.DeepEqual(current, res) { t.Errorf("Expected result to be \n%v\n but got \n%v", res, current) } } go-imap-1.2.0/backend/backendutil/search.go000066400000000000000000000113071412725504300205200ustar00rootroot00000000000000package backendutil import ( "bytes" "fmt" "io" "strings" "time" "github.com/emersion/go-imap" "github.com/emersion/go-message" "github.com/emersion/go-message/mail" "github.com/emersion/go-message/textproto" ) func matchString(s, substr string) bool { return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) } func bufferBody(e *message.Entity) (*bytes.Buffer, error) { b := new(bytes.Buffer) if _, err := io.Copy(b, e.Body); err != nil { return nil, err } e.Body = b return b, nil } func matchBody(e *message.Entity, substr string) (bool, error) { if s, ok := e.Body.(fmt.Stringer); ok { return matchString(s.String(), substr), nil } b, err := bufferBody(e) if err != nil { return false, err } return matchString(b.String(), substr), nil } type lengther interface { Len() int } type countWriter struct { N int } func (w *countWriter) Write(b []byte) (int, error) { w.N += len(b) return len(b), nil } func bodyLen(e *message.Entity) (int, error) { headerSize := countWriter{} textproto.WriteHeader(&headerSize, e.Header.Header) if l, ok := e.Body.(lengther); ok { return l.Len() + headerSize.N, nil } b, err := bufferBody(e) if err != nil { return 0, err } return b.Len() + headerSize.N, nil } // Match returns true if a message and its metadata matches the provided // criteria. func Match(e *message.Entity, seqNum, uid uint32, date time.Time, flags []string, c *imap.SearchCriteria) (bool, error) { // TODO: support encoded header fields for Bcc, Cc, From, To // TODO: add header size for Larger and Smaller h := mail.Header{Header: e.Header} if !c.SentBefore.IsZero() || !c.SentSince.IsZero() { t, err := h.Date() if err != nil { return false, err } t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC) if !c.SentBefore.IsZero() && !t.Before(c.SentBefore) { return false, nil } if !c.SentSince.IsZero() && t.Before(c.SentSince) { return false, nil } } for key, wantValues := range c.Header { ok := e.Header.Has(key) for _, wantValue := range wantValues { if wantValue == "" && !ok { return false, nil } if wantValue != "" { ok := false values := e.Header.FieldsByKey(key) for values.Next() { decoded, _ := values.Text() if matchString(decoded, wantValue) { ok = true break } } if !ok { return false, nil } } } } for _, body := range c.Body { if ok, err := matchBody(e, body); err != nil || !ok { return false, err } } for _, text := range c.Text { headerMatch := false for f := e.Header.Fields(); f.Next(); { decoded, err := f.Text() if err != nil { continue } if strings.Contains(f.Key()+": "+decoded, text) { headerMatch = true } } if ok, err := matchBody(e, text); err != nil || !ok && !headerMatch { return false, err } } if c.Larger > 0 || c.Smaller > 0 { n, err := bodyLen(e) if err != nil { return false, err } if c.Larger > 0 && uint32(n) <= c.Larger { return false, nil } if c.Smaller > 0 && uint32(n) >= c.Smaller { return false, nil } } if !c.Since.IsZero() || !c.Before.IsZero() { if !matchDate(date, c) { return false, nil } } if c.WithFlags != nil || c.WithoutFlags != nil { if !matchFlags(flags, c) { return false, nil } } if c.SeqNum != nil || c.Uid != nil { if !matchSeqNumAndUid(seqNum, uid, c) { return false, nil } } for _, not := range c.Not { ok, err := Match(e, seqNum, uid, date, flags, not) if err != nil || ok { return false, err } } for _, or := range c.Or { ok1, err := Match(e, seqNum, uid, date, flags, or[0]) if err != nil { return ok1, err } ok2, err := Match(e, seqNum, uid, date, flags, or[1]) if err != nil || (!ok1 && !ok2) { return false, err } } return true, nil } func matchFlags(flags []string, c *imap.SearchCriteria) bool { flagsMap := make(map[string]bool) for _, f := range flags { flagsMap[f] = true } for _, f := range c.WithFlags { if !flagsMap[f] { return false } } for _, f := range c.WithoutFlags { if flagsMap[f] { return false } } return true } func matchSeqNumAndUid(seqNum uint32, uid uint32, c *imap.SearchCriteria) bool { if c.SeqNum != nil && !c.SeqNum.Contains(seqNum) { return false } if c.Uid != nil && !c.Uid.Contains(uid) { return false } return true } func matchDate(date time.Time, c *imap.SearchCriteria) bool { // We discard time zone information by setting it to UTC. // RFC 3501 explicitly requires zone unaware date comparison. date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC) if !c.Since.IsZero() && !date.After(c.Since) { return false } if !c.Before.IsZero() && !date.Before(c.Before) { return false } return true } go-imap-1.2.0/backend/backendutil/search_test.go000066400000000000000000000231371412725504300215630ustar00rootroot00000000000000package backendutil import ( "net/textproto" "strings" "testing" "time" "github.com/emersion/go-imap" "github.com/emersion/go-message" ) var testInternalDate = time.Unix(1483997966, 0) var matchTests = []struct { criteria *imap.SearchCriteria seqNum uint32 uid uint32 date time.Time flags []string res bool }{ { criteria: &imap.SearchCriteria{ Header: textproto.MIMEHeader{"From": {"Mitsuha"}}, }, res: true, }, { criteria: &imap.SearchCriteria{ Header: textproto.MIMEHeader{"To": {"Mitsuha"}}, }, res: false, }, { criteria: &imap.SearchCriteria{SentBefore: testDate.Add(48 * time.Hour)}, res: true, }, { criteria: &imap.SearchCriteria{ Not: []*imap.SearchCriteria{{SentSince: testDate.Add(48 * time.Hour)}}, }, res: true, }, { criteria: &imap.SearchCriteria{ Not: []*imap.SearchCriteria{{Body: []string{"name"}}}, }, res: false, }, { criteria: &imap.SearchCriteria{ Text: []string{"name"}, }, res: true, }, { criteria: &imap.SearchCriteria{ Or: [][2]*imap.SearchCriteria{{ {Text: []string{"i'm not in the text"}}, {Body: []string{"i'm not in the body"}}, }}, }, res: false, }, { criteria: &imap.SearchCriteria{ Header: textproto.MIMEHeader{"Message-Id": {"42@example.org"}}, }, res: true, }, { criteria: &imap.SearchCriteria{ Header: textproto.MIMEHeader{"Message-Id": {"43@example.org"}}, }, res: false, }, { criteria: &imap.SearchCriteria{ Header: textproto.MIMEHeader{"Message-Id": {""}}, }, res: true, }, { criteria: &imap.SearchCriteria{ Header: textproto.MIMEHeader{"Totally-Not-Reply-To": {""}}, }, res: false, }, { criteria: &imap.SearchCriteria{ Larger: 10, }, res: true, }, { criteria: &imap.SearchCriteria{ Smaller: 10, }, res: false, }, { criteria: &imap.SearchCriteria{ Header: textproto.MIMEHeader{"Subject": {"your"}}, }, res: true, }, { criteria: &imap.SearchCriteria{ Header: textproto.MIMEHeader{"Subject": {"Taki"}}, }, res: false, }, { flags: []string{imap.SeenFlag}, criteria: &imap.SearchCriteria{ WithFlags: []string{imap.SeenFlag}, WithoutFlags: []string{imap.FlaggedFlag}, }, res: true, }, { flags: []string{imap.SeenFlag}, criteria: &imap.SearchCriteria{ WithFlags: []string{imap.DraftFlag}, WithoutFlags: []string{imap.FlaggedFlag}, }, res: false, }, { flags: []string{imap.SeenFlag, imap.FlaggedFlag}, criteria: &imap.SearchCriteria{ WithFlags: []string{imap.SeenFlag}, WithoutFlags: []string{imap.FlaggedFlag}, }, res: false, }, { flags: []string{imap.SeenFlag, imap.FlaggedFlag}, criteria: &imap.SearchCriteria{ Or: [][2]*imap.SearchCriteria{{ {WithFlags: []string{imap.DraftFlag}}, {WithoutFlags: []string{imap.SeenFlag}}, }}, }, res: false, }, { flags: []string{imap.SeenFlag, imap.FlaggedFlag}, criteria: &imap.SearchCriteria{ Not: []*imap.SearchCriteria{ {WithFlags: []string{imap.SeenFlag}}, }, }, res: false, }, { seqNum: 42, uid: 69, criteria: &imap.SearchCriteria{ Or: [][2]*imap.SearchCriteria{{ { Uid: new(imap.SeqSet), Not: []*imap.SearchCriteria{{SeqNum: new(imap.SeqSet)}}, }, { SeqNum: new(imap.SeqSet), }, }}, }, res: false, }, { seqNum: 42, uid: 69, criteria: &imap.SearchCriteria{ Or: [][2]*imap.SearchCriteria{{ { Uid: &imap.SeqSet{Set: []imap.Seq{{69, 69}}}, Not: []*imap.SearchCriteria{{SeqNum: new(imap.SeqSet)}}, }, { SeqNum: new(imap.SeqSet), }, }}, }, res: true, }, { seqNum: 42, uid: 69, criteria: &imap.SearchCriteria{ Or: [][2]*imap.SearchCriteria{{ { Uid: &imap.SeqSet{Set: []imap.Seq{{69, 69}}}, Not: []*imap.SearchCriteria{{ SeqNum: &imap.SeqSet{Set: []imap.Seq{imap.Seq{42, 42}}}, }}, }, { SeqNum: new(imap.SeqSet), }, }}, }, res: false, }, { seqNum: 42, uid: 69, criteria: &imap.SearchCriteria{ Or: [][2]*imap.SearchCriteria{{ { Uid: &imap.SeqSet{Set: []imap.Seq{{69, 69}}}, Not: []*imap.SearchCriteria{{ SeqNum: &imap.SeqSet{Set: []imap.Seq{{42, 42}}}, }}, }, { SeqNum: &imap.SeqSet{Set: []imap.Seq{{42, 42}}}, }, }}, }, res: true, }, { date: testInternalDate, criteria: &imap.SearchCriteria{ Or: [][2]*imap.SearchCriteria{{ { Since: testInternalDate.Add(48 * time.Hour), Not: []*imap.SearchCriteria{{ Since: testInternalDate.Add(48 * time.Hour), }}, }, { Before: testInternalDate.Add(-48 * time.Hour), }, }}, }, res: false, }, { date: testInternalDate, criteria: &imap.SearchCriteria{ Or: [][2]*imap.SearchCriteria{{ { Since: testInternalDate.Add(-48 * time.Hour), Not: []*imap.SearchCriteria{{ Since: testInternalDate.Add(48 * time.Hour), }}, }, { Before: testInternalDate.Add(-48 * time.Hour), }, }}, }, res: true, }, { date: testInternalDate, criteria: &imap.SearchCriteria{ Or: [][2]*imap.SearchCriteria{{ { Since: testInternalDate.Add(-48 * time.Hour), Not: []*imap.SearchCriteria{{ Since: testInternalDate.Add(-48 * time.Hour), }}, }, { Before: testInternalDate.Add(-48 * time.Hour), }, }}, }, res: false, }, { date: testInternalDate, criteria: &imap.SearchCriteria{ Or: [][2]*imap.SearchCriteria{{ { Since: testInternalDate.Add(-48 * time.Hour), Not: []*imap.SearchCriteria{{ Since: testInternalDate.Add(-48 * time.Hour), }}, }, { Before: testInternalDate.Add(48 * time.Hour), }, }}, }, res: true, }, } func TestMatch(t *testing.T) { for i, test := range matchTests { e, err := message.Read(strings.NewReader(testMailString)) if err != nil { t.Fatal("Expected no error while reading entity, got:", err) } ok, err := Match(e, test.seqNum, test.uid, test.date, test.flags, test.criteria) if err != nil { t.Fatal("Expected no error while matching entity, got:", err) } if test.res && !ok { t.Errorf("Expected #%v to match search criteria", i+1) } if !test.res && ok { t.Errorf("Expected #%v not to match search criteria", i+1) } } } func TestMatchEncoded(t *testing.T) { encodedTestMsg := `From: "fox.cpp" To: "fox.cpp" Subject: =?utf-8?B?0J/RgNC+0LLQtdGA0LrQsCE=?= Date: Sun, 09 Jun 2019 00:06:43 +0300 MIME-Version: 1.0 Message-ID: Content-Type: text/plain; charset=utf-8; format=flowed Content-Transfer-Encoding: quoted-printable =D0=AD=D1=82=D0=BE=D1=82 =D1=82=D0=B5=D0=BA=D1=81=D1=82 =D0=B4=D0=BE=D0=BB= =D0=B6=D0=B5=D0=BD =D0=B1=D1=8B=D1=82=D1=8C =D0=B7=D0=B0=D0=BA=D0=BE=D0=B4= =D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD =D0=B2 base64 =D0=B8=D0=BB=D0=B8 quote= d-encoding.` e, err := message.Read(strings.NewReader(encodedTestMsg)) if err != nil { t.Fatal("Expected no error while reading entity, got:", err) } // Check encoded header. crit := imap.SearchCriteria{ Header: textproto.MIMEHeader{"Subject": []string{"Проверка!"}}, } ok, err := Match(e, 0, 0, time.Now(), []string{}, &crit) if err != nil { t.Fatal("Expected no error while matching entity, got:", err) } if !ok { t.Error("Expected match for encoded header") } // Encoded body. crit = imap.SearchCriteria{ Body: []string{"или"}, } ok, err = Match(e, 0, 0, time.Now(), []string{}, &crit) if err != nil { t.Fatal("Expected no error while matching entity, got:", err) } if !ok { t.Error("Expected match for encoded body") } } func TestMatchIssue298Regression(t *testing.T) { raw1 := "Subject: 1\r\n\r\n1" raw2 := "Subject: 2\r\n\r\n22" raw3 := "Subject: 3\r\n\r\n333" e1, err := message.Read(strings.NewReader(raw1)) if err != nil { t.Fatal("Expected no error while reading entity, got:", err) } e2, err := message.Read(strings.NewReader(raw2)) if err != nil { t.Fatal("Expected no error while reading entity, got:", err) } e3, err := message.Read(strings.NewReader(raw3)) if err != nil { t.Fatal("Expected no error while reading entity, got:", err) } // Search for body size > 15 ("LARGER 15"), which should match messages #2 and #3 criteria := &imap.SearchCriteria{ Larger: 15, } ok1, err := Match(e1, 1, 101, time.Now(), nil, criteria) if err != nil { t.Fatal("Expected no error while matching entity, got:", err) } if ok1 { t.Errorf("Expected message #1 to not match search criteria") } ok2, err := Match(e2, 2, 102, time.Now(), nil, criteria) if err != nil { t.Fatal("Expected no error while matching entity, got:", err) } if !ok2 { t.Errorf("Expected message #2 to match search criteria") } ok3, err := Match(e3, 3, 103, time.Now(), nil, criteria) if err != nil { t.Fatal("Expected no error while matching entity, got:", err) } if !ok3 { t.Errorf("Expected message #3 to match search criteria") } // Search for body size < 17 ("SMALLER 17"), which should match messages #1 and #2 criteria = &imap.SearchCriteria{ Smaller: 17, } ok1, err = Match(e1, 1, 101, time.Now(), nil, criteria) if err != nil { t.Fatal("Expected no error while matching entity, got:", err) } if !ok1 { t.Errorf("Expected message #1 to match search criteria") } ok2, err = Match(e2, 2, 102, time.Now(), nil, criteria) if err != nil { t.Fatal("Expected no error while matching entity, got:", err) } if !ok2 { t.Errorf("Expected message #2 to match search criteria") } ok3, err = Match(e3, 3, 103, time.Now(), nil, criteria) if err != nil { t.Fatal("Expected no error while matching entity, got:", err) } if ok3 { t.Errorf("Expected message #3 to not match search criteria") } } go-imap-1.2.0/backend/mailbox.go000066400000000000000000000064171412725504300164270ustar00rootroot00000000000000package backend import ( "time" "github.com/emersion/go-imap" ) // Mailbox represents a mailbox belonging to a user in the mail storage system. // A mailbox operation always deals with messages. type Mailbox interface { // Name returns this mailbox name. Name() string // Info returns this mailbox info. Info() (*imap.MailboxInfo, error) // Status returns this mailbox status. The fields Name, Flags, PermanentFlags // and UnseenSeqNum in the returned MailboxStatus must be always populated. // This function does not affect the state of any messages in the mailbox. See // RFC 3501 section 6.3.10 for a list of items that can be requested. Status(items []imap.StatusItem) (*imap.MailboxStatus, error) // SetSubscribed adds or removes the mailbox to the server's set of "active" // or "subscribed" mailboxes. SetSubscribed(subscribed bool) error // Check requests a checkpoint of the currently selected mailbox. A checkpoint // refers to any implementation-dependent housekeeping associated with the // mailbox (e.g., resolving the server's in-memory state of the mailbox with // the state on its disk). A checkpoint MAY take a non-instantaneous amount of // real time to complete. If a server implementation has no such housekeeping // considerations, CHECK is equivalent to NOOP. Check() error // ListMessages returns a list of messages. seqset must be interpreted as UIDs // if uid is set to true and as message sequence numbers otherwise. See RFC // 3501 section 6.4.5 for a list of items that can be requested. // // Messages must be sent to ch. When the function returns, ch must be closed. ListMessages(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error // SearchMessages searches messages. The returned list must contain UIDs if // uid is set to true, or sequence numbers otherwise. SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) // CreateMessage appends a new message to this mailbox. The \Recent flag will // be added no matter flags is empty or not. If date is nil, the current time // will be used. // // If the Backend implements Updater, it must notify the client immediately // via a mailbox update. CreateMessage(flags []string, date time.Time, body imap.Literal) error // UpdateMessagesFlags alters flags for the specified message(s). // // If the Backend implements Updater, it must notify the client immediately // via a message update. UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, operation imap.FlagsOp, flags []string) error // CopyMessages copies the specified message(s) to the end of the specified // destination mailbox. The flags and internal date of the message(s) SHOULD // be preserved, and the Recent flag SHOULD be set, in the copy. // // If the destination mailbox does not exist, a server SHOULD return an error. // It SHOULD NOT automatically create the mailbox. // // If the Backend implements Updater, it must notify the client immediately // via a mailbox update. CopyMessages(uid bool, seqset *imap.SeqSet, dest string) error // Expunge permanently removes all messages that have the \Deleted flag set // from the currently selected mailbox. // // If the Backend implements Updater, it must notify the client immediately // via an expunge update. Expunge() error } go-imap-1.2.0/backend/memory/000077500000000000000000000000001412725504300157455ustar00rootroot00000000000000go-imap-1.2.0/backend/memory/backend.go000066400000000000000000000021511412725504300176620ustar00rootroot00000000000000// A memory backend. package memory import ( "errors" "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" ) type Backend struct { users map[string]*User } func (be *Backend) Login(_ *imap.ConnInfo, username, password string) (backend.User, error) { user, ok := be.users[username] if ok && user.password == password { return user, nil } return nil, errors.New("Bad username or password") } func New() *Backend { user := &User{username: "username", password: "password"} body := "From: contact@example.org\r\n" + "To: contact@example.org\r\n" + "Subject: A little message, just for you\r\n" + "Date: Wed, 11 May 2016 14:31:59 +0000\r\n" + "Message-ID: <0000000@localhost/>\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + "Hi there :)" user.mailboxes = map[string]*Mailbox{ "INBOX": { name: "INBOX", user: user, Messages: []*Message{ { Uid: 6, Date: time.Now(), Flags: []string{"\\Seen"}, Size: uint32(len(body)), Body: []byte(body), }, }, }, } return &Backend{ users: map[string]*User{user.username: user}, } } go-imap-1.2.0/backend/memory/mailbox.go000066400000000000000000000103741412725504300177340ustar00rootroot00000000000000package memory import ( "io/ioutil" "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/backend/backendutil" ) var Delimiter = "/" type Mailbox struct { Subscribed bool Messages []*Message name string user *User } func (mbox *Mailbox) Name() string { return mbox.name } func (mbox *Mailbox) Info() (*imap.MailboxInfo, error) { info := &imap.MailboxInfo{ Delimiter: Delimiter, Name: mbox.name, } return info, nil } func (mbox *Mailbox) uidNext() uint32 { var uid uint32 for _, msg := range mbox.Messages { if msg.Uid > uid { uid = msg.Uid } } uid++ return uid } func (mbox *Mailbox) flags() []string { flagsMap := make(map[string]bool) for _, msg := range mbox.Messages { for _, f := range msg.Flags { if !flagsMap[f] { flagsMap[f] = true } } } var flags []string for f := range flagsMap { flags = append(flags, f) } return flags } func (mbox *Mailbox) unseenSeqNum() uint32 { for i, msg := range mbox.Messages { seqNum := uint32(i + 1) seen := false for _, flag := range msg.Flags { if flag == imap.SeenFlag { seen = true break } } if !seen { return seqNum } } return 0 } func (mbox *Mailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) { status := imap.NewMailboxStatus(mbox.name, items) status.Flags = mbox.flags() status.PermanentFlags = []string{"\\*"} status.UnseenSeqNum = mbox.unseenSeqNum() for _, name := range items { switch name { case imap.StatusMessages: status.Messages = uint32(len(mbox.Messages)) case imap.StatusUidNext: status.UidNext = mbox.uidNext() case imap.StatusUidValidity: status.UidValidity = 1 case imap.StatusRecent: status.Recent = 0 // TODO case imap.StatusUnseen: status.Unseen = 0 // TODO } } return status, nil } func (mbox *Mailbox) SetSubscribed(subscribed bool) error { mbox.Subscribed = subscribed return nil } func (mbox *Mailbox) Check() error { return nil } func (mbox *Mailbox) ListMessages(uid bool, seqSet *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error { defer close(ch) for i, msg := range mbox.Messages { seqNum := uint32(i + 1) var id uint32 if uid { id = msg.Uid } else { id = seqNum } if !seqSet.Contains(id) { continue } m, err := msg.Fetch(seqNum, items) if err != nil { continue } ch <- m } return nil } func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) { var ids []uint32 for i, msg := range mbox.Messages { seqNum := uint32(i + 1) ok, err := msg.Match(seqNum, criteria) if err != nil || !ok { continue } var id uint32 if uid { id = msg.Uid } else { id = seqNum } ids = append(ids, id) } return ids, nil } func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error { if date.IsZero() { date = time.Now() } b, err := ioutil.ReadAll(body) if err != nil { return err } mbox.Messages = append(mbox.Messages, &Message{ Uid: mbox.uidNext(), Date: date, Size: uint32(len(b)), Flags: flags, Body: b, }) return nil } func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, flags []string) error { for i, msg := range mbox.Messages { var id uint32 if uid { id = msg.Uid } else { id = uint32(i + 1) } if !seqset.Contains(id) { continue } msg.Flags = backendutil.UpdateFlags(msg.Flags, op, flags) } return nil } func (mbox *Mailbox) CopyMessages(uid bool, seqset *imap.SeqSet, destName string) error { dest, ok := mbox.user.mailboxes[destName] if !ok { return backend.ErrNoSuchMailbox } for i, msg := range mbox.Messages { var id uint32 if uid { id = msg.Uid } else { id = uint32(i + 1) } if !seqset.Contains(id) { continue } msgCopy := *msg msgCopy.Uid = dest.uidNext() dest.Messages = append(dest.Messages, &msgCopy) } return nil } func (mbox *Mailbox) Expunge() error { for i := len(mbox.Messages) - 1; i >= 0; i-- { msg := mbox.Messages[i] deleted := false for _, flag := range msg.Flags { if flag == imap.DeletedFlag { deleted = true break } } if deleted { mbox.Messages = append(mbox.Messages[:i], mbox.Messages[i+1:]...) } } return nil } go-imap-1.2.0/backend/memory/message.go000066400000000000000000000034511412725504300177230ustar00rootroot00000000000000package memory import ( "bufio" "bytes" "io" "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend/backendutil" "github.com/emersion/go-message" "github.com/emersion/go-message/textproto" ) type Message struct { Uid uint32 Date time.Time Size uint32 Flags []string Body []byte } func (m *Message) entity() (*message.Entity, error) { return message.Read(bytes.NewReader(m.Body)) } func (m *Message) headerAndBody() (textproto.Header, io.Reader, error) { body := bufio.NewReader(bytes.NewReader(m.Body)) hdr, err := textproto.ReadHeader(body) return hdr, body, err } func (m *Message) Fetch(seqNum uint32, items []imap.FetchItem) (*imap.Message, error) { fetched := imap.NewMessage(seqNum, items) for _, item := range items { switch item { case imap.FetchEnvelope: hdr, _, _ := m.headerAndBody() fetched.Envelope, _ = backendutil.FetchEnvelope(hdr) case imap.FetchBody, imap.FetchBodyStructure: hdr, body, _ := m.headerAndBody() fetched.BodyStructure, _ = backendutil.FetchBodyStructure(hdr, body, item == imap.FetchBodyStructure) case imap.FetchFlags: fetched.Flags = m.Flags case imap.FetchInternalDate: fetched.InternalDate = m.Date case imap.FetchRFC822Size: fetched.Size = m.Size case imap.FetchUid: fetched.Uid = m.Uid default: section, err := imap.ParseBodySectionName(item) if err != nil { break } body := bufio.NewReader(bytes.NewReader(m.Body)) hdr, err := textproto.ReadHeader(body) if err != nil { return nil, err } l, _ := backendutil.FetchBodySection(hdr, body, section) fetched.Body[section] = l } } return fetched, nil } func (m *Message) Match(seqNum uint32, c *imap.SearchCriteria) (bool, error) { e, _ := m.entity() return backendutil.Match(e, seqNum, m.Uid, m.Date, m.Flags, c) } go-imap-1.2.0/backend/memory/user.go000066400000000000000000000027301412725504300172540ustar00rootroot00000000000000package memory import ( "errors" "github.com/emersion/go-imap/backend" ) type User struct { username string password string mailboxes map[string]*Mailbox } func (u *User) Username() string { return u.username } func (u *User) ListMailboxes(subscribed bool) (mailboxes []backend.Mailbox, err error) { for _, mailbox := range u.mailboxes { if subscribed && !mailbox.Subscribed { continue } mailboxes = append(mailboxes, mailbox) } return } func (u *User) GetMailbox(name string) (mailbox backend.Mailbox, err error) { mailbox, ok := u.mailboxes[name] if !ok { err = errors.New("No such mailbox") } return } func (u *User) CreateMailbox(name string) error { if _, ok := u.mailboxes[name]; ok { return errors.New("Mailbox already exists") } u.mailboxes[name] = &Mailbox{name: name, user: u} return nil } func (u *User) DeleteMailbox(name string) error { if name == "INBOX" { return errors.New("Cannot delete INBOX") } if _, ok := u.mailboxes[name]; !ok { return errors.New("No such mailbox") } delete(u.mailboxes, name) return nil } func (u *User) RenameMailbox(existingName, newName string) error { mbox, ok := u.mailboxes[existingName] if !ok { return errors.New("No such mailbox") } u.mailboxes[newName] = &Mailbox{ name: newName, Messages: mbox.Messages, user: u, } mbox.Messages = nil if existingName != "INBOX" { delete(u.mailboxes, existingName) } return nil } func (u *User) Logout() error { return nil } go-imap-1.2.0/backend/move.go000066400000000000000000000012001412725504300157230ustar00rootroot00000000000000package backend import ( "github.com/emersion/go-imap" ) // MoveMailbox is a mailbox that supports moving messages. type MoveMailbox interface { Mailbox // Move the specified message(s) to the end of the specified destination // mailbox. This means that a new message is created in the target mailbox // with a new UID, the original message is removed from the source mailbox, // and it appears to the client as a single action. // // If the destination mailbox does not exist, a server SHOULD return an error. // It SHOULD NOT automatically create the mailbox. MoveMessages(uid bool, seqset *imap.SeqSet, dest string) error } go-imap-1.2.0/backend/updates.go000066400000000000000000000044331412725504300164350ustar00rootroot00000000000000package backend import ( "github.com/emersion/go-imap" ) // Update contains user and mailbox information about an unilateral backend // update. type Update interface { // The user targeted by this update. If empty, all connected users will // be notified. Username() string // The mailbox targeted by this update. If empty, the update targets all // mailboxes. Mailbox() string // Done returns a channel that is closed when the update has been broadcast to // all clients. Done() chan struct{} } // NewUpdate creates a new update. func NewUpdate(username, mailbox string) Update { return &update{ username: username, mailbox: mailbox, } } type update struct { username string mailbox string done chan struct{} } func (u *update) Username() string { return u.username } func (u *update) Mailbox() string { return u.mailbox } func (u *update) Done() chan struct{} { if u.done == nil { u.done = make(chan struct{}) } return u.done } // StatusUpdate is a status update. See RFC 3501 section 7.1 for a list of // status responses. type StatusUpdate struct { Update *imap.StatusResp } // MailboxUpdate is a mailbox update. type MailboxUpdate struct { Update *imap.MailboxStatus } // MailboxInfoUpdate is a maiblox info update. type MailboxInfoUpdate struct { Update *imap.MailboxInfo } // MessageUpdate is a message update. type MessageUpdate struct { Update *imap.Message } // ExpungeUpdate is an expunge update. type ExpungeUpdate struct { Update SeqNum uint32 } // BackendUpdater is a Backend that implements Updater is able to send // unilateral backend updates. Backends not implementing this interface don't // correctly send unilateral updates, for instance if a user logs in from two // connections and deletes a message from one of them, the over is not aware // that such a mesage has been deleted. More importantly, backends implementing // Updater can notify the user for external updates such as new message // notifications. type BackendUpdater interface { // Updates returns a set of channels where updates are sent to. Updates() <-chan Update } // MailboxPoller is a Mailbox that is able to poll updates for new messages or // message status updates during a period of inactivity. type MailboxPoller interface { // Poll requests mailbox updates. Poll() error } go-imap-1.2.0/backend/user.go000066400000000000000000000103631412725504300157450ustar00rootroot00000000000000package backend import "errors" var ( // ErrNoSuchMailbox is returned by User.GetMailbox, User.DeleteMailbox and // User.RenameMailbox when retrieving, deleting or renaming a mailbox that // doesn't exist. ErrNoSuchMailbox = errors.New("No such mailbox") // ErrMailboxAlreadyExists is returned by User.CreateMailbox and // User.RenameMailbox when creating or renaming mailbox that already exists. ErrMailboxAlreadyExists = errors.New("Mailbox already exists") ) // User represents a user in the mail storage system. A user operation always // deals with mailboxes. type User interface { // Username returns this user's username. Username() string // ListMailboxes returns a list of mailboxes belonging to this user. If // subscribed is set to true, only returns subscribed mailboxes. ListMailboxes(subscribed bool) ([]Mailbox, error) // GetMailbox returns a mailbox. If it doesn't exist, it returns // ErrNoSuchMailbox. GetMailbox(name string) (Mailbox, error) // CreateMailbox creates a new mailbox. // // If the mailbox already exists, an error must be returned. If the mailbox // name is suffixed with the server's hierarchy separator character, this is a // declaration that the client intends to create mailbox names under this name // in the hierarchy. // // If the server's hierarchy separator character appears elsewhere in the // name, the server SHOULD create any superior hierarchical names that are // needed for the CREATE command to be successfully completed. In other // words, an attempt to create "foo/bar/zap" on a server in which "/" is the // hierarchy separator character SHOULD create foo/ and foo/bar/ if they do // not already exist. // // If a new mailbox is created with the same name as a mailbox which was // deleted, its unique identifiers MUST be greater than any unique identifiers // used in the previous incarnation of the mailbox UNLESS the new incarnation // has a different unique identifier validity value. CreateMailbox(name string) error // DeleteMailbox permanently remove the mailbox with the given name. It is an // error to // attempt to delete INBOX or a mailbox name that does not exist. // // The DELETE command MUST NOT remove inferior hierarchical names. For // example, if a mailbox "foo" has an inferior "foo.bar" (assuming "." is the // hierarchy delimiter character), removing "foo" MUST NOT remove "foo.bar". // // The value of the highest-used unique identifier of the deleted mailbox MUST // be preserved so that a new mailbox created with the same name will not // reuse the identifiers of the former incarnation, UNLESS the new incarnation // has a different unique identifier validity value. DeleteMailbox(name string) error // RenameMailbox changes the name of a mailbox. It is an error to attempt to // rename from a mailbox name that does not exist or to a mailbox name that // already exists. // // If the name has inferior hierarchical names, then the inferior hierarchical // names MUST also be renamed. For example, a rename of "foo" to "zap" will // rename "foo/bar" (assuming "/" is the hierarchy delimiter character) to // "zap/bar". // // If the server's hierarchy separator character appears in the name, the // server SHOULD create any superior hierarchical names that are needed for // the RENAME command to complete successfully. In other words, an attempt to // rename "foo/bar/zap" to baz/rag/zowie on a server in which "/" is the // hierarchy separator character SHOULD create baz/ and baz/rag/ if they do // not already exist. // // The value of the highest-used unique identifier of the old mailbox name // MUST be preserved so that a new mailbox created with the same name will not // reuse the identifiers of the former incarnation, UNLESS the new incarnation // has a different unique identifier validity value. // // Renaming INBOX is permitted, and has special behavior. It moves all // messages in INBOX to a new mailbox with the given name, leaving INBOX // empty. If the server implementation supports inferior hierarchical names // of INBOX, these are unaffected by a rename of INBOX. RenameMailbox(existingName, newName string) error // Logout is called when this User will no longer be used, likely because the // client closed the connection. Logout() error } go-imap-1.2.0/client/000077500000000000000000000000001412725504300143245ustar00rootroot00000000000000go-imap-1.2.0/client/client.go000066400000000000000000000426171412725504300161430ustar00rootroot00000000000000// Package client provides an IMAP client. // // It is not safe to use the same Client from multiple goroutines. In general, // the IMAP protocol doesn't make it possible to send multiple independent // IMAP commands on the same connection. package client import ( "crypto/tls" "fmt" "io" "log" "net" "os" "sync" "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" ) // errClosed is used when a connection is closed while waiting for a command // response. var errClosed = fmt.Errorf("imap: connection closed") // errUnregisterHandler is returned by a response handler to unregister itself. var errUnregisterHandler = fmt.Errorf("imap: unregister handler") // Update is an unilateral server update. type Update interface { update() } // StatusUpdate is delivered when a status update is received. type StatusUpdate struct { Status *imap.StatusResp } func (u *StatusUpdate) update() {} // MailboxUpdate is delivered when a mailbox status changes. type MailboxUpdate struct { Mailbox *imap.MailboxStatus } func (u *MailboxUpdate) update() {} // ExpungeUpdate is delivered when a message is deleted. type ExpungeUpdate struct { SeqNum uint32 } func (u *ExpungeUpdate) update() {} // MessageUpdate is delivered when a message attribute changes. type MessageUpdate struct { Message *imap.Message } func (u *MessageUpdate) update() {} // Client is an IMAP client. type Client struct { conn *imap.Conn isTLS bool serverName string loggedOut chan struct{} continues chan<- bool upgrading bool handlers []responses.Handler handlersLocker sync.Mutex // The current connection state. state imap.ConnState // The selected mailbox, if there is one. mailbox *imap.MailboxStatus // The cached server capabilities. caps map[string]bool // state, mailbox and caps may be accessed in different goroutines. Protect // access. locker sync.Mutex // A channel to which unilateral updates from the server will be sent. An // update can be one of: *StatusUpdate, *MailboxUpdate, *MessageUpdate, // *ExpungeUpdate. Note that blocking this channel blocks the whole client, // so it's recommended to use a separate goroutine and a buffered channel to // prevent deadlocks. Updates chan<- Update // ErrorLog specifies an optional logger for errors accepting connections and // unexpected behavior from handlers. By default, logging goes to os.Stderr // via the log package's standard logger. The logger must be safe to use // simultaneously from multiple goroutines. ErrorLog imap.Logger // Timeout specifies a maximum amount of time to wait on a command. // // A Timeout of zero means no timeout. This is the default. Timeout time.Duration } func (c *Client) registerHandler(h responses.Handler) { if h == nil { return } c.handlersLocker.Lock() c.handlers = append(c.handlers, h) c.handlersLocker.Unlock() } func (c *Client) handle(resp imap.Resp) error { c.handlersLocker.Lock() for i := len(c.handlers) - 1; i >= 0; i-- { if err := c.handlers[i].Handle(resp); err != responses.ErrUnhandled { if err == errUnregisterHandler { c.handlers = append(c.handlers[:i], c.handlers[i+1:]...) err = nil } c.handlersLocker.Unlock() return err } } c.handlersLocker.Unlock() return responses.ErrUnhandled } func (c *Client) reader() { defer close(c.loggedOut) // Loop while connected. for { connected, err := c.readOnce() if err != nil { c.ErrorLog.Println("error reading response:", err) } if !connected { return } } } func (c *Client) readOnce() (bool, error) { if c.State() == imap.LogoutState { return false, nil } resp, err := imap.ReadResp(c.conn.Reader) if err == io.EOF || c.State() == imap.LogoutState { return false, nil } else if err != nil { if imap.IsParseError(err) { return true, err } else { return false, err } } if err := c.handle(resp); err != nil && err != responses.ErrUnhandled { c.ErrorLog.Println("cannot handle response ", resp, err) } return true, nil } func (c *Client) writeReply(reply []byte) error { if _, err := c.conn.Writer.Write(reply); err != nil { return err } // Flush reply return c.conn.Writer.Flush() } type handleResult struct { status *imap.StatusResp err error } func (c *Client) execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) { cmd := cmdr.Command() cmd.Tag = generateTag() var replies <-chan []byte if replier, ok := h.(responses.Replier); ok { replies = replier.Replies() } if c.Timeout > 0 { err := c.conn.SetDeadline(time.Now().Add(c.Timeout)) if err != nil { return nil, err } } else { // It's possible the client had a timeout set from a previous command, but no // longer does. Ensure we respect that. The zero time means no deadline. if err := c.conn.SetDeadline(time.Time{}); err != nil { return nil, err } } // Check if we are upgrading. upgrading := c.upgrading // Add handler before sending command, to be sure to get the response in time // (in tests, the response is sent right after our command is received, so // sometimes the response was received before the setup of this handler) doneHandle := make(chan handleResult, 1) unregister := make(chan struct{}) c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error { select { case <-unregister: // If an error occured while sending the command, abort return errUnregisterHandler default: } if s, ok := resp.(*imap.StatusResp); ok && s.Tag == cmd.Tag { // This is the command's status response, we're done doneHandle <- handleResult{s, nil} // Special handling of connection upgrading. if upgrading { c.upgrading = false // Wait for upgrade to finish. c.conn.Wait() } // Cancel any pending literal write select { case c.continues <- false: default: } return errUnregisterHandler } if h != nil { // Pass the response to the response handler if err := h.Handle(resp); err != nil && err != responses.ErrUnhandled { // If the response handler returns an error, abort doneHandle <- handleResult{nil, err} return errUnregisterHandler } else { return err } } return responses.ErrUnhandled })) // Send the command to the server if err := cmd.WriteTo(c.conn.Writer); err != nil { // Error while sending the command close(unregister) if err, ok := err.(imap.LiteralLengthErr); ok { // Expected > Actual // The server is waiting for us to write // more bytes, we don't have them. Run. // Expected < Actual // We are about to send a potentially truncated message, we don't // want this (ths terminating CRLF is not sent at this point). c.conn.Close() return nil, err } return nil, err } // Flush writer if we are upgrading if upgrading { if err := c.conn.Writer.Flush(); err != nil { // Error while sending the command close(unregister) return nil, err } } for { select { case reply := <-replies: // Response handler needs to send a reply (Used for AUTHENTICATE) if err := c.writeReply(reply); err != nil { close(unregister) return nil, err } case <-c.loggedOut: // If the connection is closed (such as from an I/O error), ensure we // realize this and don't block waiting on a response that will never // come. loggedOut is a channel that closes when the reader goroutine // ends. close(unregister) return nil, errClosed case result := <-doneHandle: return result.status, result.err } } } // State returns the current connection state. func (c *Client) State() imap.ConnState { c.locker.Lock() state := c.state c.locker.Unlock() return state } // Mailbox returns the selected mailbox. It returns nil if there isn't one. func (c *Client) Mailbox() *imap.MailboxStatus { // c.Mailbox fields are not supposed to change, so we can return the pointer. c.locker.Lock() mbox := c.mailbox c.locker.Unlock() return mbox } // SetState sets this connection's internal state. // // This function should not be called directly, it must only be used by // libraries implementing extensions of the IMAP protocol. func (c *Client) SetState(state imap.ConnState, mailbox *imap.MailboxStatus) { c.locker.Lock() c.state = state c.mailbox = mailbox c.locker.Unlock() } // Execute executes a generic command. cmdr is a value that can be converted to // a raw command and h is a response handler. The function returns when the // command has completed or failed, in this case err is nil. A non-nil err value // indicates a network error. // // This function should not be called directly, it must only be used by // libraries implementing extensions of the IMAP protocol. func (c *Client) Execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) { return c.execute(cmdr, h) } func (c *Client) handleContinuationReqs() { c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error { if _, ok := resp.(*imap.ContinuationReq); ok { go func() { c.continues <- true }() return nil } return responses.ErrUnhandled })) } func (c *Client) gotStatusCaps(args []interface{}) { c.locker.Lock() c.caps = make(map[string]bool) for _, cap := range args { if cap, ok := cap.(string); ok { c.caps[cap] = true } } c.locker.Unlock() } // The server can send unilateral data. This function handles it. func (c *Client) handleUnilateral() { c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error { switch resp := resp.(type) { case *imap.StatusResp: if resp.Tag != "*" { return responses.ErrUnhandled } switch resp.Type { case imap.StatusRespOk, imap.StatusRespNo, imap.StatusRespBad: if c.Updates != nil { c.Updates <- &StatusUpdate{resp} } case imap.StatusRespBye: c.locker.Lock() c.state = imap.LogoutState c.mailbox = nil c.locker.Unlock() c.conn.Close() if c.Updates != nil { c.Updates <- &StatusUpdate{resp} } default: return responses.ErrUnhandled } case *imap.DataResp: name, fields, ok := imap.ParseNamedResp(resp) if !ok { return responses.ErrUnhandled } switch name { case "CAPABILITY": c.gotStatusCaps(fields) case "EXISTS": if c.Mailbox() == nil { break } if messages, err := imap.ParseNumber(fields[0]); err == nil { c.locker.Lock() c.mailbox.Messages = messages c.locker.Unlock() c.mailbox.ItemsLocker.Lock() c.mailbox.Items[imap.StatusMessages] = nil c.mailbox.ItemsLocker.Unlock() } if c.Updates != nil { c.Updates <- &MailboxUpdate{c.Mailbox()} } case "RECENT": if c.Mailbox() == nil { break } if recent, err := imap.ParseNumber(fields[0]); err == nil { c.locker.Lock() c.mailbox.Recent = recent c.locker.Unlock() c.mailbox.ItemsLocker.Lock() c.mailbox.Items[imap.StatusRecent] = nil c.mailbox.ItemsLocker.Unlock() } if c.Updates != nil { c.Updates <- &MailboxUpdate{c.Mailbox()} } case "EXPUNGE": seqNum, _ := imap.ParseNumber(fields[0]) if c.Updates != nil { c.Updates <- &ExpungeUpdate{seqNum} } case "FETCH": seqNum, _ := imap.ParseNumber(fields[0]) fields, _ := fields[1].([]interface{}) msg := &imap.Message{SeqNum: seqNum} if err := msg.Parse(fields); err != nil { break } if c.Updates != nil { c.Updates <- &MessageUpdate{msg} } default: return responses.ErrUnhandled } default: return responses.ErrUnhandled } return nil })) } func (c *Client) handleGreetAndStartReading() error { var greetErr error gotGreet := false c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error { status, ok := resp.(*imap.StatusResp) if !ok { greetErr = fmt.Errorf("invalid greeting received from server: not a status response") return errUnregisterHandler } c.locker.Lock() switch status.Type { case imap.StatusRespPreauth: c.state = imap.AuthenticatedState case imap.StatusRespBye: c.state = imap.LogoutState case imap.StatusRespOk: c.state = imap.NotAuthenticatedState default: c.state = imap.LogoutState c.locker.Unlock() greetErr = fmt.Errorf("invalid greeting received from server: %v", status.Type) return errUnregisterHandler } c.locker.Unlock() if status.Code == imap.CodeCapability { c.gotStatusCaps(status.Arguments) } gotGreet = true return errUnregisterHandler })) // call `readOnce` until we get the greeting or an error for !gotGreet { connected, err := c.readOnce() // Check for read errors if err != nil { // return read errors return err } // Check for invalid greet if greetErr != nil { // return read errors return greetErr } // Check if connection was closed. if !connected { // connection closed. return io.EOF } } // We got the greeting, now start the reader goroutine. go c.reader() return nil } // Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted // tunnel. // // This function should not be called directly, it must only be used by // libraries implementing extensions of the IMAP protocol. func (c *Client) Upgrade(upgrader imap.ConnUpgrader) error { return c.conn.Upgrade(upgrader) } // Writer returns the imap.Writer for this client's connection. // // This function should not be called directly, it must only be used by // libraries implementing extensions of the IMAP protocol. func (c *Client) Writer() *imap.Writer { return c.conn.Writer } // IsTLS checks if this client's connection has TLS enabled. func (c *Client) IsTLS() bool { return c.isTLS } // LoggedOut returns a channel which is closed when the connection to the server // is closed. func (c *Client) LoggedOut() <-chan struct{} { return c.loggedOut } // SetDebug defines an io.Writer to which all network activity will be logged. // If nil is provided, network activity will not be logged. func (c *Client) SetDebug(w io.Writer) { // Need to send a command to unblock the reader goroutine. cmd := new(commands.Noop) err := c.Upgrade(func(conn net.Conn) (net.Conn, error) { // Flag connection as in upgrading c.upgrading = true if status, err := c.execute(cmd, nil); err != nil { return nil, err } else if err := status.Err(); err != nil { return nil, err } // Wait for reader to block. c.conn.WaitReady() c.conn.SetDebug(w) return conn, nil }) if err != nil { log.Println("SetDebug:", err) } } // New creates a new client from an existing connection. func New(conn net.Conn) (*Client, error) { continues := make(chan bool) w := imap.NewClientWriter(nil, continues) r := imap.NewReader(nil) c := &Client{ conn: imap.NewConn(conn, r, w), loggedOut: make(chan struct{}), continues: continues, state: imap.ConnectingState, ErrorLog: log.New(os.Stderr, "imap/client: ", log.LstdFlags), } c.handleContinuationReqs() c.handleUnilateral() if err := c.handleGreetAndStartReading(); err != nil { return c, err } plusOk, _ := c.Support("LITERAL+") minusOk, _ := c.Support("LITERAL-") // We don't use non-sync literal if it is bigger than 4096 bytes, so // LITERAL- is fine too. c.conn.AllowAsyncLiterals = plusOk || minusOk return c, nil } // Dial connects to an IMAP server using an unencrypted connection. func Dial(addr string) (*Client, error) { return DialWithDialer(new(net.Dialer), addr) } type Dialer interface { // Dial connects to the given address. Dial(network, addr string) (net.Conn, error) } // DialWithDialer connects to an IMAP server using an unencrypted connection // using dialer.Dial. // // Among other uses, this allows to apply a dial timeout. func DialWithDialer(dialer Dialer, addr string) (*Client, error) { conn, err := dialer.Dial("tcp", addr) if err != nil { return nil, err } // We don't return to the caller until we try to receive a greeting. As such, // there is no way to set the client's Timeout for that action. As a // workaround, if the dialer has a timeout set, use that for the connection's // deadline. if netDialer, ok := dialer.(*net.Dialer); ok && netDialer.Timeout > 0 { err := conn.SetDeadline(time.Now().Add(netDialer.Timeout)) if err != nil { return nil, err } } c, err := New(conn) if err != nil { return nil, err } c.serverName, _, _ = net.SplitHostPort(addr) return c, nil } // DialTLS connects to an IMAP server using an encrypted connection. func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) { return DialWithDialerTLS(new(net.Dialer), addr, tlsConfig) } // DialWithDialerTLS connects to an IMAP server using an encrypted connection // using dialer.Dial. // // Among other uses, this allows to apply a dial timeout. func DialWithDialerTLS(dialer Dialer, addr string, tlsConfig *tls.Config) (*Client, error) { conn, err := dialer.Dial("tcp", addr) if err != nil { return nil, err } serverName, _, _ := net.SplitHostPort(addr) if tlsConfig == nil { tlsConfig = &tls.Config{} } if tlsConfig.ServerName == "" { tlsConfig = tlsConfig.Clone() tlsConfig.ServerName = serverName } tlsConn := tls.Client(conn, tlsConfig) // We don't return to the caller until we try to receive a greeting. As such, // there is no way to set the client's Timeout for that action. As a // workaround, if the dialer has a timeout set, use that for the connection's // deadline. if netDialer, ok := dialer.(*net.Dialer); ok && netDialer.Timeout > 0 { err := tlsConn.SetDeadline(time.Now().Add(netDialer.Timeout)) if err != nil { return nil, err } } c, err := New(tlsConn) if err != nil { return nil, err } c.isTLS = true c.serverName = serverName return c, nil } go-imap-1.2.0/client/client_test.go000066400000000000000000000105151412725504300171720ustar00rootroot00000000000000package client import ( "bufio" "bytes" "io" "net" "strings" "testing" "github.com/emersion/go-imap" ) type cmdScanner struct { scanner *bufio.Scanner } func (s *cmdScanner) ScanLine() string { s.scanner.Scan() return s.scanner.Text() } func (s *cmdScanner) ScanCmd() (tag string, cmd string) { parts := strings.SplitN(s.ScanLine(), " ", 2) return parts[0], parts[1] } func newCmdScanner(r io.Reader) *cmdScanner { return &cmdScanner{ scanner: bufio.NewScanner(r), } } type serverConn struct { *cmdScanner net.Conn net.Listener } func (c *serverConn) Close() error { if err := c.Conn.Close(); err != nil { return err } return c.Listener.Close() } func (c *serverConn) WriteString(s string) (n int, err error) { return io.WriteString(c.Conn, s) } func newTestClient(t *testing.T) (c *Client, s *serverConn) { return newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN UNSELECT] Server ready.\r\n") } func newTestClientWithGreeting(t *testing.T, greeting string) (c *Client, s *serverConn) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } done := make(chan struct{}) go func() { conn, err := l.Accept() if err != nil { panic(err) } if _, err := io.WriteString(conn, greeting); err != nil { panic(err) } s = &serverConn{newCmdScanner(conn), conn, l} close(done) }() c, err = Dial(l.Addr().String()) if err != nil { t.Fatal(err) } <-done return } func setClientState(c *Client, state imap.ConnState, mailbox *imap.MailboxStatus) { c.locker.Lock() c.state = state c.mailbox = mailbox c.locker.Unlock() } func TestClient(t *testing.T) { c, s := newTestClient(t) defer s.Close() if ok, err := c.Support("IMAP4rev1"); err != nil { t.Fatal("c.Support(IMAP4rev1) =", err) } else if !ok { t.Fatal("c.Support(IMAP4rev1) = false, want true") } } func TestClient_SetDebug(t *testing.T) { c, s := newTestClient(t) defer s.Close() var b bytes.Buffer done := make(chan error) go func() { c.SetDebug(&b) done <- nil }() if tag, cmd := s.ScanCmd(); cmd != "NOOP" { t.Fatal("Bad command:", cmd) } else { s.WriteString(tag + " OK NOOP completed.\r\n") } // wait for SetDebug to finish. <-done go func() { _, err := c.Capability() done <- err }() tag, cmd := s.ScanCmd() if cmd != "CAPABILITY" { t.Fatal("Bad command:", cmd) } s.WriteString("* CAPABILITY IMAP4rev1\r\n") s.WriteString(tag + " OK CAPABILITY completed.\r\n") if err := <-done; err != nil { t.Fatal("c.Capability() =", err) } if b.Len() == 0 { t.Error("empty debug buffer") } } func TestClient_unilateral(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, imap.NewMailboxStatus("INBOX", nil)) updates := make(chan Update, 1) c.Updates = updates s.WriteString("* 42 EXISTS\r\n") if update, ok := (<-updates).(*MailboxUpdate); !ok || update.Mailbox.Messages != 42 { t.Errorf("Invalid messages count: expected %v but got %v", 42, update.Mailbox.Messages) } s.WriteString("* 587 RECENT\r\n") if update, ok := (<-updates).(*MailboxUpdate); !ok || update.Mailbox.Recent != 587 { t.Errorf("Invalid recent count: expected %v but got %v", 587, update.Mailbox.Recent) } s.WriteString("* 65535 EXPUNGE\r\n") if update, ok := (<-updates).(*ExpungeUpdate); !ok || update.SeqNum != 65535 { t.Errorf("Invalid expunged sequence number: expected %v but got %v", 65535, update.SeqNum) } s.WriteString("* 431 FETCH (FLAGS (\\Seen))\r\n") if update, ok := (<-updates).(*MessageUpdate); !ok || update.Message.SeqNum != 431 { t.Errorf("Invalid expunged sequence number: expected %v but got %v", 431, update.Message.SeqNum) } s.WriteString("* OK Reticulating splines...\r\n") if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Reticulating splines..." { t.Errorf("Invalid info: got %v", update.Status.Info) } s.WriteString("* NO Kansai band competition is in 30 seconds !\r\n") if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Kansai band competition is in 30 seconds !" { t.Errorf("Invalid warning: got %v", update.Status.Info) } s.WriteString("* BAD Battery level too low, shutting down.\r\n") if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Battery level too low, shutting down." { t.Errorf("Invalid error: got %v", update.Status.Info) } } go-imap-1.2.0/client/cmd_any.go000066400000000000000000000041421412725504300162660ustar00rootroot00000000000000package client import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/commands" ) // ErrAlreadyLoggedOut is returned if Logout is called when the client is // already logged out. var ErrAlreadyLoggedOut = errors.New("Already logged out") // Capability requests a listing of capabilities that the server supports. // Capabilities are often returned by the server with the greeting or with the // STARTTLS and LOGIN responses, so usually explicitly requesting capabilities // isn't needed. // // Most of the time, Support should be used instead. func (c *Client) Capability() (map[string]bool, error) { cmd := &commands.Capability{} if status, err := c.execute(cmd, nil); err != nil { return nil, err } else if err := status.Err(); err != nil { return nil, err } c.locker.Lock() caps := c.caps c.locker.Unlock() return caps, nil } // Support checks if cap is a capability supported by the server. If the server // hasn't sent its capabilities yet, Support requests them. func (c *Client) Support(cap string) (bool, error) { c.locker.Lock() ok := c.caps != nil c.locker.Unlock() // If capabilities are not cached, request them if !ok { if _, err := c.Capability(); err != nil { return false, err } } c.locker.Lock() supported := c.caps[cap] c.locker.Unlock() return supported, nil } // Noop always succeeds and does nothing. // // It can be used as a periodic poll for new messages or message status updates // during a period of inactivity. It can also be used to reset any inactivity // autologout timer on the server. func (c *Client) Noop() error { cmd := new(commands.Noop) status, err := c.execute(cmd, nil) if err != nil { return err } return status.Err() } // Logout gracefully closes the connection. func (c *Client) Logout() error { if c.State() == imap.LogoutState { return ErrAlreadyLoggedOut } cmd := new(commands.Logout) if status, err := c.execute(cmd, nil); err == errClosed { // Server closed connection, that's what we want anyway return nil } else if err != nil { return err } else if status != nil { return status.Err() } return nil } go-imap-1.2.0/client/cmd_any_test.go000066400000000000000000000030441412725504300173250ustar00rootroot00000000000000package client import ( "testing" "github.com/emersion/go-imap" ) func TestClient_Capability(t *testing.T) { c, s := newTestClient(t) defer s.Close() var caps map[string]bool done := make(chan error, 1) go func() { var err error caps, err = c.Capability() done <- err }() tag, cmd := s.ScanCmd() if cmd != "CAPABILITY" { t.Fatalf("client sent command %v, want CAPABILITY", cmd) } s.WriteString("* CAPABILITY IMAP4rev1 XTEST\r\n") s.WriteString(tag + " OK CAPABILITY completed.\r\n") if err := <-done; err != nil { t.Error("c.Capability() = ", err) } if !caps["XTEST"] { t.Error("XTEST capability missing") } } func TestClient_Noop(t *testing.T) { c, s := newTestClient(t) defer s.Close() done := make(chan error, 1) go func() { done <- c.Noop() }() tag, cmd := s.ScanCmd() if cmd != "NOOP" { t.Fatalf("client sent command %v, want NOOP", cmd) } s.WriteString(tag + " OK NOOP completed\r\n") if err := <-done; err != nil { t.Error("c.Noop() = ", err) } } func TestClient_Logout(t *testing.T) { c, s := newTestClient(t) defer s.Close() done := make(chan error, 1) go func() { done <- c.Logout() }() tag, cmd := s.ScanCmd() if cmd != "LOGOUT" { t.Fatalf("client sent command %v, want LOGOUT", cmd) } s.WriteString("* BYE Client asked to close the connection.\r\n") s.WriteString(tag + " OK LOGOUT completed\r\n") if err := <-done; err != nil { t.Error("c.Logout() =", err) } if state := c.State(); state != imap.LogoutState { t.Errorf("c.State() = %v, want %v", state, imap.LogoutState) } } go-imap-1.2.0/client/cmd_auth.go000066400000000000000000000211501412725504300164360ustar00rootroot00000000000000package client import ( "errors" "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" ) // ErrNotLoggedIn is returned if a function that requires the client to be // logged in is called then the client isn't. var ErrNotLoggedIn = errors.New("Not logged in") func (c *Client) ensureAuthenticated() error { state := c.State() if state != imap.AuthenticatedState && state != imap.SelectedState { return ErrNotLoggedIn } return nil } // Select selects a mailbox so that messages in the mailbox can be accessed. Any // currently selected mailbox is deselected before attempting the new selection. // Even if the readOnly parameter is set to false, the server can decide to open // the mailbox in read-only mode. func (c *Client) Select(name string, readOnly bool) (*imap.MailboxStatus, error) { if err := c.ensureAuthenticated(); err != nil { return nil, err } cmd := &commands.Select{ Mailbox: name, ReadOnly: readOnly, } mbox := &imap.MailboxStatus{Name: name, Items: make(map[imap.StatusItem]interface{})} res := &responses.Select{ Mailbox: mbox, } c.locker.Lock() c.mailbox = mbox c.locker.Unlock() status, err := c.execute(cmd, res) if err != nil { c.locker.Lock() c.mailbox = nil c.locker.Unlock() return nil, err } if err := status.Err(); err != nil { c.locker.Lock() c.mailbox = nil c.locker.Unlock() return nil, err } c.locker.Lock() mbox.ReadOnly = (status.Code == imap.CodeReadOnly) c.state = imap.SelectedState c.locker.Unlock() return mbox, nil } // Create creates a mailbox with the given name. func (c *Client) Create(name string) error { if err := c.ensureAuthenticated(); err != nil { return err } cmd := &commands.Create{ Mailbox: name, } status, err := c.execute(cmd, nil) if err != nil { return err } return status.Err() } // Delete permanently removes the mailbox with the given name. func (c *Client) Delete(name string) error { if err := c.ensureAuthenticated(); err != nil { return err } cmd := &commands.Delete{ Mailbox: name, } status, err := c.execute(cmd, nil) if err != nil { return err } return status.Err() } // Rename changes the name of a mailbox. func (c *Client) Rename(existingName, newName string) error { if err := c.ensureAuthenticated(); err != nil { return err } cmd := &commands.Rename{ Existing: existingName, New: newName, } status, err := c.execute(cmd, nil) if err != nil { return err } return status.Err() } // Subscribe adds the specified mailbox name to the server's set of "active" or // "subscribed" mailboxes. func (c *Client) Subscribe(name string) error { if err := c.ensureAuthenticated(); err != nil { return err } cmd := &commands.Subscribe{ Mailbox: name, } status, err := c.execute(cmd, nil) if err != nil { return err } return status.Err() } // Unsubscribe removes the specified mailbox name from the server's set of // "active" or "subscribed" mailboxes. func (c *Client) Unsubscribe(name string) error { if err := c.ensureAuthenticated(); err != nil { return err } cmd := &commands.Unsubscribe{ Mailbox: name, } status, err := c.execute(cmd, nil) if err != nil { return err } return status.Err() } // List returns a subset of names from the complete set of all names available // to the client. // // An empty name argument is a special request to return the hierarchy delimiter // and the root name of the name given in the reference. The character "*" is a // wildcard, and matches zero or more characters at this position. The // character "%" is similar to "*", but it does not match a hierarchy delimiter. func (c *Client) List(ref, name string, ch chan *imap.MailboxInfo) error { defer close(ch) if err := c.ensureAuthenticated(); err != nil { return err } cmd := &commands.List{ Reference: ref, Mailbox: name, } res := &responses.List{Mailboxes: ch} status, err := c.execute(cmd, res) if err != nil { return err } return status.Err() } // Lsub returns a subset of names from the set of names that the user has // declared as being "active" or "subscribed". func (c *Client) Lsub(ref, name string, ch chan *imap.MailboxInfo) error { defer close(ch) if err := c.ensureAuthenticated(); err != nil { return err } cmd := &commands.List{ Reference: ref, Mailbox: name, Subscribed: true, } res := &responses.List{ Mailboxes: ch, Subscribed: true, } status, err := c.execute(cmd, res) if err != nil { return err } return status.Err() } // Status requests the status of the indicated mailbox. It does not change the // currently selected mailbox, nor does it affect the state of any messages in // the queried mailbox. // // See RFC 3501 section 6.3.10 for a list of items that can be requested. func (c *Client) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) { if err := c.ensureAuthenticated(); err != nil { return nil, err } cmd := &commands.Status{ Mailbox: name, Items: items, } res := &responses.Status{ Mailbox: new(imap.MailboxStatus), } status, err := c.execute(cmd, res) if err != nil { return nil, err } return res.Mailbox, status.Err() } // Append appends the literal argument as a new message to the end of the // specified destination mailbox. This argument SHOULD be in the format of an // RFC 2822 message. flags and date are optional arguments and can be set to // nil and the empty struct. func (c *Client) Append(mbox string, flags []string, date time.Time, msg imap.Literal) error { if err := c.ensureAuthenticated(); err != nil { return err } cmd := &commands.Append{ Mailbox: mbox, Flags: flags, Date: date, Message: msg, } status, err := c.execute(cmd, nil) if err != nil { return err } return status.Err() } // Enable requests the server to enable the named extensions. The extensions // which were successfully enabled are returned. // // See RFC 5161 section 3.1. func (c *Client) Enable(caps []string) ([]string, error) { if ok, err := c.Support("ENABLE"); !ok || err != nil { return nil, ErrExtensionUnsupported } // ENABLE is invalid if a mailbox has been selected. if c.State() != imap.AuthenticatedState { return nil, ErrNotLoggedIn } cmd := &commands.Enable{Caps: caps} res := &responses.Enabled{} if status, err := c.Execute(cmd, res); err != nil { return nil, err } else { return res.Caps, status.Err() } } func (c *Client) idle(stop <-chan struct{}) error { cmd := &commands.Idle{} res := &responses.Idle{ Stop: stop, RepliesCh: make(chan []byte, 10), } if status, err := c.Execute(cmd, res); err != nil { return err } else { return status.Err() } } // IdleOptions holds options for Client.Idle. type IdleOptions struct { // LogoutTimeout is used to avoid being logged out by the server when // idling. Each LogoutTimeout, the IDLE command is restarted. If set to // zero, a default is used. If negative, this behavior is disabled. LogoutTimeout time.Duration // Poll interval when the server doesn't support IDLE. If zero, a default // is used. If negative, polling is always disabled. PollInterval time.Duration } // Idle indicates to the server that the client is ready to receive unsolicited // mailbox update messages. When the client wants to send commands again, it // must first close stop. // // If the server doesn't support IDLE, go-imap falls back to polling. func (c *Client) Idle(stop <-chan struct{}, opts *IdleOptions) error { if ok, err := c.Support("IDLE"); err != nil { return err } else if !ok { return c.idleFallback(stop, opts) } logoutTimeout := 25 * time.Minute if opts != nil { if opts.LogoutTimeout > 0 { logoutTimeout = opts.LogoutTimeout } else if opts.LogoutTimeout < 0 { return c.idle(stop) } } t := time.NewTicker(logoutTimeout) defer t.Stop() for { stopOrRestart := make(chan struct{}) done := make(chan error, 1) go func() { done <- c.idle(stopOrRestart) }() select { case <-t.C: close(stopOrRestart) if err := <-done; err != nil { return err } case <-stop: close(stopOrRestart) return <-done case err := <-done: close(stopOrRestart) if err != nil { return err } } } } func (c *Client) idleFallback(stop <-chan struct{}, opts *IdleOptions) error { pollInterval := time.Minute if opts != nil { if opts.PollInterval > 0 { pollInterval = opts.PollInterval } else if opts.PollInterval < 0 { return ErrExtensionUnsupported } } t := time.NewTicker(pollInterval) defer t.Stop() for { select { case <-t.C: if err := c.Noop(); err != nil { return err } case <-stop: return nil case <-c.LoggedOut(): return errors.New("disconnected while idling") } } } go-imap-1.2.0/client/cmd_auth_test.go000066400000000000000000000264241412725504300175060ustar00rootroot00000000000000package client import ( "bytes" "io" "reflect" "testing" "time" "github.com/emersion/go-imap" ) func TestClient_Select(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) var mbox *imap.MailboxStatus done := make(chan error, 1) go func() { var err error mbox, err = c.Select("INBOX", false) done <- err }() tag, cmd := s.ScanCmd() if cmd != "SELECT INBOX" { t.Fatalf("client sent command %v, want SELECT \"INBOX\"", cmd) } s.WriteString("* 172 EXISTS\r\n") s.WriteString("* 1 RECENT\r\n") s.WriteString("* OK [UNSEEN 12] Message 12 is first unseen\r\n") s.WriteString("* OK [UIDVALIDITY 3857529045] UIDs valid\r\n") s.WriteString("* OK [UIDNEXT 4392] Predicted next UID\r\n") s.WriteString("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n") s.WriteString("* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n") s.WriteString(tag + " OK SELECT completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Select() = %v", err) } want := &imap.MailboxStatus{ Name: "INBOX", ReadOnly: false, Flags: []string{imap.AnsweredFlag, imap.FlaggedFlag, imap.DeletedFlag, imap.SeenFlag, imap.DraftFlag}, PermanentFlags: []string{imap.DeletedFlag, imap.SeenFlag, "\\*"}, UnseenSeqNum: 12, Messages: 172, Recent: 1, UidNext: 4392, UidValidity: 3857529045, } mbox.Items = nil if !reflect.DeepEqual(mbox, want) { t.Errorf("c.Select() = \n%+v\n want \n%+v", mbox, want) } } func TestClient_Select_ReadOnly(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) var mbox *imap.MailboxStatus done := make(chan error, 1) go func() { var err error mbox, err = c.Select("INBOX", true) done <- err }() tag, cmd := s.ScanCmd() if cmd != "EXAMINE INBOX" { t.Fatalf("client sent command %v, want EXAMINE \"INBOX\"", cmd) } s.WriteString(tag + " OK [READ-ONLY] EXAMINE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Select() = %v", err) } if !mbox.ReadOnly { t.Errorf("c.Select().ReadOnly = false, want true") } } func TestClient_Create(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) done := make(chan error, 1) go func() { done <- c.Create("New Mailbox") }() tag, cmd := s.ScanCmd() if cmd != "CREATE \"New Mailbox\"" { t.Fatalf("client sent command %v, want %v", cmd, "CREATE \"New Mailbox\"") } s.WriteString(tag + " OK CREATE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Create() = %v", err) } } func TestClient_Delete(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) done := make(chan error, 1) go func() { done <- c.Delete("Old Mailbox") }() tag, cmd := s.ScanCmd() if cmd != "DELETE \"Old Mailbox\"" { t.Fatalf("client sent command %v, want %v", cmd, "DELETE \"Old Mailbox\"") } s.WriteString(tag + " OK DELETE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Delete() = %v", err) } } func TestClient_Rename(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) done := make(chan error, 1) go func() { done <- c.Rename("Old Mailbox", "New Mailbox") }() tag, cmd := s.ScanCmd() if cmd != "RENAME \"Old Mailbox\" \"New Mailbox\"" { t.Fatalf("client sent command %v, want %v", cmd, "RENAME \"Old Mailbox\" \"New Mailbox\"") } s.WriteString(tag + " OK RENAME completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Rename() = %v", err) } } func TestClient_Subscribe(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) done := make(chan error, 1) go func() { done <- c.Subscribe("Mailbox") }() tag, cmd := s.ScanCmd() if cmd != "SUBSCRIBE \"Mailbox\"" { t.Fatalf("client sent command %v, want %v", cmd, "SUBSCRIBE \"Mailbox\"") } s.WriteString(tag + " OK SUBSCRIBE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Subscribe() = %v", err) } } func TestClient_Unsubscribe(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) done := make(chan error, 1) go func() { done <- c.Unsubscribe("Mailbox") }() tag, cmd := s.ScanCmd() if cmd != "UNSUBSCRIBE \"Mailbox\"" { t.Fatalf("client sent command %v, want %v", cmd, "UNSUBSCRIBE \"Mailbox\"") } s.WriteString(tag + " OK UNSUBSCRIBE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Unsubscribe() = %v", err) } } func TestClient_List(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) done := make(chan error, 1) mailboxes := make(chan *imap.MailboxInfo, 3) go func() { done <- c.List("", "%", mailboxes) }() tag, cmd := s.ScanCmd() if cmd != "LIST \"\" \"%\"" { t.Fatalf("client sent command %v, want %v", cmd, "LIST \"\" \"%\"") } s.WriteString("* LIST (flag1) \"/\" INBOX\r\n") s.WriteString("* LIST (flag2 flag3) \"/\" Drafts\r\n") s.WriteString("* LIST () \"/\" Sent\r\n") s.WriteString(tag + " OK LIST completed\r\n") if err := <-done; err != nil { t.Fatalf("c.List() = %v", err) } want := []struct { name string attributes []string }{ {"INBOX", []string{"flag1"}}, {"Drafts", []string{"flag2", "flag3"}}, {"Sent", []string{}}, } i := 0 for mbox := range mailboxes { if mbox.Name != want[i].name { t.Errorf("Bad mailbox name for %v: %v, want %v", i, mbox.Name, want[i].name) } if !reflect.DeepEqual(mbox.Attributes, want[i].attributes) { t.Errorf("Bad mailbox attributes for %v: %v, want %v", i, mbox.Attributes, want[i].attributes) } i++ } } func TestClient_Lsub(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) done := make(chan error, 1) mailboxes := make(chan *imap.MailboxInfo, 1) go func() { done <- c.Lsub("", "%", mailboxes) }() tag, cmd := s.ScanCmd() if cmd != "LSUB \"\" \"%\"" { t.Fatalf("client sent command %v, want %v", cmd, "LSUB \"\" \"%\"") } s.WriteString("* LSUB () \"/\" INBOX\r\n") s.WriteString(tag + " OK LSUB completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Lsub() = %v", err) } mbox := <-mailboxes if mbox.Name != "INBOX" { t.Errorf("Bad mailbox name: %v", mbox.Name) } if len(mbox.Attributes) != 0 { t.Errorf("Bad mailbox flags: %v", mbox.Attributes) } } func TestClient_Status(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) done := make(chan error, 1) var mbox *imap.MailboxStatus go func() { var err error mbox, err = c.Status("INBOX", []imap.StatusItem{imap.StatusMessages, imap.StatusRecent}) done <- err }() tag, cmd := s.ScanCmd() if cmd != "STATUS INBOX (MESSAGES RECENT)" { t.Fatalf("client sent command %v, want %v", cmd, "STATUS \"INBOX\" (MESSAGES RECENT)") } s.WriteString("* STATUS INBOX (MESSAGES 42 RECENT 1)\r\n") s.WriteString(tag + " OK STATUS completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Status() = %v", err) } if mbox.Messages != 42 { t.Errorf("Bad mailbox messages: %v", mbox.Messages) } if mbox.Recent != 1 { t.Errorf("Bad mailbox recent: %v", mbox.Recent) } } type literalWrap struct { io.Reader L int } func (lw literalWrap) Len() int { return lw.L } func TestClient_Append_SmallerLiteral(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) msg := "Hello World!\r\nHello Gophers!\r\n" date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) flags := []string{imap.SeenFlag, imap.DraftFlag} r := bytes.NewBufferString(msg) done := make(chan error, 1) go func() { done <- c.Append("INBOX", flags, date, literalWrap{r, 35}) // The buffer is not flushed on error, force it so io.ReadFull can // continue. c.conn.Flush() }() tag, _ := s.ScanCmd() s.WriteString("+ send literal\r\n") b := make([]byte, 30) // The client will close connection. if _, err := io.ReadFull(s, b); err != io.EOF { t.Error("Expected EOF, got", err) } s.WriteString(tag + " OK APPEND completed\r\n") err, ok := (<-done).(imap.LiteralLengthErr) if !ok { t.Fatalf("c.Append() = %v", err) } if err.Expected != 35 { t.Fatalf("err.Expected = %v", err.Expected) } if err.Actual != 30 { t.Fatalf("err.Actual = %v", err.Actual) } } func TestClient_Append_BiggerLiteral(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) msg := "Hello World!\r\nHello Gophers!\r\n" date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) flags := []string{imap.SeenFlag, imap.DraftFlag} r := bytes.NewBufferString(msg) done := make(chan error, 1) go func() { done <- c.Append("INBOX", flags, date, literalWrap{r, 25}) // The buffer is not flushed on error, force it so io.ReadFull can // continue. c.conn.Flush() }() tag, _ := s.ScanCmd() s.WriteString("+ send literal\r\n") // The client will close connection. b := make([]byte, 25) if _, err := io.ReadFull(s, b); err != io.EOF { t.Error("Expected EOF, got", err) } s.WriteString(tag + " OK APPEND completed\r\n") err, ok := (<-done).(imap.LiteralLengthErr) if !ok { t.Fatalf("c.Append() = %v", err) } if err.Expected != 25 { t.Fatalf("err.Expected = %v", err.Expected) } if err.Actual != 30 { t.Fatalf("err.Actual = %v", err.Actual) } } func TestClient_Append(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) msg := "Hello World!\r\nHello Gophers!\r\n" date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) flags := []string{imap.SeenFlag, imap.DraftFlag} done := make(chan error, 1) go func() { done <- c.Append("INBOX", flags, date, bytes.NewBufferString(msg)) }() tag, cmd := s.ScanCmd() if cmd != "APPEND INBOX (\\Seen \\Draft) \"10-Nov-2009 23:00:00 +0000\" {30}" { t.Fatalf("client sent command %v, want %v", cmd, "APPEND \"INBOX\" (\\Seen \\Draft) \"10-Nov-2009 23:00:00 +0000\" {30}") } s.WriteString("+ send literal\r\n") b := make([]byte, 30) if _, err := io.ReadFull(s, b); err != nil { t.Fatal(err) } else if string(b) != msg { t.Fatal("Bad literal:", string(b)) } s.WriteString(tag + " OK APPEND completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Append() = %v", err) } } func TestClient_Append_failed(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) // First the server refuses msg := "First try" done := make(chan error, 1) go func() { done <- c.Append("INBOX", nil, time.Time{}, bytes.NewBufferString(msg)) }() tag, _ := s.ScanCmd() s.WriteString(tag + " BAD APPEND failed\r\n") if err := <-done; err == nil { t.Fatal("c.Append() = nil, want an error from the server") } // Try a second time, the server accepts msg = "Second try" go func() { done <- c.Append("INBOX", nil, time.Time{}, bytes.NewBufferString(msg)) }() tag, _ = s.ScanCmd() s.WriteString("+ send literal\r\n") b := make([]byte, len(msg)) if _, err := io.ReadFull(s, b); err != nil { t.Fatal(err) } else if string(b) != msg { t.Fatal("Bad literal:", string(b)) } s.WriteString(tag + " OK APPEND completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Append() = %v", err) } } go-imap-1.2.0/client/cmd_noauth.go000066400000000000000000000076421412725504300170050ustar00rootroot00000000000000package client import ( "crypto/tls" "errors" "net" "github.com/emersion/go-imap" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" "github.com/emersion/go-sasl" ) var ( // ErrAlreadyLoggedIn is returned if Login or Authenticate is called when the // client is already logged in. ErrAlreadyLoggedIn = errors.New("Already logged in") // ErrTLSAlreadyEnabled is returned if StartTLS is called when TLS is already // enabled. ErrTLSAlreadyEnabled = errors.New("TLS is already enabled") // ErrLoginDisabled is returned if Login or Authenticate is called when the // server has disabled authentication. Most of the time, calling enabling TLS // solves the problem. ErrLoginDisabled = errors.New("Login is disabled in current state") ) // SupportStartTLS checks if the server supports STARTTLS. func (c *Client) SupportStartTLS() (bool, error) { return c.Support("STARTTLS") } // StartTLS starts TLS negotiation. func (c *Client) StartTLS(tlsConfig *tls.Config) error { if c.isTLS { return ErrTLSAlreadyEnabled } if tlsConfig == nil { tlsConfig = new(tls.Config) } if tlsConfig.ServerName == "" { tlsConfig = tlsConfig.Clone() tlsConfig.ServerName = c.serverName } cmd := new(commands.StartTLS) err := c.Upgrade(func(conn net.Conn) (net.Conn, error) { // Flag connection as in upgrading c.upgrading = true if status, err := c.execute(cmd, nil); err != nil { return nil, err } else if err := status.Err(); err != nil { return nil, err } // Wait for reader to block. c.conn.WaitReady() tlsConn := tls.Client(conn, tlsConfig) if err := tlsConn.Handshake(); err != nil { return nil, err } // Capabilities change when TLS is enabled c.locker.Lock() c.caps = nil c.locker.Unlock() return tlsConn, nil }) if err != nil { return err } c.isTLS = true return nil } // SupportAuth checks if the server supports a given authentication mechanism. func (c *Client) SupportAuth(mech string) (bool, error) { return c.Support("AUTH=" + mech) } // Authenticate indicates a SASL authentication mechanism to the server. If the // server supports the requested authentication mechanism, it performs an // authentication protocol exchange to authenticate and identify the client. func (c *Client) Authenticate(auth sasl.Client) error { if c.State() != imap.NotAuthenticatedState { return ErrAlreadyLoggedIn } mech, ir, err := auth.Start() if err != nil { return err } cmd := &commands.Authenticate{ Mechanism: mech, } irOk, err := c.Support("SASL-IR") if err != nil { return err } if irOk { cmd.InitialResponse = ir } res := &responses.Authenticate{ Mechanism: auth, InitialResponse: ir, RepliesCh: make(chan []byte, 10), } if irOk { res.InitialResponse = nil } status, err := c.execute(cmd, res) if err != nil { return err } if err = status.Err(); err != nil { return err } c.locker.Lock() c.state = imap.AuthenticatedState c.caps = nil // Capabilities change when user is logged in c.locker.Unlock() if status.Code == "CAPABILITY" { c.gotStatusCaps(status.Arguments) } return nil } // Login identifies the client to the server and carries the plaintext password // authenticating this user. func (c *Client) Login(username, password string) error { if state := c.State(); state == imap.AuthenticatedState || state == imap.SelectedState { return ErrAlreadyLoggedIn } c.locker.Lock() loginDisabled := c.caps != nil && c.caps["LOGINDISABLED"] c.locker.Unlock() if loginDisabled { return ErrLoginDisabled } cmd := &commands.Login{ Username: username, Password: password, } status, err := c.execute(cmd, nil) if err != nil { return err } if err = status.Err(); err != nil { return err } c.locker.Lock() c.state = imap.AuthenticatedState c.caps = nil // Capabilities change when user is logged in c.locker.Unlock() if status.Code == "CAPABILITY" { c.gotStatusCaps(status.Arguments) } return nil } go-imap-1.2.0/client/cmd_noauth_test.go000066400000000000000000000167061412725504300200450ustar00rootroot00000000000000package client import ( "crypto/tls" "io" "testing" "github.com/emersion/go-imap" "github.com/emersion/go-imap/internal" "github.com/emersion/go-sasl" ) func TestClient_StartTLS(t *testing.T) { c, s := newTestClient(t) defer s.Close() cert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey) if err != nil { t.Fatal("cannot load test certificate:", err) } tlsConfig := &tls.Config{ InsecureSkipVerify: true, Certificates: []tls.Certificate{cert}, } if c.IsTLS() { t.Fatal("Client has TLS enabled before STARTTLS") } if ok, err := c.SupportStartTLS(); err != nil { t.Fatalf("c.SupportStartTLS() = %v", err) } else if !ok { t.Fatalf("c.SupportStartTLS() = %v, want true", ok) } done := make(chan error, 1) go func() { done <- c.StartTLS(tlsConfig) }() tag, cmd := s.ScanCmd() if cmd != "STARTTLS" { t.Fatalf("client sent command %v, want STARTTLS", cmd) } s.WriteString(tag + " OK Begin TLS negotiation now\r\n") ss := tls.Server(s.Conn, tlsConfig) if err := ss.Handshake(); err != nil { t.Fatal("cannot perform TLS handshake:", err) } if err := <-done; err != nil { t.Error("c.StartTLS() =", err) } if !c.IsTLS() { t.Errorf("Client has not TLS enabled after STARTTLS") } go func() { _, err := c.Capability() done <- err }() tag, cmd = newCmdScanner(ss).ScanCmd() if cmd != "CAPABILITY" { t.Fatalf("client sent command %v, want CAPABILITY", cmd) } io.WriteString(ss, "* CAPABILITY IMAP4rev1 AUTH=PLAIN\r\n") io.WriteString(ss, tag+" OK CAPABILITY completed.\r\n") } func TestClient_Authenticate(t *testing.T) { c, s := newTestClient(t) defer s.Close() if ok, err := c.SupportAuth(sasl.Plain); err != nil { t.Fatalf("c.SupportAuth(sasl.Plain) = %v", err) } else if !ok { t.Fatalf("c.SupportAuth(sasl.Plain) = %v, want true", ok) } sasl := sasl.NewPlainClient("", "username", "password") done := make(chan error, 1) go func() { done <- c.Authenticate(sasl) }() tag, cmd := s.ScanCmd() if cmd != "AUTHENTICATE PLAIN" { t.Fatalf("client sent command %v, want AUTHENTICATE PLAIN", cmd) } s.WriteString("+ \r\n") wantLine := "AHVzZXJuYW1lAHBhc3N3b3Jk" if line := s.ScanLine(); line != wantLine { t.Fatalf("client sent auth %v, want %v", line, wantLine) } s.WriteString(tag + " OK AUTHENTICATE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Authenticate() = %v", err) } if state := c.State(); state != imap.AuthenticatedState { t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState) } } func TestClient_Authenticate_InitialResponse(t *testing.T) { c, s := newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 SASL-IR STARTTLS AUTH=PLAIN] Server ready.\r\n") defer s.Close() if ok, err := c.SupportAuth(sasl.Plain); err != nil { t.Fatalf("c.SupportAuth(sasl.Plain) = %v", err) } else if !ok { t.Fatalf("c.SupportAuth(sasl.Plain) = %v, want true", ok) } sasl := sasl.NewPlainClient("", "username", "password") done := make(chan error, 1) go func() { done <- c.Authenticate(sasl) }() tag, cmd := s.ScanCmd() if cmd != "AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk" { t.Fatalf("client sent command %v, want AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk", cmd) } s.WriteString(tag + " OK AUTHENTICATE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Authenticate() = %v", err) } if state := c.State(); state != imap.AuthenticatedState { t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState) } } func TestClient_Login_Success(t *testing.T) { c, s := newTestClient(t) defer s.Close() done := make(chan error, 1) go func() { done <- c.Login("username", "password") }() tag, cmd := s.ScanCmd() if cmd != "LOGIN \"username\" \"password\"" { t.Fatalf("client sent command %v, want LOGIN username password", cmd) } s.WriteString(tag + " OK LOGIN completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Login() = %v", err) } if state := c.State(); state != imap.AuthenticatedState { t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState) } } func TestClient_Login_8bitSync(t *testing.T) { c, s := newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 SASL-IR STARTTLS AUTH=PLAIN] Server ready.\r\n") defer s.Close() // Use of UTF-8 will force go-imap to send password in literal. done := make(chan error, 1) go func() { done <- c.Login("username", "пароль") }() tag, cmd := s.ScanCmd() if cmd != "LOGIN \"username\" {12}" { t.Fatalf("client sent command %v, want LOGIN \"username\" {12}", cmd) } s.WriteString("+ send literal\r\n") pass := s.ScanLine() if pass != "пароль" { t.Fatalf("client sent %v, want {12}'пароль' literal", pass) } s.WriteString(tag + " OK LOGIN completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Login() = %v", err) } if state := c.State(); state != imap.AuthenticatedState { t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState) } } func TestClient_Login_8bitNonSync(t *testing.T) { c, s := newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 LITERAL- SASL-IR STARTTLS AUTH=PLAIN] Server ready.\r\n") defer s.Close() // Use of UTF-8 will force go-imap to send password in literal. done := make(chan error, 1) go func() { done <- c.Login("username", "пароль") }() tag, cmd := s.ScanCmd() if cmd != "LOGIN \"username\" {12+}" { t.Fatalf("client sent command %v, want LOGIN \"username\" {12+}", cmd) } pass := s.ScanLine() if pass != "пароль" { t.Fatalf("client sent %v, want {12+}'пароль' literal", pass) } s.WriteString(tag + " OK LOGIN completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Login() = %v", err) } if state := c.State(); state != imap.AuthenticatedState { t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState) } } func TestClient_Login_Error(t *testing.T) { c, s := newTestClient(t) defer s.Close() done := make(chan error, 1) go func() { done <- c.Login("username", "password") }() tag, cmd := s.ScanCmd() if cmd != "LOGIN \"username\" \"password\"" { t.Fatalf("client sent command %v, want LOGIN username password", cmd) } s.WriteString(tag + " NO LOGIN incorrect\r\n") if err := <-done; err == nil { t.Fatal("c.Login() = nil, want LOGIN incorrect") } if state := c.State(); state != imap.NotAuthenticatedState { t.Errorf("c.State() = %v, want %v", state, imap.NotAuthenticatedState) } } func TestClient_Login_State_Allowed(t *testing.T) { c, s := newTestClient(t) defer s.Close() done := make(chan error, 1) go func() { done <- c.Login("username", "password") }() tag, cmd := s.ScanCmd() if cmd != "LOGIN \"username\" \"password\"" { t.Fatalf("client sent command %v, want LOGIN \"username\" \"password\"", cmd) } s.WriteString(tag + " OK LOGIN completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Login() = %v", err) } if state := c.State(); state != imap.AuthenticatedState { t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState) } go func() { done <- c.Login("username", "password") }() if err := <-done; err != ErrAlreadyLoggedIn { t.Fatalf("c.Login() = %v, want %v", err, ErrAlreadyLoggedIn) } go func() { done <- c.Logout() }() s.ScanCmd() s.WriteString("* BYE Client asked to close the connection.\r\n") s.WriteString(tag + " OK LOGOUT completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Logout() = %v", err) } if err := c.Login("username", "password"); err == ErrAlreadyLoggedIn { t.Errorf("Client is logout, login must not give %v", ErrAlreadyLoggedIn) } } go-imap-1.2.0/client/cmd_selected.go000066400000000000000000000233541412725504300172750ustar00rootroot00000000000000package client import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" ) var ( // ErrNoMailboxSelected is returned if a command that requires a mailbox to be // selected is called when there isn't. ErrNoMailboxSelected = errors.New("No mailbox selected") // ErrExtensionUnsupported is returned if a command uses a extension that // is not supported by the server. ErrExtensionUnsupported = errors.New("The required extension is not supported by the server") ) // Check requests a checkpoint of the currently selected mailbox. A checkpoint // refers to any implementation-dependent housekeeping associated with the // mailbox that is not normally executed as part of each command. func (c *Client) Check() error { if c.State() != imap.SelectedState { return ErrNoMailboxSelected } cmd := new(commands.Check) status, err := c.execute(cmd, nil) if err != nil { return err } return status.Err() } // Close permanently removes all messages that have the \Deleted flag set from // the currently selected mailbox, and returns to the authenticated state from // the selected state. func (c *Client) Close() error { if c.State() != imap.SelectedState { return ErrNoMailboxSelected } cmd := new(commands.Close) status, err := c.execute(cmd, nil) if err != nil { return err } else if err := status.Err(); err != nil { return err } c.locker.Lock() c.state = imap.AuthenticatedState c.mailbox = nil c.locker.Unlock() return nil } // Terminate closes the tcp connection func (c *Client) Terminate() error { return c.conn.Close() } // Expunge permanently removes all messages that have the \Deleted flag set from // the currently selected mailbox. If ch is not nil, sends sequence IDs of each // deleted message to this channel. func (c *Client) Expunge(ch chan uint32) error { if ch != nil { defer close(ch) } if c.State() != imap.SelectedState { return ErrNoMailboxSelected } cmd := new(commands.Expunge) var h responses.Handler if ch != nil { h = &responses.Expunge{SeqNums: ch} } status, err := c.execute(cmd, h) if err != nil { return err } return status.Err() } func (c *Client) executeSearch(uid bool, criteria *imap.SearchCriteria, charset string) (ids []uint32, status *imap.StatusResp, err error) { if c.State() != imap.SelectedState { err = ErrNoMailboxSelected return } var cmd imap.Commander = &commands.Search{ Charset: charset, Criteria: criteria, } if uid { cmd = &commands.Uid{Cmd: cmd} } res := new(responses.Search) status, err = c.execute(cmd, res) if err != nil { return } err, ids = status.Err(), res.Ids return } func (c *Client) search(uid bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { ids, status, err := c.executeSearch(uid, criteria, "UTF-8") if status != nil && status.Code == imap.CodeBadCharset { // Some servers don't support UTF-8 ids, _, err = c.executeSearch(uid, criteria, "US-ASCII") } return } // Search searches the mailbox for messages that match the given searching // criteria. Searching criteria consist of one or more search keys. The response // contains a list of message sequence IDs corresponding to those messages that // match the searching criteria. When multiple keys are specified, the result is // the intersection (AND function) of all the messages that match those keys. // Criteria must be UTF-8 encoded. See RFC 3501 section 6.4.4 for a list of // searching criteria. When no criteria has been set, all messages in the mailbox // will be searched using ALL criteria. func (c *Client) Search(criteria *imap.SearchCriteria) (seqNums []uint32, err error) { return c.search(false, criteria) } // UidSearch is identical to Search, but UIDs are returned instead of message // sequence numbers. func (c *Client) UidSearch(criteria *imap.SearchCriteria) (uids []uint32, err error) { return c.search(true, criteria) } func (c *Client) fetch(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { defer close(ch) if c.State() != imap.SelectedState { return ErrNoMailboxSelected } var cmd imap.Commander = &commands.Fetch{ SeqSet: seqset, Items: items, } if uid { cmd = &commands.Uid{Cmd: cmd} } res := &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid} status, err := c.execute(cmd, res) if err != nil { return err } return status.Err() } // Fetch retrieves data associated with a message in the mailbox. See RFC 3501 // section 6.4.5 for a list of items that can be requested. func (c *Client) Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { return c.fetch(false, seqset, items, ch) } // UidFetch is identical to Fetch, but seqset is interpreted as containing // unique identifiers instead of message sequence numbers. func (c *Client) UidFetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { return c.fetch(true, seqset, items, ch) } func (c *Client) store(uid bool, seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { if ch != nil { defer close(ch) } if c.State() != imap.SelectedState { return ErrNoMailboxSelected } // TODO: this could break extensions (this only works when item is FLAGS) if fields, ok := value.([]interface{}); ok { for i, field := range fields { if s, ok := field.(string); ok { fields[i] = imap.RawString(s) } } } // If ch is nil, the updated values are data which will be lost, so don't // retrieve it. if ch == nil { op, _, err := imap.ParseFlagsOp(item) if err == nil { item = imap.FormatFlagsOp(op, true) } } var cmd imap.Commander = &commands.Store{ SeqSet: seqset, Item: item, Value: value, } if uid { cmd = &commands.Uid{Cmd: cmd} } var h responses.Handler if ch != nil { h = &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid} } status, err := c.execute(cmd, h) if err != nil { return err } return status.Err() } // Store alters data associated with a message in the mailbox. If ch is not nil, // the updated value of the data will be sent to this channel. See RFC 3501 // section 6.4.6 for a list of items that can be updated. func (c *Client) Store(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { return c.store(false, seqset, item, value, ch) } // UidStore is identical to Store, but seqset is interpreted as containing // unique identifiers instead of message sequence numbers. func (c *Client) UidStore(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { return c.store(true, seqset, item, value, ch) } func (c *Client) copy(uid bool, seqset *imap.SeqSet, dest string) error { if c.State() != imap.SelectedState { return ErrNoMailboxSelected } var cmd imap.Commander = &commands.Copy{ SeqSet: seqset, Mailbox: dest, } if uid { cmd = &commands.Uid{Cmd: cmd} } status, err := c.execute(cmd, nil) if err != nil { return err } return status.Err() } // Copy copies the specified message(s) to the end of the specified destination // mailbox. func (c *Client) Copy(seqset *imap.SeqSet, dest string) error { return c.copy(false, seqset, dest) } // UidCopy is identical to Copy, but seqset is interpreted as containing unique // identifiers instead of message sequence numbers. func (c *Client) UidCopy(seqset *imap.SeqSet, dest string) error { return c.copy(true, seqset, dest) } func (c *Client) move(uid bool, seqset *imap.SeqSet, dest string) error { if c.State() != imap.SelectedState { return ErrNoMailboxSelected } if ok, err := c.Support("MOVE"); err != nil { return err } else if !ok { return c.moveFallback(uid, seqset, dest) } var cmd imap.Commander = &commands.Move{ SeqSet: seqset, Mailbox: dest, } if uid { cmd = &commands.Uid{Cmd: cmd} } if status, err := c.Execute(cmd, nil); err != nil { return err } else { return status.Err() } } // moveFallback uses COPY, STORE and EXPUNGE for servers which don't support // MOVE. func (c *Client) moveFallback(uid bool, seqset *imap.SeqSet, dest string) error { item := imap.FormatFlagsOp(imap.AddFlags, true) flags := []interface{}{imap.DeletedFlag} if uid { if err := c.UidCopy(seqset, dest); err != nil { return err } if err := c.UidStore(seqset, item, flags, nil); err != nil { return err } } else { if err := c.Copy(seqset, dest); err != nil { return err } if err := c.Store(seqset, item, flags, nil); err != nil { return err } } return c.Expunge(nil) } // Move moves the specified message(s) to the end of the specified destination // mailbox. // // If the server doesn't support the MOVE extension defined in RFC 6851, // go-imap will fallback to copy, store and expunge. func (c *Client) Move(seqset *imap.SeqSet, dest string) error { return c.move(false, seqset, dest) } // UidMove is identical to Move, but seqset is interpreted as containing unique // identifiers instead of message sequence numbers. func (c *Client) UidMove(seqset *imap.SeqSet, dest string) error { return c.move(true, seqset, dest) } // Unselect frees server's resources associated with the selected mailbox and // returns the server to the authenticated state. This command performs the same // actions as Close, except that no messages are permanently removed from the // currently selected mailbox. // // If client does not support the UNSELECT extension, ErrExtensionUnsupported // is returned. func (c *Client) Unselect() error { if ok, err := c.Support("UNSELECT"); !ok || err != nil { return ErrExtensionUnsupported } if c.State() != imap.SelectedState { return ErrNoMailboxSelected } cmd := &commands.Unselect{} if status, err := c.Execute(cmd, nil); err != nil { return err } else if err := status.Err(); err != nil { return err } c.SetState(imap.AuthenticatedState, nil) return nil } go-imap-1.2.0/client/cmd_selected_test.go000066400000000000000000000357731412725504300203440ustar00rootroot00000000000000package client import ( "io/ioutil" "net/textproto" "reflect" "testing" "time" "github.com/emersion/go-imap" ) func TestClient_Check(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) done := make(chan error, 1) go func() { done <- c.Check() }() tag, cmd := s.ScanCmd() if cmd != "CHECK" { t.Fatalf("client sent command %v, want %v", cmd, "CHECK") } s.WriteString(tag + " OK CHECK completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Check() = %v", err) } } func TestClient_Close(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, &imap.MailboxStatus{Name: "INBOX"}) done := make(chan error, 1) go func() { done <- c.Close() }() tag, cmd := s.ScanCmd() if cmd != "CLOSE" { t.Fatalf("client sent command %v, want %v", cmd, "CLOSE") } s.WriteString(tag + " OK CLOSE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Check() = %v", err) } if state := c.State(); state != imap.AuthenticatedState { t.Errorf("Bad state: %v", state) } if mailbox := c.Mailbox(); mailbox != nil { t.Errorf("Client selected mailbox is not nil: %v", mailbox) } } func TestClient_Expunge(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) done := make(chan error, 1) expunged := make(chan uint32, 4) go func() { done <- c.Expunge(expunged) }() tag, cmd := s.ScanCmd() if cmd != "EXPUNGE" { t.Fatalf("client sent command %v, want %v", cmd, "EXPUNGE") } s.WriteString("* 3 EXPUNGE\r\n") s.WriteString("* 3 EXPUNGE\r\n") s.WriteString("* 5 EXPUNGE\r\n") s.WriteString("* 8 EXPUNGE\r\n") s.WriteString(tag + " OK EXPUNGE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Expunge() = %v", err) } expected := []uint32{3, 3, 5, 8} i := 0 for id := range expunged { if id != expected[i] { t.Errorf("Bad expunged sequence number: got %v instead of %v", id, expected[i]) } i++ } } func TestClient_Search(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) date, _ := time.Parse(imap.DateLayout, "1-Feb-1994") criteria := &imap.SearchCriteria{ WithFlags: []string{imap.DeletedFlag}, Header: textproto.MIMEHeader{"From": {"Smith"}}, Since: date, Not: []*imap.SearchCriteria{{ Header: textproto.MIMEHeader{"To": {"Pauline"}}, }}, } done := make(chan error, 1) var results []uint32 go func() { var err error results, err = c.Search(criteria) done <- err }() wantCmd := `SEARCH CHARSET UTF-8 SINCE "1-Feb-1994" FROM "Smith" DELETED NOT (TO "Pauline")` tag, cmd := s.ScanCmd() if cmd != wantCmd { t.Fatalf("client sent command %v, want %v", cmd, wantCmd) } s.WriteString("* SEARCH 2 84 882\r\n") s.WriteString(tag + " OK SEARCH completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Search() = %v", err) } want := []uint32{2, 84, 882} if !reflect.DeepEqual(results, want) { t.Errorf("c.Search() = %v, want %v", results, want) } } func TestClient_Search_Uid(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) criteria := &imap.SearchCriteria{ WithoutFlags: []string{imap.DeletedFlag}, } done := make(chan error, 1) var results []uint32 go func() { var err error results, err = c.UidSearch(criteria) done <- err }() wantCmd := "UID SEARCH CHARSET UTF-8 UNDELETED" tag, cmd := s.ScanCmd() if cmd != wantCmd { t.Fatalf("client sent command %v, want %v", cmd, wantCmd) } s.WriteString("* SEARCH 1 78 2010\r\n") s.WriteString(tag + " OK UID SEARCH completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Search() = %v", err) } want := []uint32{1, 78, 2010} if !reflect.DeepEqual(results, want) { t.Errorf("c.Search() = %v, want %v", results, want) } } func TestClient_Fetch(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("2:3") fields := []imap.FetchItem{imap.FetchUid, imap.FetchItem("BODY[]")} done := make(chan error, 1) messages := make(chan *imap.Message, 2) go func() { done <- c.Fetch(seqset, fields, messages) }() tag, cmd := s.ScanCmd() if cmd != "FETCH 2:3 (UID BODY[])" { t.Fatalf("client sent command %v, want %v", cmd, "FETCH 2:3 (UID BODY[])") } s.WriteString("* 2 FETCH (UID 42 BODY[] {16}\r\n") s.WriteString("I love potatoes.") s.WriteString(")\r\n") s.WriteString("* 3 FETCH (UID 28 BODY[] {12}\r\n") s.WriteString("Hello World!") s.WriteString(")\r\n") s.WriteString(tag + " OK FETCH completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Fetch() = %v", err) } section, _ := imap.ParseBodySectionName("BODY[]") msg := <-messages if msg.SeqNum != 2 { t.Errorf("First message has bad sequence number: %v", msg.SeqNum) } if msg.Uid != 42 { t.Errorf("First message has bad UID: %v", msg.Uid) } if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "I love potatoes." { t.Errorf("First message has bad body: %q", body) } msg = <-messages if msg.SeqNum != 3 { t.Errorf("First message has bad sequence number: %v", msg.SeqNum) } if msg.Uid != 28 { t.Errorf("Second message has bad UID: %v", msg.Uid) } if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "Hello World!" { t.Errorf("Second message has bad body: %q", body) } } func TestClient_Fetch_ClosedState(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.AuthenticatedState, nil) seqset, _ := imap.ParseSeqSet("2:3") fields := []imap.FetchItem{imap.FetchUid, imap.FetchItem("BODY[]")} done := make(chan error, 1) messages := make(chan *imap.Message, 2) go func() { done <- c.Fetch(seqset, fields, messages) }() _, more := <-messages if more { t.Fatalf("Messages channel has more messages, but it must be closed with no messages sent") } err := <-done if err != ErrNoMailboxSelected { t.Fatalf("Expected error to be IMAP Client ErrNoMailboxSelected") } } func TestClient_Fetch_Partial(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("1") fields := []imap.FetchItem{imap.FetchItem("BODY.PEEK[]<0.10>")} done := make(chan error, 1) messages := make(chan *imap.Message, 1) go func() { done <- c.Fetch(seqset, fields, messages) }() tag, cmd := s.ScanCmd() if cmd != "FETCH 1 (BODY.PEEK[]<0.10>)" { t.Fatalf("client sent command %v, want %v", cmd, "FETCH 1 (BODY.PEEK[]<0.10>)") } s.WriteString("* 1 FETCH (BODY[]<0> {10}\r\n") s.WriteString("I love pot") s.WriteString(")\r\n") s.WriteString(tag + " OK FETCH completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Fetch() = %v", err) } section, _ := imap.ParseBodySectionName("BODY.PEEK[]<0.10>") msg := <-messages if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "I love pot" { t.Errorf("Message has bad body: %q", body) } } func TestClient_Fetch_part(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("1") fields := []imap.FetchItem{imap.FetchItem("BODY.PEEK[1]")} done := make(chan error, 1) messages := make(chan *imap.Message, 1) go func() { done <- c.Fetch(seqset, fields, messages) }() tag, cmd := s.ScanCmd() if cmd != "FETCH 1 (BODY.PEEK[1])" { t.Fatalf("client sent command %v, want %v", cmd, "FETCH 1 (BODY.PEEK[1])") } s.WriteString("* 1 FETCH (BODY[1] {3}\r\n") s.WriteString("Hey") s.WriteString(")\r\n") s.WriteString(tag + " OK FETCH completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Fetch() = %v", err) } <-messages } func TestClient_Fetch_Uid(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("1:867") fields := []imap.FetchItem{imap.FetchFlags} done := make(chan error, 1) messages := make(chan *imap.Message, 1) go func() { done <- c.UidFetch(seqset, fields, messages) }() tag, cmd := s.ScanCmd() if cmd != "UID FETCH 1:867 (FLAGS)" { t.Fatalf("client sent command %v, want %v", cmd, "UID FETCH 1:867 (FLAGS)") } s.WriteString("* 23 FETCH (UID 42 FLAGS (\\Seen))\r\n") s.WriteString(tag + " OK UID FETCH completed\r\n") if err := <-done; err != nil { t.Fatalf("c.UidFetch() = %v", err) } msg := <-messages if msg.SeqNum != 23 { t.Errorf("First message has bad sequence number: %v", msg.SeqNum) } if msg.Uid != 42 { t.Errorf("Message has bad UID: %v", msg.Uid) } if len(msg.Flags) != 1 || msg.Flags[0] != "\\Seen" { t.Errorf("Message has bad flags: %v", msg.Flags) } } func TestClient_Fetch_Unilateral(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("1:4") fields := []imap.FetchItem{imap.FetchFlags} done := make(chan error, 1) messages := make(chan *imap.Message, 3) go func() { done <- c.Fetch(seqset, fields, messages) }() tag, cmd := s.ScanCmd() if cmd != "FETCH 1:4 (FLAGS)" { t.Fatalf("client sent command %v, want %v", cmd, "FETCH 1:4 (FLAGS)") } s.WriteString("* 2 FETCH (FLAGS (\\Seen))\r\n") s.WriteString("* 123 FETCH (FLAGS (\\Deleted))\r\n") s.WriteString("* 4 FETCH (FLAGS (\\Seen))\r\n") s.WriteString(tag + " OK FETCH completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Fetch() = %v", err) } msg := <-messages if msg.SeqNum != 2 { t.Errorf("First message has bad sequence number: %v", msg.SeqNum) } msg = <-messages if msg.SeqNum != 4 { t.Errorf("Second message has bad sequence number: %v", msg.SeqNum) } _, ok := <-messages if ok { t.Errorf("More than two messages") } } func TestClient_Fetch_Unilateral_Uid(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("1:4") fields := []imap.FetchItem{imap.FetchFlags} done := make(chan error, 1) messages := make(chan *imap.Message, 3) go func() { done <- c.UidFetch(seqset, fields, messages) }() tag, cmd := s.ScanCmd() if cmd != "UID FETCH 1:4 (FLAGS)" { t.Fatalf("client sent command %v, want %v", cmd, "UID FETCH 1:4 (FLAGS)") } s.WriteString("* 23 FETCH (UID 2 FLAGS (\\Seen))\r\n") s.WriteString("* 123 FETCH (FLAGS (\\Deleted))\r\n") s.WriteString("* 49 FETCH (UID 4 FLAGS (\\Seen))\r\n") s.WriteString(tag + " OK FETCH completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Fetch() = %v", err) } msg := <-messages if msg.Uid != 2 { t.Errorf("First message has bad UID: %v", msg.Uid) } msg = <-messages if msg.Uid != 4 { t.Errorf("Second message has bad UID: %v", msg.Uid) } _, ok := <-messages if ok { t.Errorf("More than two messages") } } func TestClient_Fetch_Uid_Dynamic(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("4:*") fields := []imap.FetchItem{imap.FetchFlags} done := make(chan error, 1) messages := make(chan *imap.Message, 1) go func() { done <- c.UidFetch(seqset, fields, messages) }() tag, cmd := s.ScanCmd() if cmd != "UID FETCH 4:* (FLAGS)" { t.Fatalf("client sent command %v, want %v", cmd, "UID FETCH 4:* (FLAGS)") } s.WriteString("* 23 FETCH (UID 2 FLAGS (\\Seen))\r\n") s.WriteString(tag + " OK FETCH completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Fetch() = %v", err) } msg, ok := <-messages if !ok { t.Errorf("No message supplied") } else if msg.Uid != 2 { t.Errorf("First message has bad UID: %v", msg.Uid) } } func TestClient_Store(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("2") done := make(chan error, 1) updates := make(chan *imap.Message, 1) go func() { done <- c.Store(seqset, imap.AddFlags, []interface{}{imap.SeenFlag, "foobar"}, updates) }() tag, cmd := s.ScanCmd() if cmd != "STORE 2 +FLAGS (\\Seen foobar)" { t.Fatalf("client sent command %v, want %v", cmd, "STORE 2 +FLAGS (\\Seen foobar)") } s.WriteString("* 2 FETCH (FLAGS (\\Seen foobar))\r\n") s.WriteString(tag + " OK STORE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Store() = %v", err) } msg := <-updates if len(msg.Flags) != 2 || msg.Flags[0] != "\\Seen" || msg.Flags[1] != "foobar" { t.Errorf("Bad message flags: %v", msg.Flags) } } func TestClient_Store_Silent(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("2:3") done := make(chan error, 1) go func() { done <- c.Store(seqset, imap.AddFlags, []interface{}{imap.SeenFlag, "foobar"}, nil) }() tag, cmd := s.ScanCmd() if cmd != "STORE 2:3 +FLAGS.SILENT (\\Seen foobar)" { t.Fatalf("client sent command %v, want %v", cmd, "STORE 2:3 +FLAGS.SILENT (\\Seen foobar)") } s.WriteString(tag + " OK STORE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Store() = %v", err) } } func TestClient_Store_Uid(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("27:901") done := make(chan error, 1) go func() { done <- c.UidStore(seqset, imap.AddFlags, []interface{}{imap.DeletedFlag, "foobar"}, nil) }() tag, cmd := s.ScanCmd() if cmd != "UID STORE 27:901 +FLAGS.SILENT (\\Deleted foobar)" { t.Fatalf("client sent command %v, want %v", cmd, "UID STORE 27:901 +FLAGS.SILENT (\\Deleted foobar)") } s.WriteString(tag + " OK STORE completed\r\n") if err := <-done; err != nil { t.Fatalf("c.UidStore() = %v", err) } } func TestClient_Copy(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("2:4") done := make(chan error, 1) go func() { done <- c.Copy(seqset, "Sent") }() tag, cmd := s.ScanCmd() if cmd != "COPY 2:4 \"Sent\"" { t.Fatalf("client sent command %v, want %v", cmd, "COPY 2:4 \"Sent\"") } s.WriteString(tag + " OK COPY completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Copy() = %v", err) } } func TestClient_Copy_Uid(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) seqset, _ := imap.ParseSeqSet("78:102") done := make(chan error, 1) go func() { done <- c.UidCopy(seqset, "Drafts") }() tag, cmd := s.ScanCmd() if cmd != "UID COPY 78:102 \"Drafts\"" { t.Fatalf("client sent command %v, want %v", cmd, "UID COPY 78:102 \"Drafts\"") } s.WriteString(tag + " OK UID COPY completed\r\n") if err := <-done; err != nil { t.Fatalf("c.UidCopy() = %v", err) } } func TestClient_Unselect(t *testing.T) { c, s := newTestClient(t) defer s.Close() setClientState(c, imap.SelectedState, nil) done := make(chan error, 1) go func() { done <- c.Unselect() }() tag, cmd := s.ScanCmd() if cmd != "UNSELECT" { t.Fatalf("client sent command %v, want %v", cmd, "UNSELECT") } s.WriteString(tag + " OK UNSELECT completed\r\n") if err := <-done; err != nil { t.Fatalf("c.Unselect() = %v", err) } if c.State() != imap.AuthenticatedState { t.Fatal("Client is not Authenticated after UNSELECT") } } go-imap-1.2.0/client/example_test.go000066400000000000000000000144711412725504300173540ustar00rootroot00000000000000package client_test import ( "bytes" "crypto/tls" "io/ioutil" "log" "net/mail" "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/client" ) func ExampleClient() { log.Println("Connecting to server...") // Connect to server c, err := client.DialTLS("mail.example.org:993", nil) if err != nil { log.Fatal(err) } log.Println("Connected") // Don't forget to logout defer c.Logout() // Login if err := c.Login("username", "password"); err != nil { log.Fatal(err) } log.Println("Logged in") // List mailboxes mailboxes := make(chan *imap.MailboxInfo, 10) done := make(chan error, 1) go func() { done <- c.List("", "*", mailboxes) }() log.Println("Mailboxes:") for m := range mailboxes { log.Println("* " + m.Name) } if err := <-done; err != nil { log.Fatal(err) } // Select INBOX mbox, err := c.Select("INBOX", false) if err != nil { log.Fatal(err) } log.Println("Flags for INBOX:", mbox.Flags) // Get the last 4 messages from := uint32(1) to := mbox.Messages if mbox.Messages > 3 { // We're using unsigned integers here, only substract if the result is > 0 from = mbox.Messages - 3 } seqset := new(imap.SeqSet) seqset.AddRange(from, to) items := []imap.FetchItem{imap.FetchEnvelope} messages := make(chan *imap.Message, 10) done = make(chan error, 1) go func() { done <- c.Fetch(seqset, items, messages) }() log.Println("Last 4 messages:") for msg := range messages { log.Println("* " + msg.Envelope.Subject) } if err := <-done; err != nil { log.Fatal(err) } log.Println("Done!") } func ExampleClient_Fetch() { // Let's assume c is a client var c *client.Client // Select INBOX mbox, err := c.Select("INBOX", false) if err != nil { log.Fatal(err) } // Get the last message if mbox.Messages == 0 { log.Fatal("No message in mailbox") } seqset := new(imap.SeqSet) seqset.AddRange(mbox.Messages, mbox.Messages) // Get the whole message body section := &imap.BodySectionName{} items := []imap.FetchItem{section.FetchItem()} messages := make(chan *imap.Message, 1) done := make(chan error, 1) go func() { done <- c.Fetch(seqset, items, messages) }() log.Println("Last message:") msg := <-messages r := msg.GetBody(section) if r == nil { log.Fatal("Server didn't returned message body") } if err := <-done; err != nil { log.Fatal(err) } m, err := mail.ReadMessage(r) if err != nil { log.Fatal(err) } header := m.Header log.Println("Date:", header.Get("Date")) log.Println("From:", header.Get("From")) log.Println("To:", header.Get("To")) log.Println("Subject:", header.Get("Subject")) body, err := ioutil.ReadAll(m.Body) if err != nil { log.Fatal(err) } log.Println(body) } func ExampleClient_Append() { // Let's assume c is a client var c *client.Client // Write the message to a buffer var b bytes.Buffer b.WriteString("From: \r\n") b.WriteString("To: \r\n") b.WriteString("Subject: Hey there\r\n") b.WriteString("\r\n") b.WriteString("Hey <3") // Append it to INBOX, with two flags flags := []string{imap.FlaggedFlag, "foobar"} if err := c.Append("INBOX", flags, time.Now(), &b); err != nil { log.Fatal(err) } } func ExampleClient_Expunge() { // Let's assume c is a client var c *client.Client // Select INBOX mbox, err := c.Select("INBOX", false) if err != nil { log.Fatal(err) } // We will delete the last message if mbox.Messages == 0 { log.Fatal("No message in mailbox") } seqset := new(imap.SeqSet) seqset.AddNum(mbox.Messages) // First mark the message as deleted item := imap.FormatFlagsOp(imap.AddFlags, true) flags := []interface{}{imap.DeletedFlag} if err := c.Store(seqset, item, flags, nil); err != nil { log.Fatal(err) } // Then delete it if err := c.Expunge(nil); err != nil { log.Fatal(err) } log.Println("Last message has been deleted") } func ExampleClient_StartTLS() { log.Println("Connecting to server...") // Connect to server c, err := client.Dial("mail.example.org:143") if err != nil { log.Fatal(err) } log.Println("Connected") // Don't forget to logout defer c.Logout() // Start a TLS session tlsConfig := &tls.Config{ServerName: "mail.example.org"} if err := c.StartTLS(tlsConfig); err != nil { log.Fatal(err) } log.Println("TLS started") // Now we can login if err := c.Login("username", "password"); err != nil { log.Fatal(err) } log.Println("Logged in") } func ExampleClient_Store() { // Let's assume c is a client var c *client.Client // Select INBOX _, err := c.Select("INBOX", false) if err != nil { log.Fatal(err) } // Mark message 42 as seen seqSet := new(imap.SeqSet) seqSet.AddNum(42) item := imap.FormatFlagsOp(imap.AddFlags, true) flags := []interface{}{imap.SeenFlag} err = c.Store(seqSet, item, flags, nil) if err != nil { log.Fatal(err) } log.Println("Message has been marked as seen") } func ExampleClient_Search() { // Let's assume c is a client var c *client.Client // Select INBOX _, err := c.Select("INBOX", false) if err != nil { log.Fatal(err) } // Set search criteria criteria := imap.NewSearchCriteria() criteria.WithoutFlags = []string{imap.SeenFlag} ids, err := c.Search(criteria) if err != nil { log.Fatal(err) } log.Println("IDs found:", ids) if len(ids) > 0 { seqset := new(imap.SeqSet) seqset.AddNum(ids...) messages := make(chan *imap.Message, 10) done := make(chan error, 1) go func() { done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages) }() log.Println("Unseen messages:") for msg := range messages { log.Println("* " + msg.Envelope.Subject) } if err := <-done; err != nil { log.Fatal(err) } } log.Println("Done!") } func ExampleClient_Idle() { // Let's assume c is a client var c *client.Client // Select a mailbox if _, err := c.Select("INBOX", false); err != nil { log.Fatal(err) } // Create a channel to receive mailbox updates updates := make(chan client.Update) c.Updates = updates // Start idling stopped := false stop := make(chan struct{}) done := make(chan error, 1) go func() { done <- c.Idle(stop, nil) }() // Listen for updates for { select { case update := <-updates: log.Println("New update:", update) if !stopped { close(stop) stopped = true } case err := <-done: if err != nil { log.Fatal(err) } log.Println("Not idling anymore") return } } } go-imap-1.2.0/client/tag.go000066400000000000000000000005351412725504300154310ustar00rootroot00000000000000package client import ( "crypto/rand" "encoding/base64" ) func randomString(n int) (string, error) { b := make([]byte, n) _, err := rand.Read(b) if err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b), nil } func generateTag() string { tag, err := randomString(4) if err != nil { panic(err) } return tag } go-imap-1.2.0/command.go000066400000000000000000000023501412725504300150130ustar00rootroot00000000000000package imap import ( "errors" "strings" ) // A value that can be converted to a command. type Commander interface { Command() *Command } // A command. type Command struct { // The command tag. It acts as a unique identifier for this command. If empty, // the command is untagged. Tag string // The command name. Name string // The command arguments. Arguments []interface{} } // Implements the Commander interface. func (cmd *Command) Command() *Command { return cmd } func (cmd *Command) WriteTo(w *Writer) error { tag := cmd.Tag if tag == "" { tag = "*" } fields := []interface{}{RawString(tag), RawString(cmd.Name)} fields = append(fields, cmd.Arguments...) return w.writeLine(fields...) } // Parse a command from fields. func (cmd *Command) Parse(fields []interface{}) error { if len(fields) < 2 { return errors.New("imap: cannot parse command: no enough fields") } var ok bool if cmd.Tag, ok = fields[0].(string); !ok { return errors.New("imap: cannot parse command: invalid tag") } if cmd.Name, ok = fields[1].(string); !ok { return errors.New("imap: cannot parse command: invalid name") } cmd.Name = strings.ToUpper(cmd.Name) // Command names are case-insensitive cmd.Arguments = fields[2:] return nil } go-imap-1.2.0/command_test.go000066400000000000000000000037421412725504300160600ustar00rootroot00000000000000package imap_test import ( "bytes" "testing" "github.com/emersion/go-imap" ) func TestCommand_Command(t *testing.T) { cmd := &imap.Command{ Tag: "A001", Name: "NOOP", } if cmd.Command() != cmd { t.Error("Command should return itself") } } func TestCommand_WriteTo_NoArgs(t *testing.T) { var b bytes.Buffer w := imap.NewWriter(&b) cmd := &imap.Command{ Tag: "A001", Name: "NOOP", } if err := cmd.WriteTo(w); err != nil { t.Fatal(err) } if b.String() != "A001 NOOP\r\n" { t.Fatal("Not the expected command: ", b.String()) } } func TestCommand_WriteTo_WithArgs(t *testing.T) { var b bytes.Buffer w := imap.NewWriter(&b) cmd := &imap.Command{ Tag: "A002", Name: "LOGIN", Arguments: []interface{}{"username", "password"}, } if err := cmd.WriteTo(w); err != nil { t.Fatal(err) } if b.String() != "A002 LOGIN \"username\" \"password\"\r\n" { t.Fatal("Not the expected command: ", b.String()) } } func TestCommand_Parse_NoArgs(t *testing.T) { fields := []interface{}{"a", "NOOP"} cmd := &imap.Command{} if err := cmd.Parse(fields); err != nil { t.Fatal(err) } if cmd.Tag != "a" { t.Error("Invalid tag:", cmd.Tag) } if cmd.Name != "NOOP" { t.Error("Invalid name:", cmd.Name) } if len(cmd.Arguments) != 0 { t.Error("Invalid arguments:", cmd.Arguments) } } func TestCommand_Parse_WithArgs(t *testing.T) { fields := []interface{}{"a", "LOGIN", "username", "password"} cmd := &imap.Command{} if err := cmd.Parse(fields); err != nil { t.Fatal(err) } if cmd.Tag != "a" { t.Error("Invalid tag:", cmd.Tag) } if cmd.Name != "LOGIN" { t.Error("Invalid name:", cmd.Name) } if len(cmd.Arguments) != 2 { t.Error("Invalid arguments:", cmd.Arguments) } if username, ok := cmd.Arguments[0].(string); !ok || username != "username" { t.Error("Invalid first argument:", cmd.Arguments[0]) } if password, ok := cmd.Arguments[1].(string); !ok || password != "password" { t.Error("Invalid second argument:", cmd.Arguments[1]) } } go-imap-1.2.0/commands/000077500000000000000000000000001412725504300146475ustar00rootroot00000000000000go-imap-1.2.0/commands/append.go000066400000000000000000000037101412725504300164460ustar00rootroot00000000000000package commands import ( "errors" "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/utf7" ) // Append is an APPEND command, as defined in RFC 3501 section 6.3.11. type Append struct { Mailbox string Flags []string Date time.Time Message imap.Literal } func (cmd *Append) Command() *imap.Command { var args []interface{} mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) args = append(args, imap.FormatMailboxName(mailbox)) if cmd.Flags != nil { flags := make([]interface{}, len(cmd.Flags)) for i, flag := range cmd.Flags { flags[i] = imap.RawString(flag) } args = append(args, flags) } if !cmd.Date.IsZero() { args = append(args, cmd.Date) } args = append(args, cmd.Message) return &imap.Command{ Name: "APPEND", Arguments: args, } } func (cmd *Append) Parse(fields []interface{}) (err error) { if len(fields) < 2 { return errors.New("No enough arguments") } // Parse mailbox name if mailbox, err := imap.ParseString(fields[0]); err != nil { return err } else if mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil { return err } else { cmd.Mailbox = imap.CanonicalMailboxName(mailbox) } // Parse message literal litIndex := len(fields) - 1 var ok bool if cmd.Message, ok = fields[litIndex].(imap.Literal); !ok { return errors.New("Message must be a literal") } // Remaining fields a optional fields = fields[1:litIndex] if len(fields) > 0 { // Parse flags list if flags, ok := fields[0].([]interface{}); ok { if cmd.Flags, err = imap.ParseStringList(flags); err != nil { return err } for i, flag := range cmd.Flags { cmd.Flags[i] = imap.CanonicalFlag(flag) } fields = fields[1:] } // Parse date if len(fields) > 0 { if date, ok := fields[0].(string); !ok { return errors.New("Date must be a string") } else if cmd.Date, err = time.Parse(imap.DateTimeLayout, date); err != nil { return err } } } return } go-imap-1.2.0/commands/authenticate.go000066400000000000000000000052201412725504300176530ustar00rootroot00000000000000package commands import ( "bufio" "encoding/base64" "errors" "io" "strings" "github.com/emersion/go-imap" "github.com/emersion/go-sasl" ) // AuthenticateConn is a connection that supports IMAP authentication. type AuthenticateConn interface { io.Reader // WriteResp writes an IMAP response to this connection. WriteResp(res imap.WriterTo) error } // Authenticate is an AUTHENTICATE command, as defined in RFC 3501 section // 6.2.2. type Authenticate struct { Mechanism string InitialResponse []byte } func (cmd *Authenticate) Command() *imap.Command { args := []interface{}{imap.RawString(cmd.Mechanism)} if cmd.InitialResponse != nil { var encodedResponse string if len(cmd.InitialResponse) == 0 { // Empty initial response should be encoded as "=", not empty // string. encodedResponse = "=" } else { encodedResponse = base64.StdEncoding.EncodeToString(cmd.InitialResponse) } args = append(args, imap.RawString(encodedResponse)) } return &imap.Command{ Name: "AUTHENTICATE", Arguments: args, } } func (cmd *Authenticate) Parse(fields []interface{}) error { if len(fields) < 1 { return errors.New("Not enough arguments") } var ok bool if cmd.Mechanism, ok = fields[0].(string); !ok { return errors.New("Mechanism must be a string") } cmd.Mechanism = strings.ToUpper(cmd.Mechanism) if len(fields) != 2 { return nil } encodedResponse, ok := fields[1].(string) if !ok { return errors.New("Initial response must be a string") } if encodedResponse == "=" { cmd.InitialResponse = []byte{} return nil } var err error cmd.InitialResponse, err = base64.StdEncoding.DecodeString(encodedResponse) if err != nil { return err } return nil } func (cmd *Authenticate) Handle(mechanisms map[string]sasl.Server, conn AuthenticateConn) error { sasl, ok := mechanisms[cmd.Mechanism] if !ok { return errors.New("Unsupported mechanism") } scanner := bufio.NewScanner(conn) response := cmd.InitialResponse for { challenge, done, err := sasl.Next(response) if err != nil || done { return err } encoded := base64.StdEncoding.EncodeToString(challenge) cont := &imap.ContinuationReq{Info: encoded} if err := conn.WriteResp(cont); err != nil { return err } if !scanner.Scan() { if err := scanner.Err(); err != nil { return err } return errors.New("unexpected EOF") } encoded = scanner.Text() if encoded != "" { if encoded == "*" { return &imap.ErrStatusResp{Resp: &imap.StatusResp{ Type: imap.StatusRespBad, Info: "negotiation cancelled", }} } response, err = base64.StdEncoding.DecodeString(encoded) if err != nil { return err } } } } go-imap-1.2.0/commands/capability.go000066400000000000000000000005151412725504300173200ustar00rootroot00000000000000package commands import ( "github.com/emersion/go-imap" ) // Capability is a CAPABILITY command, as defined in RFC 3501 section 6.1.1. type Capability struct{} func (c *Capability) Command() *imap.Command { return &imap.Command{ Name: "CAPABILITY", } } func (c *Capability) Parse(fields []interface{}) error { return nil } go-imap-1.2.0/commands/check.go000066400000000000000000000004631412725504300162560ustar00rootroot00000000000000package commands import ( "github.com/emersion/go-imap" ) // Check is a CHECK command, as defined in RFC 3501 section 6.4.1. type Check struct{} func (cmd *Check) Command() *imap.Command { return &imap.Command{ Name: "CHECK", } } func (cmd *Check) Parse(fields []interface{}) error { return nil } go-imap-1.2.0/commands/close.go000066400000000000000000000004631412725504300163060ustar00rootroot00000000000000package commands import ( "github.com/emersion/go-imap" ) // Close is a CLOSE command, as defined in RFC 3501 section 6.4.2. type Close struct{} func (cmd *Close) Command() *imap.Command { return &imap.Command{ Name: "CLOSE", } } func (cmd *Close) Parse(fields []interface{}) error { return nil } go-imap-1.2.0/commands/commands.go000066400000000000000000000001231412725504300167730ustar00rootroot00000000000000// Package commands implements IMAP commands defined in RFC 3501. package commands go-imap-1.2.0/commands/copy.go000066400000000000000000000020151412725504300161460ustar00rootroot00000000000000package commands import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/utf7" ) // Copy is a COPY command, as defined in RFC 3501 section 6.4.7. type Copy struct { SeqSet *imap.SeqSet Mailbox string } func (cmd *Copy) Command() *imap.Command { mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) return &imap.Command{ Name: "COPY", Arguments: []interface{}{cmd.SeqSet, imap.FormatMailboxName(mailbox)}, } } func (cmd *Copy) Parse(fields []interface{}) error { if len(fields) < 2 { return errors.New("No enough arguments") } if seqSet, ok := fields[0].(string); !ok { return errors.New("Invalid sequence set") } else if seqSet, err := imap.ParseSeqSet(seqSet); err != nil { return err } else { cmd.SeqSet = seqSet } if mailbox, err := imap.ParseString(fields[1]); err != nil { return err } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { return err } else { cmd.Mailbox = imap.CanonicalMailboxName(mailbox) } return nil } go-imap-1.2.0/commands/create.go000066400000000000000000000014251412725504300164430ustar00rootroot00000000000000package commands import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/utf7" ) // Create is a CREATE command, as defined in RFC 3501 section 6.3.3. type Create struct { Mailbox string } func (cmd *Create) Command() *imap.Command { mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) return &imap.Command{ Name: "CREATE", Arguments: []interface{}{mailbox}, } } func (cmd *Create) Parse(fields []interface{}) error { if len(fields) < 1 { return errors.New("No enough arguments") } if mailbox, err := imap.ParseString(fields[0]); err != nil { return err } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { return err } else { cmd.Mailbox = imap.CanonicalMailboxName(mailbox) } return nil } go-imap-1.2.0/commands/delete.go000066400000000000000000000014551412725504300164450ustar00rootroot00000000000000package commands import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/utf7" ) // Delete is a DELETE command, as defined in RFC 3501 section 6.3.3. type Delete struct { Mailbox string } func (cmd *Delete) Command() *imap.Command { mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) return &imap.Command{ Name: "DELETE", Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, } } func (cmd *Delete) Parse(fields []interface{}) error { if len(fields) < 1 { return errors.New("No enough arguments") } if mailbox, err := imap.ParseString(fields[0]); err != nil { return err } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { return err } else { cmd.Mailbox = imap.CanonicalMailboxName(mailbox) } return nil } go-imap-1.2.0/commands/enable.go000066400000000000000000000006541412725504300164310ustar00rootroot00000000000000package commands import ( "github.com/emersion/go-imap" ) // An ENABLE command, defined in RFC 5161 section 3.1. type Enable struct { Caps []string } func (cmd *Enable) Command() *imap.Command { return &imap.Command{ Name: "ENABLE", Arguments: imap.FormatStringList(cmd.Caps), } } func (cmd *Enable) Parse(fields []interface{}) error { var err error cmd.Caps, err = imap.ParseStringList(fields) return err } go-imap-1.2.0/commands/expunge.go000066400000000000000000000004721412725504300166540ustar00rootroot00000000000000package commands import ( "github.com/emersion/go-imap" ) // Expunge is an EXPUNGE command, as defined in RFC 3501 section 6.4.3. type Expunge struct{} func (cmd *Expunge) Command() *imap.Command { return &imap.Command{Name: "EXPUNGE"} } func (cmd *Expunge) Parse(fields []interface{}) error { return nil } go-imap-1.2.0/commands/fetch.go000066400000000000000000000024041412725504300162670ustar00rootroot00000000000000package commands import ( "errors" "strings" "github.com/emersion/go-imap" ) // Fetch is a FETCH command, as defined in RFC 3501 section 6.4.5. type Fetch struct { SeqSet *imap.SeqSet Items []imap.FetchItem } func (cmd *Fetch) Command() *imap.Command { items := make([]interface{}, len(cmd.Items)) for i, item := range cmd.Items { items[i] = imap.RawString(item) } return &imap.Command{ Name: "FETCH", Arguments: []interface{}{cmd.SeqSet, items}, } } func (cmd *Fetch) Parse(fields []interface{}) error { if len(fields) < 2 { return errors.New("No enough arguments") } var err error if seqset, ok := fields[0].(string); !ok { return errors.New("Sequence set must be an atom") } else if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil { return err } switch items := fields[1].(type) { case string: // A macro or a single item cmd.Items = imap.FetchItem(strings.ToUpper(items)).Expand() case []interface{}: // A list of items cmd.Items = make([]imap.FetchItem, 0, len(items)) for _, v := range items { itemStr, _ := v.(string) item := imap.FetchItem(strings.ToUpper(itemStr)) cmd.Items = append(cmd.Items, item.Expand()...) } default: return errors.New("Items must be either a string or a list") } return nil } go-imap-1.2.0/commands/idle.go000066400000000000000000000004241412725504300161130ustar00rootroot00000000000000package commands import ( "github.com/emersion/go-imap" ) // An IDLE command. // Se RFC 2177 section 3. type Idle struct{} func (cmd *Idle) Command() *imap.Command { return &imap.Command{Name: "IDLE"} } func (cmd *Idle) Parse(fields []interface{}) error { return nil } go-imap-1.2.0/commands/list.go000066400000000000000000000023371412725504300161560ustar00rootroot00000000000000package commands import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/utf7" ) // List is a LIST command, as defined in RFC 3501 section 6.3.8. If Subscribed // is set to true, LSUB will be used instead. type List struct { Reference string Mailbox string Subscribed bool } func (cmd *List) Command() *imap.Command { name := "LIST" if cmd.Subscribed { name = "LSUB" } enc := utf7.Encoding.NewEncoder() ref, _ := enc.String(cmd.Reference) mailbox, _ := enc.String(cmd.Mailbox) return &imap.Command{ Name: name, Arguments: []interface{}{ref, mailbox}, } } func (cmd *List) Parse(fields []interface{}) error { if len(fields) < 2 { return errors.New("No enough arguments") } dec := utf7.Encoding.NewDecoder() if mailbox, err := imap.ParseString(fields[0]); err != nil { return err } else if mailbox, err := dec.String(mailbox); err != nil { return err } else { // TODO: canonical mailbox path cmd.Reference = imap.CanonicalMailboxName(mailbox) } if mailbox, err := imap.ParseString(fields[1]); err != nil { return err } else if mailbox, err := dec.String(mailbox); err != nil { return err } else { cmd.Mailbox = imap.CanonicalMailboxName(mailbox) } return nil } go-imap-1.2.0/commands/login.go000066400000000000000000000012311412725504300163030ustar00rootroot00000000000000package commands import ( "errors" "github.com/emersion/go-imap" ) // Login is a LOGIN command, as defined in RFC 3501 section 6.2.2. type Login struct { Username string Password string } func (cmd *Login) Command() *imap.Command { return &imap.Command{ Name: "LOGIN", Arguments: []interface{}{cmd.Username, cmd.Password}, } } func (cmd *Login) Parse(fields []interface{}) error { if len(fields) < 2 { return errors.New("Not enough arguments") } var err error if cmd.Username, err = imap.ParseString(fields[0]); err != nil { return err } if cmd.Password, err = imap.ParseString(fields[1]); err != nil { return err } return nil } go-imap-1.2.0/commands/logout.go000066400000000000000000000004651412725504300165140ustar00rootroot00000000000000package commands import ( "github.com/emersion/go-imap" ) // Logout is a LOGOUT command, as defined in RFC 3501 section 6.1.3. type Logout struct{} func (c *Logout) Command() *imap.Command { return &imap.Command{ Name: "LOGOUT", } } func (c *Logout) Parse(fields []interface{}) error { return nil } go-imap-1.2.0/commands/move.go000066400000000000000000000016361412725504300161520ustar00rootroot00000000000000package commands import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/utf7" ) // A MOVE command. // See RFC 6851 section 3.1. type Move struct { SeqSet *imap.SeqSet Mailbox string } func (cmd *Move) Command() *imap.Command { mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) return &imap.Command{ Name: "MOVE", Arguments: []interface{}{cmd.SeqSet, mailbox}, } } func (cmd *Move) Parse(fields []interface{}) (err error) { if len(fields) < 2 { return errors.New("No enough arguments") } seqset, ok := fields[0].(string) if !ok { return errors.New("Invalid sequence set") } if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil { return err } mailbox, ok := fields[1].(string) if !ok { return errors.New("Mailbox name must be a string") } if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil { return err } return } go-imap-1.2.0/commands/noop.go000066400000000000000000000004511412725504300161510ustar00rootroot00000000000000package commands import ( "github.com/emersion/go-imap" ) // Noop is a NOOP command, as defined in RFC 3501 section 6.1.2. type Noop struct{} func (c *Noop) Command() *imap.Command { return &imap.Command{ Name: "NOOP", } } func (c *Noop) Parse(fields []interface{}) error { return nil } go-imap-1.2.0/commands/rename.go000066400000000000000000000022161412725504300164460ustar00rootroot00000000000000package commands import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/utf7" ) // Rename is a RENAME command, as defined in RFC 3501 section 6.3.5. type Rename struct { Existing string New string } func (cmd *Rename) Command() *imap.Command { enc := utf7.Encoding.NewEncoder() existingName, _ := enc.String(cmd.Existing) newName, _ := enc.String(cmd.New) return &imap.Command{ Name: "RENAME", Arguments: []interface{}{imap.FormatMailboxName(existingName), imap.FormatMailboxName(newName)}, } } func (cmd *Rename) Parse(fields []interface{}) error { if len(fields) < 2 { return errors.New("No enough arguments") } dec := utf7.Encoding.NewDecoder() if existingName, err := imap.ParseString(fields[0]); err != nil { return err } else if existingName, err := dec.String(existingName); err != nil { return err } else { cmd.Existing = imap.CanonicalMailboxName(existingName) } if newName, err := imap.ParseString(fields[1]); err != nil { return err } else if newName, err := dec.String(newName); err != nil { return err } else { cmd.New = imap.CanonicalMailboxName(newName) } return nil } go-imap-1.2.0/commands/search.go000066400000000000000000000024361412725504300164500ustar00rootroot00000000000000package commands import ( "errors" "io" "strings" "github.com/emersion/go-imap" ) // Search is a SEARCH command, as defined in RFC 3501 section 6.4.4. type Search struct { Charset string Criteria *imap.SearchCriteria } func (cmd *Search) Command() *imap.Command { var args []interface{} if cmd.Charset != "" { args = append(args, imap.RawString("CHARSET"), imap.RawString(cmd.Charset)) } args = append(args, cmd.Criteria.Format()...) return &imap.Command{ Name: "SEARCH", Arguments: args, } } func (cmd *Search) Parse(fields []interface{}) error { if len(fields) == 0 { return errors.New("Missing search criteria") } // Parse charset if f, ok := fields[0].(string); ok && strings.EqualFold(f, "CHARSET") { if len(fields) < 2 { return errors.New("Missing CHARSET value") } if cmd.Charset, ok = fields[1].(string); !ok { return errors.New("Charset must be a string") } fields = fields[2:] } var charsetReader func(io.Reader) io.Reader charset := strings.ToLower(cmd.Charset) if charset != "utf-8" && charset != "us-ascii" && charset != "" { charsetReader = func(r io.Reader) io.Reader { r, _ = imap.CharsetReader(charset, r) return r } } cmd.Criteria = new(imap.SearchCriteria) return cmd.Criteria.ParseWithCharset(fields, charsetReader) } go-imap-1.2.0/commands/select.go000066400000000000000000000016761412725504300164670ustar00rootroot00000000000000package commands import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/utf7" ) // Select is a SELECT command, as defined in RFC 3501 section 6.3.1. If ReadOnly // is set to true, the EXAMINE command will be used instead. type Select struct { Mailbox string ReadOnly bool } func (cmd *Select) Command() *imap.Command { name := "SELECT" if cmd.ReadOnly { name = "EXAMINE" } mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) return &imap.Command{ Name: name, Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, } } func (cmd *Select) Parse(fields []interface{}) error { if len(fields) < 1 { return errors.New("No enough arguments") } if mailbox, err := imap.ParseString(fields[0]); err != nil { return err } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { return err } else { cmd.Mailbox = imap.CanonicalMailboxName(mailbox) } return nil } go-imap-1.2.0/commands/starttls.go000066400000000000000000000005051412725504300170560ustar00rootroot00000000000000package commands import ( "github.com/emersion/go-imap" ) // StartTLS is a STARTTLS command, as defined in RFC 3501 section 6.2.1. type StartTLS struct{} func (cmd *StartTLS) Command() *imap.Command { return &imap.Command{ Name: "STARTTLS", } } func (cmd *StartTLS) Parse(fields []interface{}) error { return nil } go-imap-1.2.0/commands/status.go000066400000000000000000000025061412725504300165240ustar00rootroot00000000000000package commands import ( "errors" "strings" "github.com/emersion/go-imap" "github.com/emersion/go-imap/utf7" ) // Status is a STATUS command, as defined in RFC 3501 section 6.3.10. type Status struct { Mailbox string Items []imap.StatusItem } func (cmd *Status) Command() *imap.Command { mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) items := make([]interface{}, len(cmd.Items)) for i, item := range cmd.Items { items[i] = imap.RawString(item) } return &imap.Command{ Name: "STATUS", Arguments: []interface{}{imap.FormatMailboxName(mailbox), items}, } } func (cmd *Status) Parse(fields []interface{}) error { if len(fields) < 2 { return errors.New("No enough arguments") } if mailbox, err := imap.ParseString(fields[0]); err != nil { return err } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { return err } else { cmd.Mailbox = imap.CanonicalMailboxName(mailbox) } items, ok := fields[1].([]interface{}) if !ok { return errors.New("STATUS command parameter is not a list") } cmd.Items = make([]imap.StatusItem, len(items)) for i, f := range items { if s, ok := f.(string); !ok { return errors.New("Got a non-string field in a STATUS command parameter") } else { cmd.Items[i] = imap.StatusItem(strings.ToUpper(s)) } } return nil } go-imap-1.2.0/commands/store.go000066400000000000000000000017201412725504300163320ustar00rootroot00000000000000package commands import ( "errors" "strings" "github.com/emersion/go-imap" ) // Store is a STORE command, as defined in RFC 3501 section 6.4.6. type Store struct { SeqSet *imap.SeqSet Item imap.StoreItem Value interface{} } func (cmd *Store) Command() *imap.Command { return &imap.Command{ Name: "STORE", Arguments: []interface{}{cmd.SeqSet, imap.RawString(cmd.Item), cmd.Value}, } } func (cmd *Store) Parse(fields []interface{}) error { if len(fields) < 3 { return errors.New("No enough arguments") } seqset, ok := fields[0].(string) if !ok { return errors.New("Invalid sequence set") } var err error if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil { return err } if item, ok := fields[1].(string); !ok { return errors.New("Item name must be a string") } else { cmd.Item = imap.StoreItem(strings.ToUpper(item)) } if len(fields[2:]) == 1 { cmd.Value = fields[2] } else { cmd.Value = fields[2:] } return nil } go-imap-1.2.0/commands/subscribe.go000066400000000000000000000026221412725504300171610ustar00rootroot00000000000000package commands import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/utf7" ) // Subscribe is a SUBSCRIBE command, as defined in RFC 3501 section 6.3.6. type Subscribe struct { Mailbox string } func (cmd *Subscribe) Command() *imap.Command { mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) return &imap.Command{ Name: "SUBSCRIBE", Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, } } func (cmd *Subscribe) Parse(fields []interface{}) error { if len(fields) < 0 { return errors.New("No enough arguments") } if mailbox, err := imap.ParseString(fields[0]); err != nil { return err } else if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil { return err } return nil } // An UNSUBSCRIBE command. // See RFC 3501 section 6.3.7 type Unsubscribe struct { Mailbox string } func (cmd *Unsubscribe) Command() *imap.Command { mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) return &imap.Command{ Name: "UNSUBSCRIBE", Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, } } func (cmd *Unsubscribe) Parse(fields []interface{}) error { if len(fields) < 0 { return errors.New("No enogh arguments") } if mailbox, err := imap.ParseString(fields[0]); err != nil { return err } else if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil { return err } return nil } go-imap-1.2.0/commands/uid.go000066400000000000000000000015541412725504300157640ustar00rootroot00000000000000package commands import ( "errors" "strings" "github.com/emersion/go-imap" ) // Uid is a UID command, as defined in RFC 3501 section 6.4.8. It wraps another // command (e.g. wrapping a Fetch command will result in a UID FETCH). type Uid struct { Cmd imap.Commander } func (cmd *Uid) Command() *imap.Command { inner := cmd.Cmd.Command() args := []interface{}{imap.RawString(inner.Name)} args = append(args, inner.Arguments...) return &imap.Command{ Name: "UID", Arguments: args, } } func (cmd *Uid) Parse(fields []interface{}) error { if len(fields) < 0 { return errors.New("No command name specified") } name, ok := fields[0].(string) if !ok { return errors.New("Command name must be a string") } cmd.Cmd = &imap.Command{ Name: strings.ToUpper(name), // Command names are case-insensitive Arguments: fields[1:], } return nil } go-imap-1.2.0/commands/unselect.go000066400000000000000000000004511412725504300170200ustar00rootroot00000000000000package commands import ( "github.com/emersion/go-imap" ) // An UNSELECT command. // See RFC 3691 section 2. type Unselect struct{} func (cmd *Unselect) Command() *imap.Command { return &imap.Command{Name: "UNSELECT"} } func (cmd *Unselect) Parse(fields []interface{}) error { return nil } go-imap-1.2.0/conn.go000066400000000000000000000140671412725504300143420ustar00rootroot00000000000000package imap import ( "bufio" "crypto/tls" "io" "net" "sync" ) // A connection state. // See RFC 3501 section 3. type ConnState int const ( // In the connecting state, the server has not yet sent a greeting and no // command can be issued. ConnectingState = 0 // In the not authenticated state, the client MUST supply // authentication credentials before most commands will be // permitted. This state is entered when a connection starts // unless the connection has been pre-authenticated. NotAuthenticatedState ConnState = 1 << 0 // In the authenticated state, the client is authenticated and MUST // select a mailbox to access before commands that affect messages // will be permitted. This state is entered when a // pre-authenticated connection starts, when acceptable // authentication credentials have been provided, after an error in // selecting a mailbox, or after a successful CLOSE command. AuthenticatedState = 1 << 1 // In a selected state, a mailbox has been selected to access. // This state is entered when a mailbox has been successfully // selected. SelectedState = AuthenticatedState + 1<<2 // In the logout state, the connection is being terminated. This // state can be entered as a result of a client request (via the // LOGOUT command) or by unilateral action on the part of either // the client or server. LogoutState = 1 << 3 // ConnectedState is either NotAuthenticatedState, AuthenticatedState or // SelectedState. ConnectedState = NotAuthenticatedState | AuthenticatedState | SelectedState ) // A function that upgrades a connection. // // This should only be used by libraries implementing an IMAP extension (e.g. // COMPRESS). type ConnUpgrader func(conn net.Conn) (net.Conn, error) type Waiter struct { start sync.WaitGroup end sync.WaitGroup finished bool } func NewWaiter() *Waiter { w := &Waiter{finished: false} w.start.Add(1) w.end.Add(1) return w } func (w *Waiter) Wait() { if !w.finished { // Signal that we are ready for upgrade to continue. w.start.Done() // Wait for upgrade to finish. w.end.Wait() w.finished = true } } func (w *Waiter) WaitReady() { if !w.finished { // Wait for reader/writer goroutine to be ready for upgrade. w.start.Wait() } } func (w *Waiter) Close() { if !w.finished { // Upgrade is finished, close chanel to release reader/writer w.end.Done() } } type LockedWriter struct { lock sync.Mutex writer io.Writer } // NewLockedWriter - goroutine safe writer. func NewLockedWriter(w io.Writer) io.Writer { return &LockedWriter{writer: w} } func (w *LockedWriter) Write(b []byte) (int, error) { w.lock.Lock() defer w.lock.Unlock() return w.writer.Write(b) } type debugWriter struct { io.Writer local io.Writer remote io.Writer } // NewDebugWriter creates a new io.Writer that will write local network activity // to local and remote network activity to remote. func NewDebugWriter(local, remote io.Writer) io.Writer { return &debugWriter{Writer: local, local: local, remote: remote} } type multiFlusher struct { flushers []flusher } func (mf *multiFlusher) Flush() error { for _, f := range mf.flushers { if err := f.Flush(); err != nil { return err } } return nil } func newMultiFlusher(flushers ...flusher) flusher { return &multiFlusher{flushers} } // Underlying connection state information. type ConnInfo struct { RemoteAddr net.Addr LocalAddr net.Addr // nil if connection is not using TLS. TLS *tls.ConnectionState } // An IMAP connection. type Conn struct { net.Conn *Reader *Writer br *bufio.Reader bw *bufio.Writer waiter *Waiter // Print all commands and responses to this io.Writer. debug io.Writer } // NewConn creates a new IMAP connection. func NewConn(conn net.Conn, r *Reader, w *Writer) *Conn { c := &Conn{Conn: conn, Reader: r, Writer: w} c.init() return c } func (c *Conn) createWaiter() *Waiter { // create new waiter each time. w := NewWaiter() c.waiter = w return w } func (c *Conn) init() { r := io.Reader(c.Conn) w := io.Writer(c.Conn) if c.debug != nil { localDebug, remoteDebug := c.debug, c.debug if debug, ok := c.debug.(*debugWriter); ok { localDebug, remoteDebug = debug.local, debug.remote } // If local and remote are the same, then we need a LockedWriter. if localDebug == remoteDebug { localDebug = NewLockedWriter(localDebug) remoteDebug = localDebug } if localDebug != nil { w = io.MultiWriter(c.Conn, localDebug) } if remoteDebug != nil { r = io.TeeReader(c.Conn, remoteDebug) } } if c.br == nil { c.br = bufio.NewReader(r) c.Reader.reader = c.br } else { c.br.Reset(r) } if c.bw == nil { c.bw = bufio.NewWriter(w) c.Writer.Writer = c.bw } else { c.bw.Reset(w) } if f, ok := c.Conn.(flusher); ok { c.Writer.Writer = struct { io.Writer flusher }{ c.bw, newMultiFlusher(c.bw, f), } } } func (c *Conn) Info() *ConnInfo { info := &ConnInfo{ RemoteAddr: c.RemoteAddr(), LocalAddr: c.LocalAddr(), } tlsConn, ok := c.Conn.(*tls.Conn) if ok { state := tlsConn.ConnectionState() info.TLS = &state } return info } // Write implements io.Writer. func (c *Conn) Write(b []byte) (n int, err error) { return c.Writer.Write(b) } // Flush writes any buffered data to the underlying connection. func (c *Conn) Flush() error { return c.Writer.Flush() } // Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted // tunnel. func (c *Conn) Upgrade(upgrader ConnUpgrader) error { // Block reads and writes during the upgrading process w := c.createWaiter() defer w.Close() upgraded, err := upgrader(c.Conn) if err != nil { return err } c.Conn = upgraded c.init() return nil } // Called by reader/writer goroutines to wait for Upgrade to finish func (c *Conn) Wait() { c.waiter.Wait() } // Called by Upgrader to wait for reader/writer goroutines to be ready for // upgrade. func (c *Conn) WaitReady() { c.waiter.WaitReady() } // SetDebug defines an io.Writer to which all network activity will be logged. // If nil is provided, network activity will not be logged. func (c *Conn) SetDebug(w io.Writer) { c.debug = w c.init() } go-imap-1.2.0/conn_test.go000066400000000000000000000031121412725504300153660ustar00rootroot00000000000000package imap_test import ( "bytes" "io" "net" "testing" "github.com/emersion/go-imap" ) func TestNewConn(t *testing.T) { b := &bytes.Buffer{} c, s := net.Pipe() done := make(chan error) go (func() { _, err := io.Copy(b, s) done <- err })() r := imap.NewReader(nil) w := imap.NewWriter(nil) ic := imap.NewConn(c, r, w) sent := []byte("hi") ic.Write(sent) ic.Flush() ic.Close() if err := <-done; err != nil { t.Fatal(err) } s.Close() received := b.Bytes() if string(sent) != string(received) { t.Errorf("Sent %v but received %v", sent, received) } } func transform(b []byte) []byte { bb := make([]byte, len(b)) for i, c := range b { if rune(c) == 'c' { bb[i] = byte('d') } else { bb[i] = c } } return bb } type upgraded struct { net.Conn } func (c *upgraded) Write(b []byte) (int, error) { return c.Conn.Write(transform(b)) } func TestConn_Upgrade(t *testing.T) { b := &bytes.Buffer{} c, s := net.Pipe() done := make(chan error) go (func() { _, err := io.Copy(b, s) done <- err })() r := imap.NewReader(nil) w := imap.NewWriter(nil) ic := imap.NewConn(c, r, w) began := make(chan struct{}) go ic.Upgrade(func(conn net.Conn) (net.Conn, error) { began <- struct{}{} ic.WaitReady() return &upgraded{conn}, nil }) <-began ic.Wait() sent := []byte("abcd") expected := transform(sent) ic.Write(sent) ic.Flush() ic.Close() if err := <-done; err != nil { t.Fatal(err) } s.Close() received := b.Bytes() if string(expected) != string(received) { t.Errorf("Expected %v but received %v", expected, received) } } go-imap-1.2.0/date.go000066400000000000000000000044771412725504300143260ustar00rootroot00000000000000package imap import ( "fmt" "regexp" "time" ) // Date and time layouts. // Dovecot adds a leading zero to dates: // https://github.com/dovecot/core/blob/4fbd5c5e113078e72f29465ccc96d44955ceadc2/src/lib-imap/imap-date.c#L166 // Cyrus adds a leading space to dates: // https://github.com/cyrusimap/cyrus-imapd/blob/1cb805a3bffbdf829df0964f3b802cdc917e76db/lib/times.c#L543 // GMail doesn't support leading spaces in dates used in SEARCH commands. const ( // Defined in RFC 3501 as date-text on page 83. DateLayout = "_2-Jan-2006" // Defined in RFC 3501 as date-time on page 83. DateTimeLayout = "_2-Jan-2006 15:04:05 -0700" // Defined in RFC 5322 section 3.3, mentioned as env-date in RFC 3501 page 84. envelopeDateTimeLayout = "Mon, 02 Jan 2006 15:04:05 -0700" // Use as an example in RFC 3501 page 54. searchDateLayout = "2-Jan-2006" ) // time.Time with a specific layout. type ( Date time.Time DateTime time.Time envelopeDateTime time.Time searchDate time.Time ) // Permutations of the layouts defined in RFC 5322, section 3.3. var envelopeDateTimeLayouts = [...]string{ envelopeDateTimeLayout, // popular, try it first "_2 Jan 2006 15:04:05 -0700", "_2 Jan 2006 15:04:05 MST", "_2 Jan 2006 15:04 -0700", "_2 Jan 2006 15:04 MST", "_2 Jan 06 15:04:05 -0700", "_2 Jan 06 15:04:05 MST", "_2 Jan 06 15:04 -0700", "_2 Jan 06 15:04 MST", "Mon, _2 Jan 2006 15:04:05 -0700", "Mon, _2 Jan 2006 15:04:05 MST", "Mon, _2 Jan 2006 15:04 -0700", "Mon, _2 Jan 2006 15:04 MST", "Mon, _2 Jan 06 15:04:05 -0700", "Mon, _2 Jan 06 15:04:05 MST", "Mon, _2 Jan 06 15:04 -0700", "Mon, _2 Jan 06 15:04 MST", } // TODO: this is a blunt way to strip any trailing CFWS (comment). A sharper // one would strip multiple CFWS, and only if really valid according to // RFC5322. var commentRE = regexp.MustCompile(`[ \t]+\(.*\)$`) // Try parsing the date based on the layouts defined in RFC 5322, section 3.3. // Inspired by https://github.com/golang/go/blob/master/src/net/mail/message.go func parseMessageDateTime(maybeDate string) (time.Time, error) { maybeDate = commentRE.ReplaceAllString(maybeDate, "") for _, layout := range envelopeDateTimeLayouts { parsed, err := time.Parse(layout, maybeDate) if err == nil { return parsed, nil } } return time.Time{}, fmt.Errorf("date %s could not be parsed", maybeDate) } go-imap-1.2.0/date_test.go000066400000000000000000000051721412725504300153560ustar00rootroot00000000000000package imap import ( "testing" "time" ) var expectedDateTime = time.Date(2009, time.November, 2, 23, 0, 0, 0, time.FixedZone("", -6*60*60)) var expectedDate = time.Date(2009, time.November, 2, 0, 0, 0, 0, time.FixedZone("", 0)) func TestParseMessageDateTime(t *testing.T) { tests := []struct { in string out time.Time ok bool }{ // some permutations {"2 Nov 2009 23:00 -0600", expectedDateTime, true}, {"Tue, 2 Nov 2009 23:00:00 -0600", expectedDateTime, true}, {"Tue, 2 Nov 2009 23:00:00 -0600 (MST)", expectedDateTime, true}, // whitespace {" 2 Nov 2009 23:00 -0600", expectedDateTime, true}, {"Tue, 2 Nov 2009 23:00:00 -0600", expectedDateTime, true}, {"Tue, 2 Nov 2009 23:00:00 -0600 (MST)", expectedDateTime, true}, // invalid {"abc10 Nov 2009 23:00 -0600123", expectedDateTime, false}, {"10.Nov.2009 11:00:00 -9900", expectedDateTime, false}, } for _, test := range tests { out, err := parseMessageDateTime(test.in) if !test.ok { if err == nil { t.Errorf("ParseMessageDateTime(%q) expected error; got %q", test.in, out) } } else if err != nil { t.Errorf("ParseMessageDateTime(%q) expected %q; got %v", test.in, test.out, err) } else if !out.Equal(test.out) { t.Errorf("ParseMessageDateTime(%q) expected %q; got %q", test.in, test.out, out) } } } func TestParseDateTime(t *testing.T) { tests := []struct { in string out time.Time ok bool }{ {"2-Nov-2009 23:00:00 -0600", expectedDateTime, true}, // whitespace {" 2-Nov-2009 23:00:00 -0600", expectedDateTime, true}, // invalid or incorrect {"10-Nov-2009", time.Time{}, false}, {"abc10-Nov-2009 23:00:00 -0600123", time.Time{}, false}, } for _, test := range tests { out, err := time.Parse(DateTimeLayout, test.in) if !test.ok { if err == nil { t.Errorf("ParseDateTime(%q) expected error; got %q", test.in, out) } } else if err != nil { t.Errorf("ParseDateTime(%q) expected %q; got %v", test.in, test.out, err) } else if !out.Equal(test.out) { t.Errorf("ParseDateTime(%q) expected %q; got %q", test.in, test.out, out) } } } func TestParseDate(t *testing.T) { tests := []struct { in string out time.Time ok bool }{ {"2-Nov-2009", expectedDate, true}, {" 2-Nov-2009", expectedDate, true}, } for _, test := range tests { out, err := time.Parse(DateLayout, test.in) if !test.ok { if err == nil { t.Errorf("ParseDate(%q) expected error; got %q", test.in, out) } } else if err != nil { t.Errorf("ParseDate(%q) expected %q; got %v", test.in, test.out, err) } else if !out.Equal(test.out) { t.Errorf("ParseDate(%q) expected %q; got %q", test.in, test.out, out) } } } go-imap-1.2.0/go.mod000066400000000000000000000002731412725504300141560ustar00rootroot00000000000000module github.com/emersion/go-imap go 1.13 require ( github.com/emersion/go-message v0.15.0 github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 golang.org/x/text v0.3.7 ) go-imap-1.2.0/go.sum000066400000000000000000000017431412725504300142060ustar00rootroot00000000000000github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= go-imap-1.2.0/imap.go000066400000000000000000000061061412725504300143260ustar00rootroot00000000000000// Package imap implements IMAP4rev1 (RFC 3501). package imap import ( "errors" "io" "strings" ) // A StatusItem is a mailbox status data item that can be retrieved with a // STATUS command. See RFC 3501 section 6.3.10. type StatusItem string const ( StatusMessages StatusItem = "MESSAGES" StatusRecent StatusItem = "RECENT" StatusUidNext StatusItem = "UIDNEXT" StatusUidValidity StatusItem = "UIDVALIDITY" StatusUnseen StatusItem = "UNSEEN" StatusAppendLimit StatusItem = "APPENDLIMIT" ) // A FetchItem is a message data item that can be fetched. type FetchItem string // List of items that can be fetched. const ( // Macros FetchAll FetchItem = "ALL" FetchFast FetchItem = "FAST" FetchFull FetchItem = "FULL" // Items FetchBody FetchItem = "BODY" FetchBodyStructure FetchItem = "BODYSTRUCTURE" FetchEnvelope FetchItem = "ENVELOPE" FetchFlags FetchItem = "FLAGS" FetchInternalDate FetchItem = "INTERNALDATE" FetchRFC822 FetchItem = "RFC822" FetchRFC822Header FetchItem = "RFC822.HEADER" FetchRFC822Size FetchItem = "RFC822.SIZE" FetchRFC822Text FetchItem = "RFC822.TEXT" FetchUid FetchItem = "UID" ) // Expand expands the item if it's a macro. func (item FetchItem) Expand() []FetchItem { switch item { case FetchAll: return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope} case FetchFast: return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size} case FetchFull: return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope, FetchBody} default: return []FetchItem{item} } } // FlagsOp is an operation that will be applied on message flags. type FlagsOp string const ( // SetFlags replaces existing flags by new ones. SetFlags FlagsOp = "FLAGS" // AddFlags adds new flags. AddFlags = "+FLAGS" // RemoveFlags removes existing flags. RemoveFlags = "-FLAGS" ) // silentOp can be appended to a FlagsOp to prevent the operation from // triggering unilateral message updates. const silentOp = ".SILENT" // A StoreItem is a message data item that can be updated. type StoreItem string // FormatFlagsOp returns the StoreItem that executes the flags operation op. func FormatFlagsOp(op FlagsOp, silent bool) StoreItem { s := string(op) if silent { s += silentOp } return StoreItem(s) } // ParseFlagsOp parses a flags operation from StoreItem. func ParseFlagsOp(item StoreItem) (op FlagsOp, silent bool, err error) { itemStr := string(item) silent = strings.HasSuffix(itemStr, silentOp) if silent { itemStr = strings.TrimSuffix(itemStr, silentOp) } op = FlagsOp(itemStr) if op != SetFlags && op != AddFlags && op != RemoveFlags { err = errors.New("Unsupported STORE operation") } return } // CharsetReader, if non-nil, defines a function to generate charset-conversion // readers, converting from the provided charset into UTF-8. Charsets are always // lower-case. utf-8 and us-ascii charsets are handled by default. One of the // the CharsetReader's result values must be non-nil. var CharsetReader func(charset string, r io.Reader) (io.Reader, error) go-imap-1.2.0/internal/000077500000000000000000000000001412725504300146625ustar00rootroot00000000000000go-imap-1.2.0/internal/testcert.go000066400000000000000000000040621412725504300170500ustar00rootroot00000000000000package internal // LocalhostCert is a PEM-encoded TLS cert with SAN IPs // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. // generated from src/crypto/tls: // 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 var LocalhostCert = []byte(`-----BEGIN CERTIFICATE----- MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4 iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9 tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM fblo6RBxUQ== -----END CERTIFICATE-----`) // LocalhostKey is the private key for localhostCert. var LocalhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9 SjY1bIw4iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZB l2+XsDulrKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQAB AoGAGRzwwir7XvBOAy5tM/uV6e+Zf6anZzus1s1Y1ClbjbE6HXbnWWF/wbZGOpet 3Zm4vD6MXc7jpTLryzTQIvVdfQbRc6+MUVeLKwZatTXtdZrhu+Jk7hx0nTPy8Jcb uJqFk541aEw+mMogY/xEcfbWd6IOkp+4xqjlFLBEDytgbIECQQDvH/E6nk+hgN4H qzzVtxxr397vWrjrIgPbJpQvBsafG7b0dA4AFjwVbFLmQcj2PprIMmPcQrooz8vp jy4SHEg1AkEA/v13/5M47K9vCxmb8QeD/asydfsgS5TeuNi8DoUBEmiSJwma7FXY fFUtxuvL7XvjwjN5B30pNEbc6Iuyt7y4MQJBAIt21su4b3sjXNueLKH85Q+phy2U fQtuUE9txblTu14q3N7gHRZB4ZMhFYyDy8CKrN2cPg/Fvyt0Xlp/DoCzjA0CQQDU y2ptGsuSmgUtWj3NM9xuwYPm+Z/F84K6+ARYiZ6PYj013sovGKUFfYAqVXVlxtIX qyUBnu3X9ps8ZfjLZO7BAkEAlT4R5Yl6cGhaJQYZHOde3JEMhNRcVFMO8dJDaFeo f9Oeos0UUothgiDktdQHxdNEwLjQf7lJJBzV+5OtwswCWA== -----END RSA PRIVATE KEY-----`) go-imap-1.2.0/internal/testutil.go000066400000000000000000000004731412725504300170720ustar00rootroot00000000000000package internal type MapListSorter []interface{} func (s MapListSorter) Len() int { return len(s) / 2 } func (s MapListSorter) Less(i, j int) bool { return s[i*2].(string) < s[j*2].(string) } func (s MapListSorter) Swap(i, j int) { s[i*2], s[j*2] = s[j*2], s[i*2] s[i*2+1], s[j*2+1] = s[j*2+1], s[i*2+1] } go-imap-1.2.0/literal.go000066400000000000000000000002701412725504300150300ustar00rootroot00000000000000package imap import ( "io" ) // A literal, as defined in RFC 3501 section 4.3. type Literal interface { io.Reader // Len returns the number of bytes of the literal. Len() int } go-imap-1.2.0/logger.go000066400000000000000000000003621412725504300146550ustar00rootroot00000000000000package imap // Logger is the behaviour used by server/client to // report errors for accepting connections and unexpected behavior from handlers. type Logger interface { Printf(format string, v ...interface{}) Println(v ...interface{}) } go-imap-1.2.0/mailbox.go000066400000000000000000000206211412725504300150310ustar00rootroot00000000000000package imap import ( "errors" "fmt" "strings" "sync" "github.com/emersion/go-imap/utf7" ) // The primary mailbox, as defined in RFC 3501 section 5.1. const InboxName = "INBOX" // CanonicalMailboxName returns the canonical form of a mailbox name. Mailbox names can be // case-sensitive or case-insensitive depending on the backend implementation. // The special INBOX mailbox is case-insensitive. func CanonicalMailboxName(name string) string { if strings.ToUpper(name) == InboxName { return InboxName } return name } // Mailbox attributes definied in RFC 3501 section 7.2.2. const ( // It is not possible for any child levels of hierarchy to exist under this\ // name; no child levels exist now and none can be created in the future. NoInferiorsAttr = "\\Noinferiors" // It is not possible to use this name as a selectable mailbox. NoSelectAttr = "\\Noselect" // The mailbox has been marked "interesting" by the server; the mailbox // probably contains messages that have been added since the last time the // mailbox was selected. MarkedAttr = "\\Marked" // The mailbox does not contain any additional messages since the last time // the mailbox was selected. UnmarkedAttr = "\\Unmarked" ) // Mailbox attributes defined in RFC 6154 section 2 (SPECIAL-USE extension). const ( // This mailbox presents all messages in the user's message store. AllAttr = "\\All" // This mailbox is used to archive messages. ArchiveAttr = "\\Archive" // This mailbox is used to hold draft messages -- typically, messages that are // being composed but have not yet been sent. DraftsAttr = "\\Drafts" // This mailbox presents all messages marked in some way as "important". FlaggedAttr = "\\Flagged" // This mailbox is where messages deemed to be junk mail are held. JunkAttr = "\\Junk" // This mailbox is used to hold copies of messages that have been sent. SentAttr = "\\Sent" // This mailbox is used to hold messages that have been deleted or marked for // deletion. TrashAttr = "\\Trash" ) // Mailbox attributes defined in RFC 3348 (CHILDREN extension) const ( // The presence of this attribute indicates that the mailbox has child // mailboxes. HasChildrenAttr = "\\HasChildren" // The presence of this attribute indicates that the mailbox has no child // mailboxes. HasNoChildrenAttr = "\\HasNoChildren" ) // This mailbox attribute is a signal that the mailbox contains messages that // are likely important to the user. This attribute is defined in RFC 8457 // section 3. const ImportantAttr = "\\Important" // Basic mailbox info. type MailboxInfo struct { // The mailbox attributes. Attributes []string // The server's path separator. Delimiter string // The mailbox name. Name string } // Parse mailbox info from fields. func (info *MailboxInfo) Parse(fields []interface{}) error { if len(fields) < 3 { return errors.New("Mailbox info needs at least 3 fields") } var err error if info.Attributes, err = ParseStringList(fields[0]); err != nil { return err } var ok bool if info.Delimiter, ok = fields[1].(string); !ok { // The delimiter may be specified as NIL, which gets converted to a nil interface. if fields[1] != nil { return errors.New("Mailbox delimiter must be a string") } info.Delimiter = "" } if name, err := ParseString(fields[2]); err != nil { return err } else if name, err := utf7.Encoding.NewDecoder().String(name); err != nil { return err } else { info.Name = CanonicalMailboxName(name) } return nil } // Format mailbox info to fields. func (info *MailboxInfo) Format() []interface{} { name, _ := utf7.Encoding.NewEncoder().String(info.Name) attrs := make([]interface{}, len(info.Attributes)) for i, attr := range info.Attributes { attrs[i] = RawString(attr) } // If the delimiter is NIL, we need to treat it specially by inserting // a nil field (so that it's later converted to an unquoted NIL atom). var del interface{} if info.Delimiter != "" { del = info.Delimiter } // Thunderbird doesn't understand delimiters if not quoted return []interface{}{attrs, del, FormatMailboxName(name)} } // TODO: optimize this func (info *MailboxInfo) match(name, pattern string) bool { i := strings.IndexAny(pattern, "*%") if i == -1 { // No more wildcards return name == pattern } // Get parts before and after wildcard chunk, wildcard, rest := pattern[0:i], pattern[i], pattern[i+1:] // Check that name begins with chunk if len(chunk) > 0 && !strings.HasPrefix(name, chunk) { return false } name = strings.TrimPrefix(name, chunk) // Expand wildcard var j int for j = 0; j < len(name); j++ { if wildcard == '%' && string(name[j]) == info.Delimiter { break // Stop on delimiter if wildcard is % } // Try to match the rest from here if info.match(name[j:], rest) { return true } } return info.match(name[j:], rest) } // Match checks if a reference and a pattern matches this mailbox name, as // defined in RFC 3501 section 6.3.8. func (info *MailboxInfo) Match(reference, pattern string) bool { name := info.Name if info.Delimiter != "" && strings.HasPrefix(pattern, info.Delimiter) { reference = "" pattern = strings.TrimPrefix(pattern, info.Delimiter) } if reference != "" { if info.Delimiter != "" && !strings.HasSuffix(reference, info.Delimiter) { reference += info.Delimiter } if !strings.HasPrefix(name, reference) { return false } name = strings.TrimPrefix(name, reference) } return info.match(name, pattern) } // A mailbox status. type MailboxStatus struct { // The mailbox name. Name string // True if the mailbox is open in read-only mode. ReadOnly bool // The mailbox items that are currently filled in. This map's values // should not be used directly, they must only be used by libraries // implementing extensions of the IMAP protocol. Items map[StatusItem]interface{} // The Items map may be accessed in different goroutines. Protect // concurrent writes. ItemsLocker sync.Mutex // The mailbox flags. Flags []string // The mailbox permanent flags. PermanentFlags []string // The sequence number of the first unseen message in the mailbox. UnseenSeqNum uint32 // The number of messages in this mailbox. Messages uint32 // The number of messages not seen since the last time the mailbox was opened. Recent uint32 // The number of unread messages. Unseen uint32 // The next UID. UidNext uint32 // Together with a UID, it is a unique identifier for a message. // Must be greater than or equal to 1. UidValidity uint32 // Per-mailbox limit of message size. Set only if server supports the // APPENDLIMIT extension. AppendLimit uint32 } // Create a new mailbox status that will contain the specified items. func NewMailboxStatus(name string, items []StatusItem) *MailboxStatus { status := &MailboxStatus{ Name: name, Items: make(map[StatusItem]interface{}), } for _, k := range items { status.Items[k] = nil } return status } func (status *MailboxStatus) Parse(fields []interface{}) error { status.Items = make(map[StatusItem]interface{}) var k StatusItem for i, f := range fields { if i%2 == 0 { if kstr, ok := f.(string); !ok { return fmt.Errorf("cannot parse mailbox status: key is not a string, but a %T", f) } else { k = StatusItem(strings.ToUpper(kstr)) } } else { status.Items[k] = nil var err error switch k { case StatusMessages: status.Messages, err = ParseNumber(f) case StatusRecent: status.Recent, err = ParseNumber(f) case StatusUnseen: status.Unseen, err = ParseNumber(f) case StatusUidNext: status.UidNext, err = ParseNumber(f) case StatusUidValidity: status.UidValidity, err = ParseNumber(f) case StatusAppendLimit: status.AppendLimit, err = ParseNumber(f) default: status.Items[k] = f } if err != nil { return err } } } return nil } func (status *MailboxStatus) Format() []interface{} { var fields []interface{} for k, v := range status.Items { switch k { case StatusMessages: v = status.Messages case StatusRecent: v = status.Recent case StatusUnseen: v = status.Unseen case StatusUidNext: v = status.UidNext case StatusUidValidity: v = status.UidValidity case StatusAppendLimit: v = status.AppendLimit } fields = append(fields, RawString(k), v) } return fields } func FormatMailboxName(name string) interface{} { // Some e-mails servers don't handle quoted INBOX names correctly so we special-case it. if strings.EqualFold(name, "INBOX") { return RawString(name) } return name } go-imap-1.2.0/mailbox_test.go000066400000000000000000000142561412725504300160770ustar00rootroot00000000000000package imap_test import ( "fmt" "reflect" "sort" "testing" "github.com/emersion/go-imap" "github.com/emersion/go-imap/internal" ) func TestCanonicalMailboxName(t *testing.T) { if got := imap.CanonicalMailboxName("Inbox"); got != imap.InboxName { t.Errorf("Invalid canonical mailbox name: expected %q but got %q", imap.InboxName, got) } if got := imap.CanonicalMailboxName("Drafts"); got != "Drafts" { t.Errorf("Invalid canonical mailbox name: expected %q but got %q", "Drafts", got) } } var mailboxInfoTests = []struct { fields []interface{} info *imap.MailboxInfo }{ { fields: []interface{}{ []interface{}{"\\Noselect", "\\Recent", "\\Unseen"}, "/", "INBOX", }, info: &imap.MailboxInfo{ Attributes: []string{"\\Noselect", "\\Recent", "\\Unseen"}, Delimiter: "/", Name: "INBOX", }, }, } func TestMailboxInfo_Parse(t *testing.T) { for _, test := range mailboxInfoTests { info := &imap.MailboxInfo{} if err := info.Parse(test.fields); err != nil { t.Fatal(err) } if fmt.Sprint(info.Attributes) != fmt.Sprint(test.info.Attributes) { t.Fatal("Invalid flags:", info.Attributes) } if info.Delimiter != test.info.Delimiter { t.Fatal("Invalid delimiter:", info.Delimiter) } if info.Name != test.info.Name { t.Fatal("Invalid name:", info.Name) } } } func TestMailboxInfo_Format(t *testing.T) { for _, test := range mailboxInfoTests { fields := test.info.Format() if fmt.Sprint(fields) != fmt.Sprint(test.fields) { t.Fatal("Invalid fields:", fields) } } } var mailboxInfoMatchTests = []struct { name, ref, pattern string result bool }{ {name: "INBOX", pattern: "INBOX", result: true}, {name: "INBOX", pattern: "Asuka", result: false}, {name: "INBOX", pattern: "*", result: true}, {name: "INBOX", pattern: "%", result: true}, {name: "Neon Genesis Evangelion/Misato", pattern: "*", result: true}, {name: "Neon Genesis Evangelion/Misato", pattern: "%", result: false}, {name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/*", result: true}, {name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/%", result: true}, {name: "Neon Genesis Evangelion/Misato", pattern: "Neo* Evangelion/Misato", result: true}, {name: "Neon Genesis Evangelion/Misato", pattern: "Neo% Evangelion/Misato", result: true}, {name: "Neon Genesis Evangelion/Misato", pattern: "*Eva*/Misato", result: true}, {name: "Neon Genesis Evangelion/Misato", pattern: "%Eva%/Misato", result: true}, {name: "Neon Genesis Evangelion/Misato", pattern: "*X*/Misato", result: false}, {name: "Neon Genesis Evangelion/Misato", pattern: "%X%/Misato", result: false}, {name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/Mi%o", result: true}, {name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/Mi%too", result: false}, {name: "Misato/Misato", pattern: "Mis*to/Misato", result: true}, {name: "Misato/Misato", pattern: "Mis*to", result: true}, {name: "Misato/Misato/Misato", pattern: "Mis*to/Mis%to", result: true}, {name: "Misato/Misato", pattern: "Mis**to/Misato", result: true}, {name: "Misato/Misato", pattern: "Misat%/Misato", result: true}, {name: "Misato/Misato", pattern: "Misat%Misato", result: false}, {name: "Misato/Misato", ref: "Misato", pattern: "Misato", result: true}, {name: "Misato/Misato", ref: "Misato/", pattern: "Misato", result: true}, {name: "Misato/Misato", ref: "Shinji", pattern: "/Misato/*", result: true}, {name: "Misato/Misato", ref: "Misato", pattern: "/Misato", result: false}, {name: "Misato/Misato", ref: "Misato", pattern: "Shinji", result: false}, {name: "Misato/Misato", ref: "Shinji", pattern: "Misato", result: false}, } func TestMailboxInfo_Match(t *testing.T) { for _, test := range mailboxInfoMatchTests { info := &imap.MailboxInfo{Name: test.name, Delimiter: "/"} result := info.Match(test.ref, test.pattern) if result != test.result { 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) } } } func TestNewMailboxStatus(t *testing.T) { status := imap.NewMailboxStatus("INBOX", []imap.StatusItem{imap.StatusMessages, imap.StatusUnseen}) expected := &imap.MailboxStatus{ Name: "INBOX", Items: map[imap.StatusItem]interface{}{imap.StatusMessages: nil, imap.StatusUnseen: nil}, } if !reflect.DeepEqual(expected, status) { t.Errorf("Invalid mailbox status: expected \n%+v\n but got \n%+v", expected, status) } } var mailboxStatusTests = [...]struct { fields []interface{} status *imap.MailboxStatus }{ { fields: []interface{}{ "MESSAGES", uint32(42), "RECENT", uint32(1), "UNSEEN", uint32(6), "UIDNEXT", uint32(65536), "UIDVALIDITY", uint32(4242), }, status: &imap.MailboxStatus{ Items: map[imap.StatusItem]interface{}{ imap.StatusMessages: nil, imap.StatusRecent: nil, imap.StatusUnseen: nil, imap.StatusUidNext: nil, imap.StatusUidValidity: nil, }, Messages: 42, Recent: 1, Unseen: 6, UidNext: 65536, UidValidity: 4242, }, }, } func TestMailboxStatus_Parse(t *testing.T) { for i, test := range mailboxStatusTests { status := &imap.MailboxStatus{} if err := status.Parse(test.fields); err != nil { t.Errorf("Expected no error while parsing mailbox status #%v, got: %v", i, err) continue } if !reflect.DeepEqual(status, test.status) { t.Errorf("Invalid parsed mailbox status for #%v: got \n%+v\n but expected \n%+v", i, status, test.status) } } } func TestMailboxStatus_Format(t *testing.T) { for i, test := range mailboxStatusTests { fields := test.status.Format() // MapListSorter does not know about RawString and will panic. stringFields := make([]interface{}, 0, len(fields)) for _, field := range fields { if s, ok := field.(imap.RawString); ok { stringFields = append(stringFields, string(s)) } else { stringFields = append(stringFields, field) } } sort.Sort(internal.MapListSorter(stringFields)) sort.Sort(internal.MapListSorter(test.fields)) if !reflect.DeepEqual(stringFields, test.fields) { t.Errorf("Invalid mailbox status fields for #%v: got \n%+v\n but expected \n%+v", i, fields, test.fields) } } } go-imap-1.2.0/message.go000066400000000000000000000671651412725504300150400ustar00rootroot00000000000000package imap import ( "bytes" "errors" "fmt" "io" "mime" "strconv" "strings" "time" ) // System message flags, defined in RFC 3501 section 2.3.2. const ( SeenFlag = "\\Seen" AnsweredFlag = "\\Answered" FlaggedFlag = "\\Flagged" DeletedFlag = "\\Deleted" DraftFlag = "\\Draft" RecentFlag = "\\Recent" ) // ImportantFlag is a message flag to signal that a message is likely important // to the user. This flag is defined in RFC 8457 section 2. const ImportantFlag = "$Important" // TryCreateFlag is a special flag in MailboxStatus.PermanentFlags indicating // that it is possible to create new keywords by attempting to store those // flags in the mailbox. const TryCreateFlag = "\\*" var flags = []string{ SeenFlag, AnsweredFlag, FlaggedFlag, DeletedFlag, DraftFlag, RecentFlag, } // A PartSpecifier specifies which parts of the MIME entity should be returned. type PartSpecifier string // Part specifiers described in RFC 3501 page 55. const ( // Refers to the entire part, including headers. EntireSpecifier PartSpecifier = "" // Refers to the header of the part. Must include the final CRLF delimiting // the header and the body. HeaderSpecifier = "HEADER" // Refers to the text body of the part, omitting the header. TextSpecifier = "TEXT" // Refers to the MIME Internet Message Body header. Must include the final // CRLF delimiting the header and the body. MIMESpecifier = "MIME" ) // CanonicalFlag returns the canonical form of a flag. Flags are case-insensitive. // // If the flag is defined in RFC 3501, it returns the flag with the case of the // RFC. Otherwise, it returns the lowercase version of the flag. func CanonicalFlag(flag string) string { flag = strings.ToLower(flag) for _, f := range flags { if strings.ToLower(f) == flag { return f } } return flag } func ParseParamList(fields []interface{}) (map[string]string, error) { params := make(map[string]string) var k string for i, f := range fields { p, err := ParseString(f) if err != nil { return nil, errors.New("Parameter list contains a non-string: " + err.Error()) } if i%2 == 0 { k = p } else { params[k] = p k = "" } } if k != "" { return nil, errors.New("Parameter list contains a key without a value") } return params, nil } func FormatParamList(params map[string]string) []interface{} { var fields []interface{} for key, value := range params { fields = append(fields, key, value) } return fields } var wordDecoder = &mime.WordDecoder{ CharsetReader: func(charset string, input io.Reader) (io.Reader, error) { if CharsetReader != nil { return CharsetReader(charset, input) } return nil, fmt.Errorf("imap: unhandled charset %q", charset) }, } func decodeHeader(s string) (string, error) { dec, err := wordDecoder.DecodeHeader(s) if err != nil { return s, err } return dec, nil } func encodeHeader(s string) string { return mime.QEncoding.Encode("utf-8", s) } func stringLowered(i interface{}) (string, bool) { s, ok := i.(string) return strings.ToLower(s), ok } func parseHeaderParamList(fields []interface{}) (map[string]string, error) { params, err := ParseParamList(fields) if err != nil { return nil, err } for k, v := range params { if lower := strings.ToLower(k); lower != k { delete(params, k) k = lower } params[k], _ = decodeHeader(v) } return params, nil } func formatHeaderParamList(params map[string]string) []interface{} { encoded := make(map[string]string) for k, v := range params { encoded[k] = encodeHeader(v) } return FormatParamList(encoded) } // A message. type Message struct { // The message sequence number. It must be greater than or equal to 1. SeqNum uint32 // The mailbox items that are currently filled in. This map's values // should not be used directly, they must only be used by libraries // implementing extensions of the IMAP protocol. Items map[FetchItem]interface{} // The message envelope. Envelope *Envelope // The message body structure (either BODYSTRUCTURE or BODY). BodyStructure *BodyStructure // The message flags. Flags []string // The date the message was received by the server. InternalDate time.Time // The message size. Size uint32 // The message unique identifier. It must be greater than or equal to 1. Uid uint32 // The message body sections. Body map[*BodySectionName]Literal // The order in which items were requested. This order must be preserved // because some bad IMAP clients (looking at you, Outlook!) refuse responses // containing items in a different order. itemsOrder []FetchItem } // Create a new empty message that will contain the specified items. func NewMessage(seqNum uint32, items []FetchItem) *Message { msg := &Message{ SeqNum: seqNum, Items: make(map[FetchItem]interface{}), Body: make(map[*BodySectionName]Literal), itemsOrder: items, } for _, k := range items { msg.Items[k] = nil } return msg } // Parse a message from fields. func (m *Message) Parse(fields []interface{}) error { m.Items = make(map[FetchItem]interface{}) m.Body = map[*BodySectionName]Literal{} m.itemsOrder = nil var k FetchItem for i, f := range fields { if i%2 == 0 { // It's a key switch f := f.(type) { case string: k = FetchItem(strings.ToUpper(f)) case RawString: k = FetchItem(strings.ToUpper(string(f))) default: return fmt.Errorf("cannot parse message: key is not a string, but a %T", f) } } else { // It's a value m.Items[k] = nil m.itemsOrder = append(m.itemsOrder, k) switch k { case FetchBody, FetchBodyStructure: bs, ok := f.([]interface{}) if !ok { return fmt.Errorf("cannot parse message: BODYSTRUCTURE is not a list, but a %T", f) } m.BodyStructure = &BodyStructure{Extended: k == FetchBodyStructure} if err := m.BodyStructure.Parse(bs); err != nil { return err } case FetchEnvelope: env, ok := f.([]interface{}) if !ok { return fmt.Errorf("cannot parse message: ENVELOPE is not a list, but a %T", f) } m.Envelope = &Envelope{} if err := m.Envelope.Parse(env); err != nil { return err } case FetchFlags: flags, ok := f.([]interface{}) if !ok { return fmt.Errorf("cannot parse message: FLAGS is not a list, but a %T", f) } m.Flags = make([]string, len(flags)) for i, flag := range flags { s, _ := ParseString(flag) m.Flags[i] = CanonicalFlag(s) } case FetchInternalDate: date, _ := f.(string) m.InternalDate, _ = time.Parse(DateTimeLayout, date) case FetchRFC822Size: m.Size, _ = ParseNumber(f) case FetchUid: m.Uid, _ = ParseNumber(f) default: // Likely to be a section of the body // First check that the section name is correct if section, err := ParseBodySectionName(k); err != nil { // Not a section name, maybe an attribute defined in an IMAP extension m.Items[k] = f } else { m.Body[section], _ = f.(Literal) } } } } return nil } func (m *Message) formatItem(k FetchItem) []interface{} { v := m.Items[k] var kk interface{} = RawString(k) switch k { case FetchBody, FetchBodyStructure: // Extension data is only returned with the BODYSTRUCTURE fetch m.BodyStructure.Extended = k == FetchBodyStructure v = m.BodyStructure.Format() case FetchEnvelope: v = m.Envelope.Format() case FetchFlags: flags := make([]interface{}, len(m.Flags)) for i, flag := range m.Flags { flags[i] = RawString(flag) } v = flags case FetchInternalDate: v = m.InternalDate case FetchRFC822Size: v = m.Size case FetchUid: v = m.Uid default: for section, literal := range m.Body { if section.value == k { // This can contain spaces, so we can't pass it as a string directly kk = section.resp() v = literal break } } } return []interface{}{kk, v} } func (m *Message) Format() []interface{} { var fields []interface{} // First send ordered items processed := make(map[FetchItem]bool) for _, k := range m.itemsOrder { if _, ok := m.Items[k]; ok { fields = append(fields, m.formatItem(k)...) processed[k] = true } } // Then send other remaining items for k := range m.Items { if !processed[k] { fields = append(fields, m.formatItem(k)...) } } return fields } // GetBody gets the body section with the specified name. Returns nil if it's not found. func (m *Message) GetBody(section *BodySectionName) Literal { section = section.resp() for s, body := range m.Body { if section.Equal(s) { if body == nil { // Server can return nil, we need to treat as empty string per RFC 3501 body = bytes.NewReader(nil) } return body } } return nil } // A body section name. // See RFC 3501 page 55. type BodySectionName struct { BodyPartName // If set to true, do not implicitly set the \Seen flag. Peek bool // The substring of the section requested. The first value is the position of // the first desired octet and the second value is the maximum number of // octets desired. Partial []int value FetchItem } func (section *BodySectionName) parse(s string) error { section.value = FetchItem(s) if s == "RFC822" { s = "BODY[]" } if s == "RFC822.HEADER" { s = "BODY.PEEK[HEADER]" } if s == "RFC822.TEXT" { s = "BODY[TEXT]" } partStart := strings.Index(s, "[") if partStart == -1 { return errors.New("Invalid body section name: must contain an open bracket") } partEnd := strings.LastIndex(s, "]") if partEnd == -1 { return errors.New("Invalid body section name: must contain a close bracket") } name := s[:partStart] part := s[partStart+1 : partEnd] partial := s[partEnd+1:] if name == "BODY.PEEK" { section.Peek = true } else if name != "BODY" { return errors.New("Invalid body section name") } b := bytes.NewBufferString(part + string(cr) + string(lf)) r := NewReader(b) fields, err := r.ReadFields() if err != nil { return err } if err := section.BodyPartName.parse(fields); err != nil { return err } if len(partial) > 0 { if !strings.HasPrefix(partial, "<") || !strings.HasSuffix(partial, ">") { return errors.New("Invalid body section name: invalid partial") } partial = partial[1 : len(partial)-1] partialParts := strings.SplitN(partial, ".", 2) var from, length int if from, err = strconv.Atoi(partialParts[0]); err != nil { return errors.New("Invalid body section name: invalid partial: invalid from: " + err.Error()) } section.Partial = []int{from} if len(partialParts) == 2 { if length, err = strconv.Atoi(partialParts[1]); err != nil { return errors.New("Invalid body section name: invalid partial: invalid length: " + err.Error()) } section.Partial = append(section.Partial, length) } } return nil } func (section *BodySectionName) FetchItem() FetchItem { if section.value != "" { return section.value } s := "BODY" if section.Peek { s += ".PEEK" } s += "[" + section.BodyPartName.string() + "]" if len(section.Partial) > 0 { s += "<" s += strconv.Itoa(section.Partial[0]) if len(section.Partial) > 1 { s += "." s += strconv.Itoa(section.Partial[1]) } s += ">" } return FetchItem(s) } // Equal checks whether two sections are equal. func (section *BodySectionName) Equal(other *BodySectionName) bool { if section.Peek != other.Peek { return false } if len(section.Partial) != len(other.Partial) { return false } if len(section.Partial) > 0 && section.Partial[0] != other.Partial[0] { return false } if len(section.Partial) > 1 && section.Partial[1] != other.Partial[1] { return false } return section.BodyPartName.Equal(&other.BodyPartName) } func (section *BodySectionName) resp() *BodySectionName { resp := *section // Copy section if resp.Peek { resp.Peek = false } if len(resp.Partial) == 2 { resp.Partial = []int{resp.Partial[0]} } if !strings.HasPrefix(string(resp.value), string(FetchRFC822)) { resp.value = "" } return &resp } // ExtractPartial returns a subset of the specified bytes matching the partial requested in the // section name. func (section *BodySectionName) ExtractPartial(b []byte) []byte { if len(section.Partial) != 2 { return b } from := section.Partial[0] length := section.Partial[1] to := from + length if from > len(b) { return nil } if to > len(b) { to = len(b) } return b[from:to] } // ParseBodySectionName parses a body section name. func ParseBodySectionName(s FetchItem) (*BodySectionName, error) { section := new(BodySectionName) err := section.parse(string(s)) return section, err } // A body part name. type BodyPartName struct { // The specifier of the requested part. Specifier PartSpecifier // The part path. Parts indexes start at 1. Path []int // If Specifier is HEADER, contains header fields that will/won't be returned, // depending of the value of NotFields. Fields []string // If set to true, Fields is a blacklist of fields instead of a whitelist. NotFields bool } func (part *BodyPartName) parse(fields []interface{}) error { if len(fields) == 0 { return nil } name, ok := fields[0].(string) if !ok { return errors.New("Invalid body section name: part name must be a string") } args := fields[1:] path := strings.Split(strings.ToUpper(name), ".") end := 0 loop: for i, node := range path { switch PartSpecifier(node) { case EntireSpecifier, HeaderSpecifier, MIMESpecifier, TextSpecifier: part.Specifier = PartSpecifier(node) end = i + 1 break loop } index, err := strconv.Atoi(node) if err != nil { return errors.New("Invalid body part name: " + err.Error()) } if index <= 0 { return errors.New("Invalid body part name: index <= 0") } part.Path = append(part.Path, index) } if part.Specifier == HeaderSpecifier && len(path) > end && path[end] == "FIELDS" && len(args) > 0 { end++ if len(path) > end && path[end] == "NOT" { part.NotFields = true } names, ok := args[0].([]interface{}) if !ok { return errors.New("Invalid body part name: HEADER.FIELDS must have a list argument") } for _, namei := range names { if name, ok := namei.(string); ok { part.Fields = append(part.Fields, name) } } } return nil } func (part *BodyPartName) string() string { path := make([]string, len(part.Path)) for i, index := range part.Path { path[i] = strconv.Itoa(index) } if part.Specifier != EntireSpecifier { path = append(path, string(part.Specifier)) } if part.Specifier == HeaderSpecifier && len(part.Fields) > 0 { path = append(path, "FIELDS") if part.NotFields { path = append(path, "NOT") } } s := strings.Join(path, ".") if len(part.Fields) > 0 { s += " (" + strings.Join(part.Fields, " ") + ")" } return s } // Equal checks whether two body part names are equal. func (part *BodyPartName) Equal(other *BodyPartName) bool { if part.Specifier != other.Specifier { return false } if part.NotFields != other.NotFields { return false } if len(part.Path) != len(other.Path) { return false } for i, node := range part.Path { if node != other.Path[i] { return false } } if len(part.Fields) != len(other.Fields) { return false } for _, field := range part.Fields { found := false for _, f := range other.Fields { if strings.EqualFold(field, f) { found = true break } } if !found { return false } } return true } // An address. type Address struct { // The personal name. PersonalName string // The SMTP at-domain-list (source route). AtDomainList string // The mailbox name. MailboxName string // The host name. HostName string } // Address returns the mailbox address (e.g. "foo@example.org"). func (addr *Address) Address() string { return addr.MailboxName + "@" + addr.HostName } // Parse an address from fields. func (addr *Address) Parse(fields []interface{}) error { if len(fields) < 4 { return errors.New("Address doesn't contain 4 fields") } if s, err := ParseString(fields[0]); err == nil { addr.PersonalName, _ = decodeHeader(s) } if s, err := ParseString(fields[1]); err == nil { addr.AtDomainList, _ = decodeHeader(s) } s, err := ParseString(fields[2]) if err != nil { return errors.New("Mailbox name could not be parsed") } addr.MailboxName, _ = decodeHeader(s) s, err = ParseString(fields[3]) if err != nil { return errors.New("Host name could not be parsed") } addr.HostName, _ = decodeHeader(s) return nil } // Format an address to fields. func (addr *Address) Format() []interface{} { fields := make([]interface{}, 4) if addr.PersonalName != "" { fields[0] = encodeHeader(addr.PersonalName) } if addr.AtDomainList != "" { fields[1] = addr.AtDomainList } if addr.MailboxName != "" { fields[2] = addr.MailboxName } if addr.HostName != "" { fields[3] = addr.HostName } return fields } // Parse an address list from fields. func ParseAddressList(fields []interface{}) (addrs []*Address) { for _, f := range fields { if addrFields, ok := f.([]interface{}); ok { addr := &Address{} if err := addr.Parse(addrFields); err == nil { addrs = append(addrs, addr) } } } return } // Format an address list to fields. func FormatAddressList(addrs []*Address) interface{} { if len(addrs) == 0 { return nil } fields := make([]interface{}, len(addrs)) for i, addr := range addrs { fields[i] = addr.Format() } return fields } // A message envelope, ie. message metadata from its headers. // See RFC 3501 page 77. type Envelope struct { // The message date. Date time.Time // The message subject. Subject string // The From header addresses. From []*Address // The message senders. Sender []*Address // The Reply-To header addresses. ReplyTo []*Address // The To header addresses. To []*Address // The Cc header addresses. Cc []*Address // The Bcc header addresses. Bcc []*Address // The In-Reply-To header. Contains the parent Message-Id. InReplyTo string // The Message-Id header. MessageId string } // Parse an envelope from fields. func (e *Envelope) Parse(fields []interface{}) error { if len(fields) < 10 { return errors.New("ENVELOPE doesn't contain 10 fields") } if date, ok := fields[0].(string); ok { e.Date, _ = parseMessageDateTime(date) } if subject, err := ParseString(fields[1]); err == nil { e.Subject, _ = decodeHeader(subject) } if from, ok := fields[2].([]interface{}); ok { e.From = ParseAddressList(from) } if sender, ok := fields[3].([]interface{}); ok { e.Sender = ParseAddressList(sender) } if replyTo, ok := fields[4].([]interface{}); ok { e.ReplyTo = ParseAddressList(replyTo) } if to, ok := fields[5].([]interface{}); ok { e.To = ParseAddressList(to) } if cc, ok := fields[6].([]interface{}); ok { e.Cc = ParseAddressList(cc) } if bcc, ok := fields[7].([]interface{}); ok { e.Bcc = ParseAddressList(bcc) } if inReplyTo, ok := fields[8].(string); ok { e.InReplyTo = inReplyTo } if msgId, ok := fields[9].(string); ok { e.MessageId = msgId } return nil } // Format an envelope to fields. func (e *Envelope) Format() (fields []interface{}) { fields = make([]interface{}, 0, 10) fields = append(fields, envelopeDateTime(e.Date)) if e.Subject != "" { fields = append(fields, encodeHeader(e.Subject)) } else { fields = append(fields, nil) } fields = append(fields, FormatAddressList(e.From), FormatAddressList(e.Sender), FormatAddressList(e.ReplyTo), FormatAddressList(e.To), FormatAddressList(e.Cc), FormatAddressList(e.Bcc), ) if e.InReplyTo != "" { fields = append(fields, e.InReplyTo) } else { fields = append(fields, nil) } if e.MessageId != "" { fields = append(fields, e.MessageId) } else { fields = append(fields, nil) } return fields } // A body structure. // See RFC 3501 page 74. type BodyStructure struct { // Basic fields // The MIME type (e.g. "text", "image") MIMEType string // The MIME subtype (e.g. "plain", "png") MIMESubType string // The MIME parameters. Params map[string]string // The Content-Id header. Id string // The Content-Description header. Description string // The Content-Encoding header. Encoding string // The Content-Length header. Size uint32 // Type-specific fields // The children parts, if multipart. Parts []*BodyStructure // The envelope, if message/rfc822. Envelope *Envelope // The body structure, if message/rfc822. BodyStructure *BodyStructure // The number of lines, if text or message/rfc822. Lines uint32 // Extension data // True if the body structure contains extension data. Extended bool // The Content-Disposition header field value. Disposition string // The Content-Disposition header field parameters. DispositionParams map[string]string // The Content-Language header field, if multipart. Language []string // The content URI, if multipart. Location []string // The MD5 checksum. MD5 string } func (bs *BodyStructure) Parse(fields []interface{}) error { if len(fields) == 0 { return nil } // Initialize params map bs.Params = make(map[string]string) switch fields[0].(type) { case []interface{}: // A multipart body part bs.MIMEType = "multipart" end := 0 for i, fi := range fields { switch f := fi.(type) { case []interface{}: // A part part := new(BodyStructure) if err := part.Parse(f); err != nil { return err } bs.Parts = append(bs.Parts, part) case string: end = i } if end > 0 { break } } bs.MIMESubType, _ = fields[end].(string) end++ // GMail seems to return only 3 extension data fields. Parse as many fields // as we can. if len(fields) > end { bs.Extended = true // Contains extension data params, _ := fields[end].([]interface{}) bs.Params, _ = parseHeaderParamList(params) end++ } if len(fields) > end { if disp, ok := fields[end].([]interface{}); ok && len(disp) >= 2 { if s, ok := disp[0].(string); ok { bs.Disposition, _ = decodeHeader(s) bs.Disposition = strings.ToLower(bs.Disposition) } if params, ok := disp[1].([]interface{}); ok { bs.DispositionParams, _ = parseHeaderParamList(params) } } end++ } if len(fields) > end { switch langs := fields[end].(type) { case string: bs.Language = []string{langs} case []interface{}: bs.Language, _ = ParseStringList(langs) default: bs.Language = nil } end++ } if len(fields) > end { location, _ := fields[end].([]interface{}) bs.Location, _ = ParseStringList(location) end++ } case string: // A non-multipart body part if len(fields) < 7 { return errors.New("Non-multipart body part doesn't have 7 fields") } bs.MIMEType, _ = stringLowered(fields[0]) bs.MIMESubType, _ = stringLowered(fields[1]) params, _ := fields[2].([]interface{}) bs.Params, _ = parseHeaderParamList(params) bs.Id, _ = fields[3].(string) if desc, err := ParseString(fields[4]); err == nil { bs.Description, _ = decodeHeader(desc) } bs.Encoding, _ = stringLowered(fields[5]) bs.Size, _ = ParseNumber(fields[6]) end := 7 // Type-specific fields if strings.EqualFold(bs.MIMEType, "message") && strings.EqualFold(bs.MIMESubType, "rfc822") { if len(fields)-end < 3 { return errors.New("Missing type-specific fields for message/rfc822") } envelope, _ := fields[end].([]interface{}) bs.Envelope = new(Envelope) bs.Envelope.Parse(envelope) structure, _ := fields[end+1].([]interface{}) bs.BodyStructure = new(BodyStructure) bs.BodyStructure.Parse(structure) bs.Lines, _ = ParseNumber(fields[end+2]) end += 3 } if strings.EqualFold(bs.MIMEType, "text") { if len(fields)-end < 1 { return errors.New("Missing type-specific fields for text/*") } bs.Lines, _ = ParseNumber(fields[end]) end++ } // GMail seems to return only 3 extension data fields. Parse as many fields // as we can. if len(fields) > end { bs.Extended = true // Contains extension data bs.MD5, _ = fields[end].(string) end++ } if len(fields) > end { if disp, ok := fields[end].([]interface{}); ok && len(disp) >= 2 { if s, ok := disp[0].(string); ok { bs.Disposition, _ = decodeHeader(s) bs.Disposition = strings.ToLower(bs.Disposition) } if params, ok := disp[1].([]interface{}); ok { bs.DispositionParams, _ = parseHeaderParamList(params) } } end++ } if len(fields) > end { switch langs := fields[end].(type) { case string: bs.Language = []string{langs} case []interface{}: bs.Language, _ = ParseStringList(langs) default: bs.Language = nil } end++ } if len(fields) > end { location, _ := fields[end].([]interface{}) bs.Location, _ = ParseStringList(location) end++ } } return nil } func (bs *BodyStructure) Format() (fields []interface{}) { if strings.EqualFold(bs.MIMEType, "multipart") { for _, part := range bs.Parts { fields = append(fields, part.Format()) } fields = append(fields, bs.MIMESubType) if bs.Extended { extended := make([]interface{}, 4) if bs.Params != nil { extended[0] = formatHeaderParamList(bs.Params) } if bs.Disposition != "" { extended[1] = []interface{}{ encodeHeader(bs.Disposition), formatHeaderParamList(bs.DispositionParams), } } if bs.Language != nil { extended[2] = FormatStringList(bs.Language) } if bs.Location != nil { extended[3] = FormatStringList(bs.Location) } fields = append(fields, extended...) } } else { fields = make([]interface{}, 7) fields[0] = bs.MIMEType fields[1] = bs.MIMESubType fields[2] = formatHeaderParamList(bs.Params) if bs.Id != "" { fields[3] = bs.Id } if bs.Description != "" { fields[4] = encodeHeader(bs.Description) } if bs.Encoding != "" { fields[5] = bs.Encoding } fields[6] = bs.Size // Type-specific fields if strings.EqualFold(bs.MIMEType, "message") && strings.EqualFold(bs.MIMESubType, "rfc822") { var env interface{} if bs.Envelope != nil { env = bs.Envelope.Format() } var bsbs interface{} if bs.BodyStructure != nil { bsbs = bs.BodyStructure.Format() } fields = append(fields, env, bsbs, bs.Lines) } if strings.EqualFold(bs.MIMEType, "text") { fields = append(fields, bs.Lines) } // Extension data if bs.Extended { extended := make([]interface{}, 4) if bs.MD5 != "" { extended[0] = bs.MD5 } if bs.Disposition != "" { extended[1] = []interface{}{ encodeHeader(bs.Disposition), formatHeaderParamList(bs.DispositionParams), } } if bs.Language != nil { extended[2] = FormatStringList(bs.Language) } if bs.Location != nil { extended[3] = FormatStringList(bs.Location) } fields = append(fields, extended...) } } return } // Filename parses the body structure's filename, if it's an attachment. An // empty string is returned if the filename isn't specified. An error is // returned if and only if a charset error occurs, in which case the undecoded // filename is returned too. func (bs *BodyStructure) Filename() (string, error) { raw, ok := bs.DispositionParams["filename"] if !ok { // Using "name" in Content-Type is discouraged raw = bs.Params["name"] } return decodeHeader(raw) } // BodyStructureWalkFunc is the type of the function called for each body // structure visited by BodyStructure.Walk. The path argument contains the IMAP // part path (see BodyPartName). // // The function should return true to visit all of the part's children or false // to skip them. type BodyStructureWalkFunc func(path []int, part *BodyStructure) (walkChildren bool) // Walk walks the body structure tree, calling f for each part in the tree, // including bs itself. The parts are visited in DFS pre-order. func (bs *BodyStructure) Walk(f BodyStructureWalkFunc) { // Non-multipart messages only have part 1 if len(bs.Parts) == 0 { f([]int{1}, bs) return } bs.walk(f, nil) } func (bs *BodyStructure) walk(f BodyStructureWalkFunc, path []int) { if !f(path, bs) { return } for i, part := range bs.Parts { num := i + 1 partPath := append([]int(nil), path...) partPath = append(partPath, num) part.walk(f, partPath) } } go-imap-1.2.0/message_test.go000066400000000000000000000475221412725504300160720ustar00rootroot00000000000000package imap import ( "bytes" "fmt" "reflect" "testing" "time" ) func TestCanonicalFlag(t *testing.T) { if got := CanonicalFlag("\\SEEN"); got != SeenFlag { t.Errorf("Invalid canonical flag: expected %q but got %q", SeenFlag, got) } if got := CanonicalFlag("Junk"); got != "junk" { t.Errorf("Invalid canonical flag: expected %q but got %q", "junk", got) } } func TestNewMessage(t *testing.T) { msg := NewMessage(42, []FetchItem{FetchBodyStructure, FetchFlags}) expected := &Message{ SeqNum: 42, Items: map[FetchItem]interface{}{FetchBodyStructure: nil, FetchFlags: nil}, Body: make(map[*BodySectionName]Literal), itemsOrder: []FetchItem{FetchBodyStructure, FetchFlags}, } if !reflect.DeepEqual(expected, msg) { t.Errorf("Invalid message: expected \n%+v\n but got \n%+v", expected, msg) } } func formatFields(fields []interface{}) (string, error) { b := &bytes.Buffer{} w := NewWriter(b) if err := w.writeList(fields); err != nil { return "", fmt.Errorf("Cannot format \n%+v\n got error: \n%v", fields, err) } return b.String(), nil } var messageTests = []struct { message *Message fields []interface{} }{ { message: &Message{ Items: map[FetchItem]interface{}{ FetchEnvelope: nil, FetchBody: nil, FetchFlags: nil, FetchRFC822Size: nil, FetchUid: nil, }, Body: map[*BodySectionName]Literal{}, Envelope: envelopeTests[0].envelope, BodyStructure: bodyStructureTests[0].bodyStructure, Flags: []string{SeenFlag, AnsweredFlag}, Size: 4242, Uid: 2424, itemsOrder: []FetchItem{FetchEnvelope, FetchBody, FetchFlags, FetchRFC822Size, FetchUid}, }, fields: []interface{}{ RawString("ENVELOPE"), envelopeTests[0].fields, RawString("BODY"), bodyStructureTests[0].fields, RawString("FLAGS"), []interface{}{RawString(SeenFlag), RawString(AnsweredFlag)}, RawString("RFC822.SIZE"), RawString("4242"), RawString("UID"), RawString("2424"), }, }, } func TestMessage_Parse(t *testing.T) { for i, test := range messageTests { m := &Message{} if err := m.Parse(test.fields); err != nil { t.Errorf("Cannot parse message for #%v: %v", i, err) } else if !reflect.DeepEqual(m, test.message) { t.Errorf("Invalid parsed message for #%v: got \n%+v\n but expected \n%+v", i, m, test.message) } } } func TestMessage_Format(t *testing.T) { for i, test := range messageTests { fields := test.message.Format() got, err := formatFields(fields) if err != nil { t.Error(err) continue } expected, _ := formatFields(test.fields) if got != expected { t.Errorf("Invalid message fields for #%v: got \n%v\n but expected \n%v", i, got, expected) } } } var bodySectionNameTests = []struct { raw string parsed *BodySectionName formatted string }{ { raw: "BODY[]", parsed: &BodySectionName{BodyPartName: BodyPartName{}}, }, { raw: "RFC822", parsed: &BodySectionName{BodyPartName: BodyPartName{}}, formatted: "BODY[]", }, { raw: "BODY[HEADER]", parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: HeaderSpecifier}}, }, { raw: "BODY.PEEK[]", parsed: &BodySectionName{BodyPartName: BodyPartName{}, Peek: true}, }, { raw: "BODY[TEXT]", parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: TextSpecifier}}, }, { raw: "RFC822.TEXT", parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: TextSpecifier}}, formatted: "BODY[TEXT]", }, { raw: "RFC822.HEADER", parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: HeaderSpecifier}, Peek: true}, formatted: "BODY.PEEK[HEADER]", }, { raw: "BODY[]<0.512>", parsed: &BodySectionName{BodyPartName: BodyPartName{}, Partial: []int{0, 512}}, }, { raw: "BODY[]<512>", parsed: &BodySectionName{BodyPartName: BodyPartName{}, Partial: []int{512}}, }, { raw: "BODY[1.2.3]", parsed: &BodySectionName{BodyPartName: BodyPartName{Path: []int{1, 2, 3}}}, }, { raw: "BODY[1.2.3.HEADER]", parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: HeaderSpecifier, Path: []int{1, 2, 3}}}, }, { raw: "BODY[5.MIME]", parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: MIMESpecifier, Path: []int{5}}}, }, { raw: "BODY[HEADER.FIELDS (From To)]", parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: HeaderSpecifier, Fields: []string{"From", "To"}}}, }, { raw: "BODY[HEADER.FIELDS.NOT (Content-Id)]", parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: HeaderSpecifier, Fields: []string{"Content-Id"}, NotFields: true}}, }, } func TestNewBodySectionName(t *testing.T) { for i, test := range bodySectionNameTests { bsn, err := ParseBodySectionName(FetchItem(test.raw)) if err != nil { t.Errorf("Cannot parse #%v: %v", i, err) continue } if !reflect.DeepEqual(bsn.BodyPartName, test.parsed.BodyPartName) { t.Errorf("Invalid body part name for #%v: %#+v", i, bsn.BodyPartName) } else if bsn.Peek != test.parsed.Peek { t.Errorf("Invalid peek value for #%v: %#+v", i, bsn.Peek) } else if !reflect.DeepEqual(bsn.Partial, test.parsed.Partial) { t.Errorf("Invalid partial for #%v: %#+v", i, bsn.Partial) } } } func TestBodySectionName_String(t *testing.T) { for i, test := range bodySectionNameTests { s := string(test.parsed.FetchItem()) expected := test.formatted if expected == "" { expected = test.raw } if expected != s { t.Errorf("Invalid body section name for #%v: got %v but expected %v", i, s, expected) } } } func TestBodySectionName_ExtractPartial(t *testing.T) { tests := []struct { bsn string whole string partial string }{ { bsn: "BODY[]", whole: "Hello World!", partial: "Hello World!", }, { bsn: "BODY[]<6.5>", whole: "Hello World!", partial: "World", }, { bsn: "BODY[]<6.1000>", whole: "Hello World!", partial: "World!", }, { bsn: "BODY[]<0.1>", whole: "Hello World!", partial: "H", }, { bsn: "BODY[]<1000.2000>", whole: "Hello World!", partial: "", }, } for i, test := range tests { bsn, err := ParseBodySectionName(FetchItem(test.bsn)) if err != nil { t.Errorf("Cannot parse body section name #%v: %v", i, err) continue } partial := string(bsn.ExtractPartial([]byte(test.whole))) if partial != test.partial { t.Errorf("Invalid partial for #%v: got %v but expected %v", i, partial, test.partial) } } } var t = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.FixedZone("", -6*60*60)) var envelopeTests = []struct { envelope *Envelope fields []interface{} }{ { envelope: &Envelope{ Date: t, Subject: "Hello World!", From: []*Address{addrTests[0].addr}, Sender: nil, ReplyTo: nil, To: nil, Cc: nil, Bcc: nil, InReplyTo: "42@example.org", MessageId: "43@example.org", }, fields: []interface{}{ "Tue, 10 Nov 2009 23:00:00 -0600", "Hello World!", []interface{}{addrTests[0].fields}, nil, nil, nil, nil, nil, "42@example.org", "43@example.org", }, }, } func TestEnvelope_Parse(t *testing.T) { for i, test := range envelopeTests { e := &Envelope{} if err := e.Parse(test.fields); err != nil { t.Error("Error parsing envelope:", err) } else if !reflect.DeepEqual(e, test.envelope) { t.Errorf("Invalid envelope for #%v: got %v but expected %v", i, e, test.envelope) } } } func TestEnvelope_Parse_literal(t *testing.T) { subject := "Hello World!" l := bytes.NewBufferString(subject) fields := []interface{}{ "Tue, 10 Nov 2009 23:00:00 -0600", l, nil, nil, nil, nil, nil, nil, "42@example.org", "43@example.org", } e := &Envelope{} if err := e.Parse(fields); err != nil { t.Error("Error parsing envelope:", err) } else if e.Subject != subject { t.Errorf("Invalid envelope subject: got %v but expected %v", e.Subject, subject) } } func TestEnvelope_Format(t *testing.T) { for i, test := range envelopeTests { fields := test.envelope.Format() got, err := formatFields(fields) if err != nil { t.Error(err) continue } expected, _ := formatFields(test.fields) if got != expected { t.Errorf("Invalid envelope fields for #%v: got %v but expected %v", i, got, expected) } } } var addrTests = []struct { fields []interface{} addr *Address }{ { fields: []interface{}{"The NSA", nil, "root", "nsa.gov"}, addr: &Address{ PersonalName: "The NSA", MailboxName: "root", HostName: "nsa.gov", }, }, } func TestAddress_Parse(t *testing.T) { for i, test := range addrTests { addr := &Address{} if err := addr.Parse(test.fields); err != nil { t.Error("Error parsing address:", err) } else if !reflect.DeepEqual(addr, test.addr) { t.Errorf("Invalid address for #%v: got %v but expected %v", i, addr, test.addr) } } } func TestAddress_Format(t *testing.T) { for i, test := range addrTests { fields := test.addr.Format() if !reflect.DeepEqual(fields, test.fields) { t.Errorf("Invalid address fields for #%v: got %v but expected %v", i, fields, test.fields) } } } func TestEmptyAddress(t *testing.T) { fields := []interface{}{nil, nil, nil, nil} addr := &Address{} err := addr.Parse(fields) if err == nil { t.Error("A nil address did not return an error") } } func TestEmptyGroupAddress(t *testing.T) { fields := []interface{}{nil, nil, "undisclosed-recipients", nil} addr := &Address{} err := addr.Parse(fields) if err == nil { t.Error("An empty group did not return an error when parsed as address") } } func TestAddressList(t *testing.T) { fields := make([]interface{}, len(addrTests)) addrs := make([]*Address, len(addrTests)) for i, test := range addrTests { fields[i] = test.fields addrs[i] = test.addr } gotAddrs := ParseAddressList(fields) if !reflect.DeepEqual(gotAddrs, addrs) { t.Error("Invalid address list: got", gotAddrs, "but expected", addrs) } gotFields := FormatAddressList(addrs) if !reflect.DeepEqual(gotFields, fields) { t.Error("Invalid address list fields: got", gotFields, "but expected", fields) } } func TestEmptyAddressList(t *testing.T) { addrs := make([]*Address, 0) gotFields := FormatAddressList(addrs) if !reflect.DeepEqual(gotFields, nil) { t.Error("Invalid address list fields: got", gotFields, "but expected nil") } } var paramsListTest = []struct { fields []interface{} params map[string]string }{ { fields: nil, params: map[string]string{}, }, { fields: []interface{}{"a", "b"}, params: map[string]string{"a": "b"}, }, } func TestParseParamList(t *testing.T) { for i, test := range paramsListTest { if params, err := ParseParamList(test.fields); err != nil { t.Errorf("Cannot parse params fields for #%v: %v", i, err) } else if !reflect.DeepEqual(params, test.params) { t.Errorf("Invalid params for #%v: got %v but expected %v", i, params, test.params) } } // Malformed params lists fields := []interface{}{"cc", []interface{}{"dille"}} if params, err := ParseParamList(fields); err == nil { t.Error("Parsed invalid params list:", params) } fields = []interface{}{"cc"} if params, err := ParseParamList(fields); err == nil { t.Error("Parsed invalid params list:", params) } } func TestFormatParamList(t *testing.T) { for i, test := range paramsListTest { fields := FormatParamList(test.params) if !reflect.DeepEqual(fields, test.fields) { t.Errorf("Invalid params fields for #%v: got %v but expected %v", i, fields, test.fields) } } } var bodyStructureTests = []struct { fields []interface{} bodyStructure *BodyStructure }{ { fields: []interface{}{"image", "jpeg", []interface{}{}, "", "A picture of cat", "base64", RawString("4242")}, bodyStructure: &BodyStructure{ MIMEType: "image", MIMESubType: "jpeg", Params: map[string]string{}, Id: "", Description: "A picture of cat", Encoding: "base64", Size: 4242, }, }, { fields: []interface{}{"text", "plain", []interface{}{"charset", "utf-8"}, nil, nil, "us-ascii", RawString("42"), RawString("2")}, bodyStructure: &BodyStructure{ MIMEType: "text", MIMESubType: "plain", Params: map[string]string{"charset": "utf-8"}, Encoding: "us-ascii", Size: 42, Lines: 2, }, }, { fields: []interface{}{ "message", "rfc822", []interface{}{}, nil, nil, "us-ascii", RawString("42"), (&Envelope{}).Format(), (&BodyStructure{}).Format(), RawString("67"), }, bodyStructure: &BodyStructure{ MIMEType: "message", MIMESubType: "rfc822", Params: map[string]string{}, Encoding: "us-ascii", Size: 42, Lines: 67, Envelope: &Envelope{ From: nil, Sender: nil, ReplyTo: nil, To: nil, Cc: nil, Bcc: nil, }, BodyStructure: &BodyStructure{ Params: map[string]string{}, }, }, }, { fields: []interface{}{ "application", "pdf", []interface{}{}, nil, nil, "base64", RawString("4242"), "e0323a9039add2978bf5b49550572c7c", []interface{}{"attachment", []interface{}{"filename", "document.pdf"}}, []interface{}{"en-US"}, []interface{}{}, }, bodyStructure: &BodyStructure{ MIMEType: "application", MIMESubType: "pdf", Params: map[string]string{}, Encoding: "base64", Size: 4242, Extended: true, MD5: "e0323a9039add2978bf5b49550572c7c", Disposition: "attachment", DispositionParams: map[string]string{"filename": "document.pdf"}, Language: []string{"en-US"}, Location: []string{}, }, }, { fields: []interface{}{ []interface{}{"text", "plain", []interface{}{}, nil, nil, "us-ascii", RawString("87"), RawString("22")}, []interface{}{"text", "html", []interface{}{}, nil, nil, "us-ascii", RawString("106"), RawString("36")}, "alternative", }, bodyStructure: &BodyStructure{ MIMEType: "multipart", MIMESubType: "alternative", Params: map[string]string{}, Parts: []*BodyStructure{ { MIMEType: "text", MIMESubType: "plain", Params: map[string]string{}, Encoding: "us-ascii", Size: 87, Lines: 22, }, { MIMEType: "text", MIMESubType: "html", Params: map[string]string{}, Encoding: "us-ascii", Size: 106, Lines: 36, }, }, }, }, { fields: []interface{}{ []interface{}{"text", "plain", []interface{}{}, nil, nil, "us-ascii", RawString("87"), RawString("22")}, "alternative", []interface{}{"hello", "world"}, []interface{}{"inline", []interface{}{}}, []interface{}{"en-US"}, []interface{}{}, }, bodyStructure: &BodyStructure{ MIMEType: "multipart", MIMESubType: "alternative", Params: map[string]string{"hello": "world"}, Parts: []*BodyStructure{ { MIMEType: "text", MIMESubType: "plain", Params: map[string]string{}, Encoding: "us-ascii", Size: 87, Lines: 22, }, }, Extended: true, Disposition: "inline", DispositionParams: map[string]string{}, Language: []string{"en-US"}, Location: []string{}, }, }, } func TestBodyStructure_Parse(t *testing.T) { for i, test := range bodyStructureTests { bs := &BodyStructure{} if err := bs.Parse(test.fields); err != nil { t.Errorf("Cannot parse #%v: %v", i, err) } else if !reflect.DeepEqual(bs, test.bodyStructure) { t.Errorf("Invalid body structure for #%v: got \n%+v\n but expected \n%+v", i, bs, test.bodyStructure) } } } func TestBodyStructure_Parse_uppercase(t *testing.T) { fields := []interface{}{ "APPLICATION", "PDF", []interface{}{"NAME", "Document.pdf"}, nil, nil, "BASE64", RawString("4242"), nil, []interface{}{"ATTACHMENT", []interface{}{"FILENAME", "Document.pdf"}}, nil, nil, } expected := &BodyStructure{ MIMEType: "application", MIMESubType: "pdf", Params: map[string]string{"name": "Document.pdf"}, Encoding: "base64", Size: 4242, Extended: true, MD5: "", Disposition: "attachment", DispositionParams: map[string]string{"filename": "Document.pdf"}, Language: nil, Location: []string{}, } bs := &BodyStructure{} if err := bs.Parse(fields); err != nil { t.Errorf("Cannot parse: %v", err) } else if !reflect.DeepEqual(bs, expected) { t.Errorf("Invalid body structure: got \n%+v\n but expected \n%+v", bs, expected) } } func TestBodyStructure_Format(t *testing.T) { for i, test := range bodyStructureTests { fields := test.bodyStructure.Format() got, err := formatFields(fields) if err != nil { t.Error(err) continue } expected, _ := formatFields(test.fields) if got != expected { t.Errorf("Invalid body structure fields for #%v: has \n%v\n but expected \n%v", i, got, expected) } } } func TestBodyStructureFilename(t *testing.T) { tests := []struct { bs BodyStructure filename string }{ { bs: BodyStructure{ DispositionParams: map[string]string{"filename": "cat.png"}, }, filename: "cat.png", }, { bs: BodyStructure{ Params: map[string]string{"name": "cat.png"}, }, filename: "cat.png", }, { bs: BodyStructure{}, filename: "", }, { bs: BodyStructure{ DispositionParams: map[string]string{"filename": "=?UTF-8?Q?Opis_przedmiotu_zam=c3=b3wienia_-_za=c5=82=c4=85cznik_nr_1?= =?UTF-8?Q?=2epdf?="}, }, filename: "Opis przedmiotu zamówienia - załącznik nr 1.pdf", }, } for i, test := range tests { got, err := test.bs.Filename() if err != nil { t.Errorf("Invalid body structure filename for #%v: error: %v", i, err) continue } if got != test.filename { t.Errorf("Invalid body structure filename for #%v: got '%v', want '%v'", i, got, test.filename) } } } func TestBodyStructureWalk(t *testing.T) { textPlain := &BodyStructure{ MIMEType: "text", MIMESubType: "plain", } textHTML := &BodyStructure{ MIMEType: "text", MIMESubType: "plain", } multipartAlternative := &BodyStructure{ MIMEType: "multipart", MIMESubType: "alternative", Parts: []*BodyStructure{textPlain, textHTML}, } imagePNG := &BodyStructure{ MIMEType: "image", MIMESubType: "png", } multipartMixed := &BodyStructure{ MIMEType: "multipart", MIMESubType: "mixed", Parts: []*BodyStructure{multipartAlternative, imagePNG}, } type testNode struct { path []int part *BodyStructure } tests := []struct { bs *BodyStructure nodes []testNode walkChildren bool }{ { bs: textPlain, nodes: []testNode{ {path: []int{1}, part: textPlain}, }, }, { bs: multipartAlternative, nodes: []testNode{ {path: nil, part: multipartAlternative}, {path: []int{1}, part: textPlain}, {path: []int{2}, part: textHTML}, }, walkChildren: true, }, { bs: multipartMixed, nodes: []testNode{ {path: nil, part: multipartMixed}, {path: []int{1}, part: multipartAlternative}, {path: []int{1, 1}, part: textPlain}, {path: []int{1, 2}, part: textHTML}, {path: []int{2}, part: imagePNG}, }, walkChildren: true, }, { bs: multipartMixed, nodes: []testNode{ {path: nil, part: multipartMixed}, }, walkChildren: false, }, } for i, test := range tests { j := 0 test.bs.Walk(func(path []int, part *BodyStructure) bool { if j >= len(test.nodes) { t.Errorf("Test #%v: invalid node count: got > %v, want %v", i, j, len(test.nodes)) return false } n := &test.nodes[j] if !reflect.DeepEqual(path, n.path) { t.Errorf("Test #%v: node #%v: invalid path: got %v, want %v", i, j, path, n.path) } if part != n.part { t.Errorf("Test #%v: node #%v: invalid part: got %v, want %v", i, j, part, n.part) } j++ return test.walkChildren }) if j != len(test.nodes) { t.Errorf("Test #%v: invalid node count: got %v, want %v", i, j, len(test.nodes)) } } } go-imap-1.2.0/read.go000066400000000000000000000216621412725504300143170ustar00rootroot00000000000000package imap import ( "bytes" "errors" "io" "strconv" "strings" ) const ( sp = ' ' cr = '\r' lf = '\n' dquote = '"' literalStart = '{' literalEnd = '}' listStart = '(' listEnd = ')' respCodeStart = '[' respCodeEnd = ']' ) const ( crlf = "\r\n" nilAtom = "NIL" ) // TODO: add CTL to atomSpecials var ( quotedSpecials = string([]rune{dquote, '\\'}) respSpecials = string([]rune{respCodeEnd}) atomSpecials = string([]rune{listStart, listEnd, literalStart, sp, '%', '*'}) + quotedSpecials + respSpecials ) type parseError struct { error } func newParseError(text string) error { return &parseError{errors.New(text)} } // IsParseError returns true if the provided error is a parse error produced by // Reader. func IsParseError(err error) bool { _, ok := err.(*parseError) return ok } // A string reader. type StringReader interface { // ReadString reads until the first occurrence of delim in the input, // returning a string containing the data up to and including the delimiter. // See https://golang.org/pkg/bufio/#Reader.ReadString ReadString(delim byte) (line string, err error) } type reader interface { io.Reader io.RuneScanner StringReader } // ParseNumber parses a number. func ParseNumber(f interface{}) (uint32, error) { // Useful for tests if n, ok := f.(uint32); ok { return n, nil } var s string switch f := f.(type) { case RawString: s = string(f) case string: s = f default: return 0, newParseError("expected a number, got a non-atom") } nbr, err := strconv.ParseUint(string(s), 10, 32) if err != nil { return 0, &parseError{err} } return uint32(nbr), nil } // ParseString parses a string, which is either a literal, a quoted string or an // atom. func ParseString(f interface{}) (string, error) { if s, ok := f.(string); ok { return s, nil } // Useful for tests if a, ok := f.(RawString); ok { return string(a), nil } if l, ok := f.(Literal); ok { b := make([]byte, l.Len()) if _, err := io.ReadFull(l, b); err != nil { return "", err } return string(b), nil } return "", newParseError("expected a string") } // Convert a field list to a string list. func ParseStringList(f interface{}) ([]string, error) { fields, ok := f.([]interface{}) if !ok { return nil, newParseError("expected a string list, got a non-list") } list := make([]string, len(fields)) for i, f := range fields { var err error if list[i], err = ParseString(f); err != nil { return nil, newParseError("cannot parse string in string list: " + err.Error()) } } return list, nil } func trimSuffix(str string, suffix rune) string { return str[:len(str)-1] } // An IMAP reader. type Reader struct { MaxLiteralSize uint32 // The maximum literal size. reader continues chan<- bool brackets int inRespCode bool } func (r *Reader) ReadSp() error { char, _, err := r.ReadRune() if err != nil { return err } if char != sp { return newParseError("expected a space") } return nil } func (r *Reader) ReadCrlf() (err error) { var char rune if char, _, err = r.ReadRune(); err != nil { return } if char == lf { return } if char != cr { err = newParseError("line doesn't end with a CR") return } if char, _, err = r.ReadRune(); err != nil { return } if char != lf { err = newParseError("line doesn't end with a LF") } return } func (r *Reader) ReadAtom() (interface{}, error) { r.brackets = 0 var atom string for { char, _, err := r.ReadRune() if err != nil { return nil, err } // TODO: list-wildcards and \ if r.brackets == 0 && (char == listStart || char == literalStart || char == dquote) { return nil, newParseError("atom contains forbidden char: " + string(char)) } if char == cr || char == lf { break } if r.brackets == 0 && (char == sp || char == listEnd) { break } if char == respCodeEnd { if r.brackets == 0 { if r.inRespCode { break } else { return nil, newParseError("atom contains bad brackets nesting") } } r.brackets-- } if char == respCodeStart { r.brackets++ } atom += string(char) } r.UnreadRune() if atom == nilAtom { return nil, nil } return atom, nil } func (r *Reader) ReadLiteral() (Literal, error) { char, _, err := r.ReadRune() if err != nil { return nil, err } else if char != literalStart { return nil, newParseError("literal string doesn't start with an open brace") } lstr, err := r.ReadString(byte(literalEnd)) if err != nil { return nil, err } lstr = trimSuffix(lstr, literalEnd) nonSync := strings.HasSuffix(lstr, "+") if nonSync { lstr = trimSuffix(lstr, '+') } n, err := strconv.ParseUint(lstr, 10, 32) if err != nil { return nil, newParseError("cannot parse literal length: " + err.Error()) } if r.MaxLiteralSize > 0 && uint32(n) > r.MaxLiteralSize { return nil, newParseError("literal exceeding maximum size") } if err := r.ReadCrlf(); err != nil { return nil, err } // Send continuation request if necessary if r.continues != nil && !nonSync { r.continues <- true } // Read literal b := make([]byte, n) if _, err := io.ReadFull(r, b); err != nil { return nil, err } return bytes.NewBuffer(b), nil } func (r *Reader) ReadQuotedString() (string, error) { if char, _, err := r.ReadRune(); err != nil { return "", err } else if char != dquote { return "", newParseError("quoted string doesn't start with a double quote") } var buf bytes.Buffer var escaped bool for { char, _, err := r.ReadRune() if err != nil { return "", err } if char == '\\' && !escaped { escaped = true } else { if char == cr || char == lf { r.UnreadRune() return "", newParseError("CR or LF not allowed in quoted string") } if char == dquote && !escaped { break } if !strings.ContainsRune(quotedSpecials, char) && escaped { return "", newParseError("quoted string cannot contain backslash followed by a non-quoted-specials char") } buf.WriteRune(char) escaped = false } } return buf.String(), nil } func (r *Reader) ReadFields() (fields []interface{}, err error) { var char rune for { if char, _, err = r.ReadRune(); err != nil { return } if err = r.UnreadRune(); err != nil { return } var field interface{} ok := true switch char { case literalStart: field, err = r.ReadLiteral() case dquote: field, err = r.ReadQuotedString() case listStart: field, err = r.ReadList() case listEnd: ok = false case cr: return default: field, err = r.ReadAtom() } if err != nil { return } if ok { fields = append(fields, field) } if char, _, err = r.ReadRune(); err != nil { return } if char == cr || char == lf || char == listEnd || char == respCodeEnd { if char == cr || char == lf { r.UnreadRune() } return } if char == listStart { r.UnreadRune() continue } if char != sp { err = newParseError("fields are not separated by a space") return } } } func (r *Reader) ReadList() (fields []interface{}, err error) { char, _, err := r.ReadRune() if err != nil { return } if char != listStart { err = newParseError("list doesn't start with an open parenthesis") return } fields, err = r.ReadFields() if err != nil { return } r.UnreadRune() if char, _, err = r.ReadRune(); err != nil { return } if char != listEnd { err = newParseError("list doesn't end with a close parenthesis") } return } func (r *Reader) ReadLine() (fields []interface{}, err error) { fields, err = r.ReadFields() if err != nil { return } r.UnreadRune() err = r.ReadCrlf() return } func (r *Reader) ReadRespCode() (code StatusRespCode, fields []interface{}, err error) { char, _, err := r.ReadRune() if err != nil { return } if char != respCodeStart { err = newParseError("response code doesn't start with an open bracket") return } r.inRespCode = true fields, err = r.ReadFields() r.inRespCode = false if err != nil { return } if len(fields) == 0 { err = newParseError("response code doesn't contain any field") return } codeStr, ok := fields[0].(string) if !ok { err = newParseError("response code doesn't start with a string atom") return } if codeStr == "" { err = newParseError("response code is empty") return } code = StatusRespCode(strings.ToUpper(codeStr)) fields = fields[1:] r.UnreadRune() char, _, err = r.ReadRune() if err != nil { return } if char != respCodeEnd { err = newParseError("response code doesn't end with a close bracket") } return } func (r *Reader) ReadInfo() (info string, err error) { info, err = r.ReadString(byte(lf)) if err != nil { return } info = strings.TrimSuffix(info, string(lf)) info = strings.TrimSuffix(info, string(cr)) info = strings.TrimLeft(info, " ") return } func NewReader(r reader) *Reader { return &Reader{reader: r} } func NewServerReader(r reader, continues chan<- bool) *Reader { return &Reader{reader: r, continues: continues} } type Parser interface { Parse(fields []interface{}) error } go-imap-1.2.0/read_test.go000066400000000000000000000330051412725504300153500ustar00rootroot00000000000000package imap_test import ( "bytes" "io" "io/ioutil" "reflect" "testing" "github.com/emersion/go-imap" ) func TestParseNumber(t *testing.T) { tests := []struct { f interface{} n uint32 err bool }{ {f: "42", n: 42}, {f: "0", n: 0}, {f: "-1", err: true}, {f: "1.2", err: true}, {f: nil, err: true}, {f: bytes.NewBufferString("cc"), err: true}, } for _, test := range tests { n, err := imap.ParseNumber(test.f) if err != nil { if !test.err { t.Errorf("Cannot parse number %v", test.f) } } else { if test.err { t.Errorf("Parsed invalid number %v", test.f) } else if n != test.n { t.Errorf("Invalid parsed number: got %v but expected %v", n, test.n) } } } } func TestParseStringList(t *testing.T) { tests := []struct { field interface{} list []string }{ { field: []interface{}{"a", "b", "c", "d"}, list: []string{"a", "b", "c", "d"}, }, { field: []interface{}{"a"}, list: []string{"a"}, }, { field: []interface{}{}, list: []string{}, }, { field: []interface{}{"a", 42, "c", "d"}, list: nil, }, { field: []interface{}{"a", nil, "c", "d"}, list: nil, }, { field: "Asuka FTW", list: nil, }, } for _, test := range tests { list, err := imap.ParseStringList(test.field) if err != nil { if test.list != nil { t.Errorf("Cannot parse string list: %v", err) } } else if !reflect.DeepEqual(list, test.list) { t.Errorf("Invalid parsed string list: got \n%+v\n but expected \n%+v", list, test.list) } } } func newReader(s string) (b *bytes.Buffer, r *imap.Reader) { b = bytes.NewBuffer([]byte(s)) r = imap.NewReader(b) return } func TestReader_ReadSp(t *testing.T) { b, r := newReader(" ") if err := r.ReadSp(); err != nil { t.Error(err) } if b.Len() > 0 { t.Error("Buffer is not empty after read") } _, r = newReader("") if err := r.ReadSp(); err == nil { t.Error("Invalid read didn't fail") } } func TestReader_ReadCrlf(t *testing.T) { b, r := newReader("\r\n") if err := r.ReadCrlf(); err != nil { t.Error(err) } if b.Len() > 0 { t.Error("Buffer is not empty after read") } _, r = newReader("") if err := r.ReadCrlf(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("\n") if err := r.ReadCrlf(); err != nil { t.Error(err) } _, r = newReader("\r") if err := r.ReadCrlf(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("\r42") if err := r.ReadCrlf(); err == nil { t.Error("Invalid read didn't fail") } } func TestReader_ReadAtom(t *testing.T) { b, r := newReader("NIL\r\n") if atom, err := r.ReadAtom(); err != nil { t.Error(err) } else if atom != nil { t.Error("NIL atom is not nil:", atom) } else { if err := r.ReadCrlf(); err != nil && err != io.EOF { t.Error("Cannot read CRLF after atom:", err) } if b.Len() > 0 { t.Error("Buffer is not empty after read") } } b, r = newReader("atom\r\n") if atom, err := r.ReadAtom(); err != nil { t.Error(err) } else if s, ok := atom.(string); !ok || s != "atom" { t.Error("String atom has not the expected value:", atom) } else { if err := r.ReadCrlf(); err != nil && err != io.EOF { t.Error("Cannot read CRLF after atom:", err) } if b.Len() > 0 { t.Error("Buffer is not empty after read") } } _, r = newReader("") if _, err := r.ReadAtom(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("(hi there)\r\n") if _, err := r.ReadAtom(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("{42}\r\n") if _, err := r.ReadAtom(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("\"\r\n") if _, err := r.ReadAtom(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("abc]") if _, err := r.ReadAtom(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("[abc]def]ghi") if _, err := r.ReadAtom(); err == nil { t.Error("Invalid read didn't fail") } } func TestReader_ReadLiteral_NonSync(t *testing.T) { // For synchronizing literal we should send continuation request. b := bytes.NewBuffer([]byte("{7}\r\nabcdefg")) cont := make(chan bool, 5) r := imap.NewServerReader(b, cont) if litr, err := r.ReadLiteral(); err != nil { t.Error(err) } else if litr.Len() != 7 { t.Error("Invalid literal length") } else { if len(cont) != 1 { t.Error("Missing continuation rejqest") } <-cont } b = bytes.NewBuffer([]byte("{7+}\r\nabcdefg")) r = imap.NewServerReader(b, cont) if litr, err := r.ReadLiteral(); err != nil { t.Error(err) } else if litr.Len() != 7 { t.Error("Invalid literal length") } else { if len(cont) != 0 { t.Error("Unexpected continuation rejqest") } if contents, err := ioutil.ReadAll(litr); err != nil { t.Error(err) } else if string(contents) != "abcdefg" { t.Error("Literal has not the expected value:", string(contents)) } else if b.Len() > 0 { t.Error("Buffer is not empty after read") } } } func TestReader_ReadLiteral(t *testing.T) { b, r := newReader("{7}\r\nabcdefg") if literal, err := r.ReadLiteral(); err != nil { t.Error(err) } else if literal.Len() != 7 { t.Error("Invalid literal length:", literal.Len()) } else { if contents, err := ioutil.ReadAll(literal); err != nil { t.Error(err) } else if string(contents) != "abcdefg" { t.Error("Literal has not the expected value:", string(contents)) } else if b.Len() > 0 { t.Error("Buffer is not empty after read") } } _, r = newReader("") if _, err := r.ReadLiteral(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("[7}\r\nabcdefg") if _, err := r.ReadLiteral(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("{7]\r\nabcdefg") if _, err := r.ReadLiteral(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("{7.4}\r\nabcdefg") if _, err := r.ReadLiteral(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("{7}abcdefg") if _, err := r.ReadLiteral(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("{7}\rabcdefg") if _, err := r.ReadLiteral(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("{7}\nabcdefg") if _, err := r.ReadLiteral(); err != nil { t.Error(err) } _, r = newReader("{7}\r\nabcd") if _, err := r.ReadLiteral(); err == nil { t.Error("Invalid read didn't fail") } } func TestReader_ReadQuotedString(t *testing.T) { b, r := newReader("\"hello gopher\"\r\n") if s, err := r.ReadQuotedString(); err != nil { t.Error(err) } else if s != "hello gopher" { t.Error("Quoted string has not the expected value:", s) } else { if err := r.ReadCrlf(); err != nil && err != io.EOF { t.Error("Cannot read CRLF after quoted string:", err) } if b.Len() > 0 { t.Error("Buffer is not empty after read") } } _, r = newReader("\"here's a backslash: \\\\, and here's a double quote: \\\" !\"\r\n") if s, err := r.ReadQuotedString(); err != nil { t.Error(err) } else if s != "here's a backslash: \\, and here's a double quote: \" !" { t.Error("Quoted string has not the expected value:", s) } _, r = newReader("") if _, err := r.ReadQuotedString(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("hello gopher\"\r\n") if _, err := r.ReadQuotedString(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("\"hello gopher\r\n") if _, err := r.ReadQuotedString(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("\"hello \\gopher\"\r\n") if _, err := r.ReadQuotedString(); err == nil { t.Error("Invalid read didn't fail") } } func TestReader_ReadFields(t *testing.T) { b, r := newReader("field1 \"field2\"\r\n") if fields, err := r.ReadFields(); err != nil { t.Error(err) } else if len(fields) != 2 { t.Error("Expected 2 fields, but got", len(fields)) } else if s, ok := fields[0].(string); !ok || s != "field1" { t.Error("Field 1 has not the expected value:", fields[0]) } else if s, ok := fields[1].(string); !ok || s != "field2" { t.Error("Field 2 has not the expected value:", fields[1]) } else { if err := r.ReadCrlf(); err != nil && err != io.EOF { t.Error("Cannot read CRLF after fields:", err) } if b.Len() > 0 { t.Error("Buffer is not empty after read") } } _, r = newReader("") if _, err := r.ReadFields(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("fi\"eld1 \"field2\"\r\n") if _, err := r.ReadFields(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("field1 ") if _, err := r.ReadFields(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("field1 (") if _, err := r.ReadFields(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("field1\"field2\"\r\n") if _, err := r.ReadFields(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("\"field1\"\"field2\"\r\n") if _, err := r.ReadFields(); err == nil { t.Error("Invalid read didn't fail") } } func TestReader_ReadList(t *testing.T) { b, r := newReader("(field1 \"field2\" {6}\r\nfield3 field4)") if fields, err := r.ReadList(); err != nil { t.Error(err) } else if len(fields) != 4 { t.Error("Expected 2 fields, but got", len(fields)) } else if s, ok := fields[0].(string); !ok || s != "field1" { t.Error("Field 1 has not the expected value:", fields[0]) } else if s, ok := fields[1].(string); !ok || s != "field2" { t.Error("Field 2 has not the expected value:", fields[1]) } else if literal, ok := fields[2].(imap.Literal); !ok { t.Error("Field 3 has not the expected value:", fields[2]) } else if contents, err := ioutil.ReadAll(literal); err != nil { t.Error(err) } else if string(contents) != "field3" { t.Error("Field 3 has not the expected value:", string(contents)) } else if s, ok := fields[3].(string); !ok || s != "field4" { t.Error("Field 4 has not the expected value:", fields[3]) } else if b.Len() > 0 { t.Error("Buffer is not empty after read") } b, r = newReader("()") if fields, err := r.ReadList(); err != nil { t.Error(err) } else if len(fields) != 0 { t.Error("Expected 0 fields, but got", len(fields)) } else if b.Len() > 0 { t.Error("Buffer is not empty after read") } _, r = newReader("") if _, err := r.ReadList(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("[field1 field2 field3)") if _, err := r.ReadList(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("(field1 fie\"ld2 field3)") if _, err := r.ReadList(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("(field1 field2 field3\r\n") if _, err := r.ReadList(); err == nil { t.Error("Invalid read didn't fail") } } func TestReader_ReadLine(t *testing.T) { b, r := newReader("field1 field2\r\n") if fields, err := r.ReadLine(); err != nil { t.Error(err) } else if len(fields) != 2 { t.Error("Expected 2 fields, but got", len(fields)) } else if s, ok := fields[0].(string); !ok || s != "field1" { t.Error("Field 1 has not the expected value:", fields[0]) } else if s, ok := fields[1].(string); !ok || s != "field2" { t.Error("Field 2 has not the expected value:", fields[1]) } else if b.Len() > 0 { t.Error("Buffer is not empty after read") } _, r = newReader("") if _, err := r.ReadLine(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("field1 field2\rabc") if _, err := r.ReadLine(); err == nil { t.Error("Invalid read didn't fail") } } func TestReader_ReadRespCode(t *testing.T) { b, r := newReader("[CAPABILITY NOOP STARTTLS]") if code, fields, err := r.ReadRespCode(); err != nil { t.Error(err) } else if code != imap.CodeCapability { t.Error("Response code has not the expected value:", code) } else if len(fields) != 2 { t.Error("Expected 2 fields, but got", len(fields)) } else if s, ok := fields[0].(string); !ok || s != "NOOP" { t.Error("Field 1 has not the expected value:", fields[0]) } else if s, ok := fields[1].(string); !ok || s != "STARTTLS" { t.Error("Field 2 has not the expected value:", fields[1]) } else if b.Len() > 0 { t.Error("Buffer is not empty after read") } _, r = newReader("") if _, _, err := r.ReadRespCode(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("{CAPABILITY NOOP STARTTLS]") if _, _, err := r.ReadRespCode(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("[CAPABILITY NO\"OP STARTTLS]") if _, _, err := r.ReadRespCode(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("[]") if _, _, err := r.ReadRespCode(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("[{3}\r\nabc]") if _, _, err := r.ReadRespCode(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("[CAPABILITY NOOP STARTTLS\r\n") if _, _, err := r.ReadRespCode(); err == nil { t.Error("Invalid read didn't fail") } } func TestReader_ReadInfo(t *testing.T) { b, r := newReader("I love potatoes.\r\n") if info, err := r.ReadInfo(); err != nil { t.Error(err) } else if info != "I love potatoes." { t.Error("Info has not the expected value:", info) } else if b.Len() > 0 { t.Error("Buffer is not empty after read") } _, r = newReader("I love potatoes.") if _, err := r.ReadInfo(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("I love potatoes.\r") if _, err := r.ReadInfo(); err == nil { t.Error("Invalid read didn't fail") } _, r = newReader("I love potatoes.\n") if _, err := r.ReadInfo(); err != nil { t.Error(err) } _, r = newReader("I love potatoes.\rabc") if _, err := r.ReadInfo(); err == nil { t.Error("Invalid read didn't fail") } } go-imap-1.2.0/response.go000066400000000000000000000073761412725504300152500ustar00rootroot00000000000000package imap import ( "strings" ) // Resp is an IMAP response. It is either a *DataResp, a // *ContinuationReq or a *StatusResp. type Resp interface { resp() } // ReadResp reads a single response from a Reader. func ReadResp(r *Reader) (Resp, error) { atom, err := r.ReadAtom() if err != nil { return nil, err } tag, ok := atom.(string) if !ok { return nil, newParseError("response tag is not an atom") } if tag == "+" { if err := r.ReadSp(); err != nil { r.UnreadRune() } resp := &ContinuationReq{} resp.Info, err = r.ReadInfo() if err != nil { return nil, err } return resp, nil } if err := r.ReadSp(); err != nil { return nil, err } // Can be either data or status // Try to parse a status var fields []interface{} if atom, err := r.ReadAtom(); err == nil { fields = append(fields, atom) if err := r.ReadSp(); err == nil { if name, ok := atom.(string); ok { status := StatusRespType(name) switch status { case StatusRespOk, StatusRespNo, StatusRespBad, StatusRespPreauth, StatusRespBye: resp := &StatusResp{ Tag: tag, Type: status, } char, _, err := r.ReadRune() if err != nil { return nil, err } r.UnreadRune() if char == '[' { // Contains code & arguments resp.Code, resp.Arguments, err = r.ReadRespCode() if err != nil { return nil, err } } resp.Info, err = r.ReadInfo() if err != nil { return nil, err } return resp, nil } } } else { r.UnreadRune() } } else { r.UnreadRune() } // Not a status so it's data resp := &DataResp{Tag: tag} var remaining []interface{} remaining, err = r.ReadLine() if err != nil { return nil, err } resp.Fields = append(fields, remaining...) return resp, nil } // DataResp is an IMAP response containing data. type DataResp struct { // The response tag. Can be either "" for untagged responses, "+" for continuation // requests or a previous command's tag. Tag string // The parsed response fields. Fields []interface{} } // NewUntaggedResp creates a new untagged response. func NewUntaggedResp(fields []interface{}) *DataResp { return &DataResp{ Tag: "*", Fields: fields, } } func (r *DataResp) resp() {} func (r *DataResp) WriteTo(w *Writer) error { tag := RawString(r.Tag) if tag == "" { tag = RawString("*") } fields := []interface{}{RawString(tag)} fields = append(fields, r.Fields...) return w.writeLine(fields...) } // ContinuationReq is a continuation request response. type ContinuationReq struct { // The info message sent with the continuation request. Info string } func (r *ContinuationReq) resp() {} func (r *ContinuationReq) WriteTo(w *Writer) error { if err := w.writeString("+"); err != nil { return err } if r.Info != "" { if err := w.writeString(string(sp) + r.Info); err != nil { return err } } return w.writeCrlf() } // ParseNamedResp attempts to parse a named data response. func ParseNamedResp(resp Resp) (name string, fields []interface{}, ok bool) { data, ok := resp.(*DataResp) if !ok || len(data.Fields) == 0 { return } // Some responses (namely EXISTS and RECENT) are formatted like so: // [num] [name] [...] // Which is fucking stupid. But we handle that here by checking if the // response name is a number and then rearranging it. if len(data.Fields) > 1 { name, ok := data.Fields[1].(string) if ok { if _, err := ParseNumber(data.Fields[0]); err == nil { fields := []interface{}{data.Fields[0]} fields = append(fields, data.Fields[2:]...) return strings.ToUpper(name), fields, true } } } // IMAP commands are formatted like this: // [name] [...] name, ok = data.Fields[0].(string) if !ok { return } return strings.ToUpper(name), data.Fields[1:], true } go-imap-1.2.0/response_test.go000066400000000000000000000130351412725504300162740ustar00rootroot00000000000000package imap_test import ( "bytes" "reflect" "testing" "github.com/emersion/go-imap" ) func TestResp_WriteTo(t *testing.T) { var b bytes.Buffer w := imap.NewWriter(&b) resp := imap.NewUntaggedResp([]interface{}{imap.RawString("76"), imap.RawString("FETCH"), []interface{}{imap.RawString("UID"), 783}}) if err := resp.WriteTo(w); err != nil { t.Fatal(err) } if b.String() != "* 76 FETCH (UID 783)\r\n" { t.Error("Invalid response:", b.String()) } } func TestContinuationReq_WriteTo(t *testing.T) { var b bytes.Buffer w := imap.NewWriter(&b) resp := &imap.ContinuationReq{} if err := resp.WriteTo(w); err != nil { t.Fatal(err) } if b.String() != "+\r\n" { t.Error("Invalid continuation:", b.String()) } } func TestContinuationReq_WriteTo_WithInfo(t *testing.T) { var b bytes.Buffer w := imap.NewWriter(&b) resp := &imap.ContinuationReq{Info: "send literal"} if err := resp.WriteTo(w); err != nil { t.Fatal(err) } if b.String() != "+ send literal\r\n" { t.Error("Invalid continuation:", b.String()) } } func TestReadResp_ContinuationReq(t *testing.T) { b := bytes.NewBufferString("+ send literal\r\n") r := imap.NewReader(b) resp, err := imap.ReadResp(r) if err != nil { t.Fatal(err) } cont, ok := resp.(*imap.ContinuationReq) if !ok { t.Fatal("Response is not a continuation request") } if cont.Info != "send literal" { t.Error("Invalid info:", cont.Info) } } func TestReadResp_ContinuationReq_NoInfo(t *testing.T) { b := bytes.NewBufferString("+\r\n") r := imap.NewReader(b) resp, err := imap.ReadResp(r) if err != nil { t.Fatal(err) } cont, ok := resp.(*imap.ContinuationReq) if !ok { t.Fatal("Response is not a continuation request") } if cont.Info != "" { t.Error("Invalid info:", cont.Info) } } func TestReadResp_Resp(t *testing.T) { b := bytes.NewBufferString("* 1 EXISTS\r\n") r := imap.NewReader(b) resp, err := imap.ReadResp(r) if err != nil { t.Fatal(err) } data, ok := resp.(*imap.DataResp) if !ok { t.Fatal("Invalid response type") } if data.Tag != "*" { t.Error("Invalid tag:", data.Tag) } if len(data.Fields) != 2 { t.Error("Invalid fields:", data.Fields) } } func TestReadResp_Resp_NoArgs(t *testing.T) { b := bytes.NewBufferString("* SEARCH\r\n") r := imap.NewReader(b) resp, err := imap.ReadResp(r) if err != nil { t.Fatal(err) } data, ok := resp.(*imap.DataResp) if !ok { t.Fatal("Invalid response type") } if data.Tag != "*" { t.Error("Invalid tag:", data.Tag) } if len(data.Fields) != 1 || data.Fields[0] != "SEARCH" { t.Error("Invalid fields:", data.Fields) } } func TestReadResp_StatusResp(t *testing.T) { tests := []struct { input string expected *imap.StatusResp }{ { input: "* OK IMAP4rev1 Service Ready\r\n", expected: &imap.StatusResp{ Tag: "*", Type: imap.StatusRespOk, Info: "IMAP4rev1 Service Ready", }, }, { input: "* PREAUTH Welcome Pauline!\r\n", expected: &imap.StatusResp{ Tag: "*", Type: imap.StatusRespPreauth, Info: "Welcome Pauline!", }, }, { input: "a001 OK NOOP completed\r\n", expected: &imap.StatusResp{ Tag: "a001", Type: imap.StatusRespOk, Info: "NOOP completed", }, }, { input: "a001 OK [READ-ONLY] SELECT completed\r\n", expected: &imap.StatusResp{ Tag: "a001", Type: imap.StatusRespOk, Code: "READ-ONLY", Info: "SELECT completed", }, }, { input: "a001 OK [CAPABILITY IMAP4rev1 UIDPLUS] LOGIN completed\r\n", expected: &imap.StatusResp{ Tag: "a001", Type: imap.StatusRespOk, Code: "CAPABILITY", Arguments: []interface{}{"IMAP4rev1", "UIDPLUS"}, Info: "LOGIN completed", }, }, } for _, test := range tests { b := bytes.NewBufferString(test.input) r := imap.NewReader(b) resp, err := imap.ReadResp(r) if err != nil { t.Fatal(err) } status, ok := resp.(*imap.StatusResp) if !ok { t.Fatal("Response is not a status:", resp) } if status.Tag != test.expected.Tag { t.Errorf("Invalid tag: expected %v but got %v", status.Tag, test.expected.Tag) } if status.Type != test.expected.Type { t.Errorf("Invalid type: expected %v but got %v", status.Type, test.expected.Type) } if status.Code != test.expected.Code { t.Errorf("Invalid code: expected %v but got %v", status.Code, test.expected.Code) } if len(status.Arguments) != len(test.expected.Arguments) { t.Errorf("Invalid arguments: expected %v but got %v", status.Arguments, test.expected.Arguments) } if status.Info != test.expected.Info { t.Errorf("Invalid info: expected %v but got %v", status.Info, test.expected.Info) } } } func TestParseNamedResp(t *testing.T) { tests := []struct { resp *imap.DataResp name string fields []interface{} }{ { resp: &imap.DataResp{Fields: []interface{}{"CAPABILITY", "IMAP4rev1"}}, name: "CAPABILITY", fields: []interface{}{"IMAP4rev1"}, }, { resp: &imap.DataResp{Fields: []interface{}{"42", "EXISTS"}}, name: "EXISTS", fields: []interface{}{"42"}, }, { resp: &imap.DataResp{Fields: []interface{}{"42", "FETCH", "blah"}}, name: "FETCH", fields: []interface{}{"42", "blah"}, }, } for _, test := range tests { name, fields, ok := imap.ParseNamedResp(test.resp) if !ok { t.Errorf("ParseNamedResp(%v)[2] = false, want true", test.resp) } else if name != test.name { t.Errorf("ParseNamedResp(%v)[0] = %v, want %v", test.resp, name, test.name) } else if !reflect.DeepEqual(fields, test.fields) { t.Errorf("ParseNamedResp(%v)[1] = %v, want %v", test.resp, fields, test.fields) } } } go-imap-1.2.0/responses/000077500000000000000000000000001412725504300150675ustar00rootroot00000000000000go-imap-1.2.0/responses/authenticate.go000066400000000000000000000023111412725504300200710ustar00rootroot00000000000000package responses import ( "encoding/base64" "github.com/emersion/go-imap" "github.com/emersion/go-sasl" ) // An AUTHENTICATE response. type Authenticate struct { Mechanism sasl.Client InitialResponse []byte RepliesCh chan []byte } // Implements func (r *Authenticate) Replies() <-chan []byte { return r.RepliesCh } func (r *Authenticate) writeLine(l string) error { r.RepliesCh <- []byte(l + "\r\n") return nil } func (r *Authenticate) cancel() error { return r.writeLine("*") } func (r *Authenticate) Handle(resp imap.Resp) error { cont, ok := resp.(*imap.ContinuationReq) if !ok { return ErrUnhandled } // Empty challenge, send initial response as stated in RFC 2222 section 5.1 if cont.Info == "" && r.InitialResponse != nil { encoded := base64.StdEncoding.EncodeToString(r.InitialResponse) if err := r.writeLine(encoded); err != nil { return err } r.InitialResponse = nil return nil } challenge, err := base64.StdEncoding.DecodeString(cont.Info) if err != nil { r.cancel() return err } reply, err := r.Mechanism.Next(challenge) if err != nil { r.cancel() return err } encoded := base64.StdEncoding.EncodeToString(reply) return r.writeLine(encoded) } go-imap-1.2.0/responses/capability.go000066400000000000000000000006201412725504300175350ustar00rootroot00000000000000package responses import ( "github.com/emersion/go-imap" ) // A CAPABILITY response. // See RFC 3501 section 7.2.1 type Capability struct { Caps []string } func (r *Capability) WriteTo(w *imap.Writer) error { fields := []interface{}{imap.RawString("CAPABILITY")} for _, cap := range r.Caps { fields = append(fields, imap.RawString(cap)) } return imap.NewUntaggedResp(fields).WriteTo(w) } go-imap-1.2.0/responses/enabled.go000066400000000000000000000012511412725504300170070ustar00rootroot00000000000000package responses import ( "github.com/emersion/go-imap" ) // An ENABLED response, defined in RFC 5161 section 3.2. type Enabled struct { Caps []string } func (r *Enabled) Handle(resp imap.Resp) error { name, fields, ok := imap.ParseNamedResp(resp) if !ok || name != "ENABLED" { return ErrUnhandled } if caps, err := imap.ParseStringList(fields); err != nil { return err } else { r.Caps = append(r.Caps, caps...) } return nil } func (r *Enabled) WriteTo(w *imap.Writer) error { fields := []interface{}{imap.RawString("ENABLED")} for _, cap := range r.Caps { fields = append(fields, imap.RawString(cap)) } return imap.NewUntaggedResp(fields).WriteTo(w) } go-imap-1.2.0/responses/expunge.go000066400000000000000000000013711412725504300170730ustar00rootroot00000000000000package responses import ( "github.com/emersion/go-imap" ) const expungeName = "EXPUNGE" // An EXPUNGE response. // See RFC 3501 section 7.4.1 type Expunge struct { SeqNums chan uint32 } func (r *Expunge) Handle(resp imap.Resp) error { name, fields, ok := imap.ParseNamedResp(resp) if !ok || name != expungeName { return ErrUnhandled } if len(fields) == 0 { return errNotEnoughFields } seqNum, err := imap.ParseNumber(fields[0]) if err != nil { return err } r.SeqNums <- seqNum return nil } func (r *Expunge) WriteTo(w *imap.Writer) error { for seqNum := range r.SeqNums { resp := imap.NewUntaggedResp([]interface{}{seqNum, imap.RawString(expungeName)}) if err := resp.WriteTo(w); err != nil { return err } } return nil } go-imap-1.2.0/responses/fetch.go000066400000000000000000000032431412725504300165110ustar00rootroot00000000000000package responses import ( "github.com/emersion/go-imap" ) const fetchName = "FETCH" // A FETCH response. // See RFC 3501 section 7.4.2 type Fetch struct { Messages chan *imap.Message SeqSet *imap.SeqSet Uid bool } func (r *Fetch) Handle(resp imap.Resp) error { name, fields, ok := imap.ParseNamedResp(resp) if !ok || name != fetchName { return ErrUnhandled } else if len(fields) < 1 { return errNotEnoughFields } seqNum, err := imap.ParseNumber(fields[0]) if err != nil { return err } msgFields, _ := fields[1].([]interface{}) msg := &imap.Message{SeqNum: seqNum} if err := msg.Parse(msgFields); err != nil { return err } if r.Uid && msg.Uid == 0 { // we requested UIDs and got a message without one --> unilateral update --> ignore return ErrUnhandled } var num uint32 if r.Uid { num = msg.Uid } else { num = seqNum } // Check whether we obtained a result we requested with our SeqSet // If the result is not contained in our SeqSet we have to handle an additional special case: // In case we requested UIDs with a dynamic sequence (i.e. * or n:*) and the maximum UID of the mailbox // is less then our n, the server will supply us with the max UID (cf. RFC 3501 §6.4.8 and §9 `seq-range`). // Thus, such a result is correct and has to be returned by us. if !r.SeqSet.Contains(num) && (!r.Uid || !r.SeqSet.Dynamic()) { return ErrUnhandled } r.Messages <- msg return nil } func (r *Fetch) WriteTo(w *imap.Writer) error { var err error for msg := range r.Messages { resp := imap.NewUntaggedResp([]interface{}{msg.SeqNum, imap.RawString(fetchName), msg.Format()}) if err == nil { err = resp.WriteTo(w) } } return err } go-imap-1.2.0/responses/idle.go000066400000000000000000000012031412725504300163270ustar00rootroot00000000000000package responses import ( "github.com/emersion/go-imap" ) // An IDLE response. type Idle struct { RepliesCh chan []byte Stop <-chan struct{} gotContinuationReq bool } func (r *Idle) Replies() <-chan []byte { return r.RepliesCh } func (r *Idle) stop() { r.RepliesCh <- []byte("DONE\r\n") } func (r *Idle) Handle(resp imap.Resp) error { // Wait for a continuation request if _, ok := resp.(*imap.ContinuationReq); ok && !r.gotContinuationReq { r.gotContinuationReq = true // We got a continuation request, wait for r.Stop to be closed go func() { <-r.Stop r.stop() }() return nil } return ErrUnhandled } go-imap-1.2.0/responses/list.go000066400000000000000000000017351412725504300163770ustar00rootroot00000000000000package responses import ( "github.com/emersion/go-imap" ) const ( listName = "LIST" lsubName = "LSUB" ) // A LIST response. // If Subscribed is set to true, LSUB will be used instead. // See RFC 3501 section 7.2.2 type List struct { Mailboxes chan *imap.MailboxInfo Subscribed bool } func (r *List) Name() string { if r.Subscribed { return lsubName } else { return listName } } func (r *List) Handle(resp imap.Resp) error { name, fields, ok := imap.ParseNamedResp(resp) if !ok || name != r.Name() { return ErrUnhandled } mbox := &imap.MailboxInfo{} if err := mbox.Parse(fields); err != nil { return err } r.Mailboxes <- mbox return nil } func (r *List) WriteTo(w *imap.Writer) error { respName := r.Name() for mbox := range r.Mailboxes { fields := []interface{}{imap.RawString(respName)} fields = append(fields, mbox.Format()...) resp := imap.NewUntaggedResp(fields) if err := resp.WriteTo(w); err != nil { return err } } return nil } go-imap-1.2.0/responses/list_test.go000066400000000000000000000022511412725504300174300ustar00rootroot00000000000000package responses import ( "bytes" "testing" "github.com/emersion/go-imap" ) func TestListSlashDelimiter(t *testing.T) { mbox := &imap.MailboxInfo{} if err := mbox.Parse([]interface{}{ []interface{}{"\\Unseen"}, "/", "INBOX", }); err != nil { t.Error(err) t.FailNow() } if response := getListResponse(t, mbox); response != `* LIST (\Unseen) "/" INBOX`+"\r\n" { t.Error("Unexpected response:", response) } } func TestListNILDelimiter(t *testing.T) { mbox := &imap.MailboxInfo{} if err := mbox.Parse([]interface{}{ []interface{}{"\\Unseen"}, nil, "INBOX", }); err != nil { t.Error(err) t.FailNow() } if response := getListResponse(t, mbox); response != `* LIST (\Unseen) NIL INBOX`+"\r\n" { t.Error("Unexpected response:", response) } } func newListResponse(mbox *imap.MailboxInfo) (l *List) { l = &List{Mailboxes: make(chan *imap.MailboxInfo)} go func() { l.Mailboxes <- mbox close(l.Mailboxes) }() return } func getListResponse(t *testing.T, mbox *imap.MailboxInfo) string { b := &bytes.Buffer{} w := imap.NewWriter(b) if err := newListResponse(mbox).WriteTo(w); err != nil { t.Error(err) t.FailNow() } return b.String() } go-imap-1.2.0/responses/responses.go000066400000000000000000000015211412725504300174360ustar00rootroot00000000000000// IMAP responses defined in RFC 3501. package responses import ( "errors" "github.com/emersion/go-imap" ) // ErrUnhandled is used when a response hasn't been handled. var ErrUnhandled = errors.New("imap: unhandled response") var errNotEnoughFields = errors.New("imap: not enough fields in response") // Handler handles responses. type Handler interface { // Handle processes a response. If the response cannot be processed, // ErrUnhandledResp must be returned. Handle(resp imap.Resp) error } // HandlerFunc is a function that handles responses. type HandlerFunc func(resp imap.Resp) error // Handle implements Handler. func (f HandlerFunc) Handle(resp imap.Resp) error { return f(resp) } // Replier is a Handler that needs to send raw data (for instance // AUTHENTICATE). type Replier interface { Handler Replies() <-chan []byte } go-imap-1.2.0/responses/search.go000066400000000000000000000013541412725504300166660ustar00rootroot00000000000000package responses import ( "github.com/emersion/go-imap" ) const searchName = "SEARCH" // A SEARCH response. // See RFC 3501 section 7.2.5 type Search struct { Ids []uint32 } func (r *Search) Handle(resp imap.Resp) error { name, fields, ok := imap.ParseNamedResp(resp) if !ok || name != searchName { return ErrUnhandled } r.Ids = make([]uint32, len(fields)) for i, f := range fields { if id, err := imap.ParseNumber(f); err != nil { return err } else { r.Ids[i] = id } } return nil } func (r *Search) WriteTo(w *imap.Writer) (err error) { fields := []interface{}{imap.RawString(searchName)} for _, id := range r.Ids { fields = append(fields, id) } resp := imap.NewUntaggedResp(fields) return resp.WriteTo(w) } go-imap-1.2.0/responses/select.go000066400000000000000000000065721412725504300167070ustar00rootroot00000000000000package responses import ( "fmt" "github.com/emersion/go-imap" ) // A SELECT response. type Select struct { Mailbox *imap.MailboxStatus } func (r *Select) Handle(resp imap.Resp) error { if r.Mailbox == nil { r.Mailbox = &imap.MailboxStatus{Items: make(map[imap.StatusItem]interface{})} } mbox := r.Mailbox switch resp := resp.(type) { case *imap.DataResp: name, fields, ok := imap.ParseNamedResp(resp) if !ok || name != "FLAGS" { return ErrUnhandled } else if len(fields) < 1 { return errNotEnoughFields } flags, _ := fields[0].([]interface{}) mbox.Flags, _ = imap.ParseStringList(flags) case *imap.StatusResp: if len(resp.Arguments) < 1 { return ErrUnhandled } var item imap.StatusItem switch resp.Code { case "UNSEEN": mbox.UnseenSeqNum, _ = imap.ParseNumber(resp.Arguments[0]) case "PERMANENTFLAGS": flags, _ := resp.Arguments[0].([]interface{}) mbox.PermanentFlags, _ = imap.ParseStringList(flags) case "UIDNEXT": mbox.UidNext, _ = imap.ParseNumber(resp.Arguments[0]) item = imap.StatusUidNext case "UIDVALIDITY": mbox.UidValidity, _ = imap.ParseNumber(resp.Arguments[0]) item = imap.StatusUidValidity default: return ErrUnhandled } if item != "" { mbox.ItemsLocker.Lock() mbox.Items[item] = nil mbox.ItemsLocker.Unlock() } default: return ErrUnhandled } return nil } func (r *Select) WriteTo(w *imap.Writer) error { mbox := r.Mailbox if mbox.Flags != nil { flags := make([]interface{}, len(mbox.Flags)) for i, f := range mbox.Flags { flags[i] = imap.RawString(f) } res := imap.NewUntaggedResp([]interface{}{imap.RawString("FLAGS"), flags}) if err := res.WriteTo(w); err != nil { return err } } if mbox.PermanentFlags != nil { flags := make([]interface{}, len(mbox.PermanentFlags)) for i, f := range mbox.PermanentFlags { flags[i] = imap.RawString(f) } statusRes := &imap.StatusResp{ Type: imap.StatusRespOk, Code: imap.CodePermanentFlags, Arguments: []interface{}{flags}, Info: "Flags permitted.", } if err := statusRes.WriteTo(w); err != nil { return err } } if mbox.UnseenSeqNum > 0 { statusRes := &imap.StatusResp{ Type: imap.StatusRespOk, Code: imap.CodeUnseen, Arguments: []interface{}{mbox.UnseenSeqNum}, Info: fmt.Sprintf("Message %d is first unseen", mbox.UnseenSeqNum), } if err := statusRes.WriteTo(w); err != nil { return err } } for k := range r.Mailbox.Items { switch k { case imap.StatusMessages: res := imap.NewUntaggedResp([]interface{}{mbox.Messages, imap.RawString("EXISTS")}) if err := res.WriteTo(w); err != nil { return err } case imap.StatusRecent: res := imap.NewUntaggedResp([]interface{}{mbox.Recent, imap.RawString("RECENT")}) if err := res.WriteTo(w); err != nil { return err } case imap.StatusUidNext: statusRes := &imap.StatusResp{ Type: imap.StatusRespOk, Code: imap.CodeUidNext, Arguments: []interface{}{mbox.UidNext}, Info: "Predicted next UID", } if err := statusRes.WriteTo(w); err != nil { return err } case imap.StatusUidValidity: statusRes := &imap.StatusResp{ Type: imap.StatusRespOk, Code: imap.CodeUidValidity, Arguments: []interface{}{mbox.UidValidity}, Info: "UIDs valid", } if err := statusRes.WriteTo(w); err != nil { return err } } } return nil } go-imap-1.2.0/responses/status.go000066400000000000000000000023151412725504300167420ustar00rootroot00000000000000package responses import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/utf7" ) const statusName = "STATUS" // A STATUS response. // See RFC 3501 section 7.2.4 type Status struct { Mailbox *imap.MailboxStatus } func (r *Status) Handle(resp imap.Resp) error { if r.Mailbox == nil { r.Mailbox = &imap.MailboxStatus{} } mbox := r.Mailbox name, fields, ok := imap.ParseNamedResp(resp) if !ok || name != statusName { return ErrUnhandled } else if len(fields) < 2 { return errNotEnoughFields } if name, err := imap.ParseString(fields[0]); err != nil { return err } else if name, err := utf7.Encoding.NewDecoder().String(name); err != nil { return err } else { mbox.Name = imap.CanonicalMailboxName(name) } var items []interface{} if items, ok = fields[1].([]interface{}); !ok { return errors.New("STATUS response expects a list as second argument") } mbox.Items = nil return mbox.Parse(items) } func (r *Status) WriteTo(w *imap.Writer) error { mbox := r.Mailbox name, _ := utf7.Encoding.NewEncoder().String(mbox.Name) fields := []interface{}{imap.RawString(statusName), imap.FormatMailboxName(name), mbox.Format()} return imap.NewUntaggedResp(fields).WriteTo(w) } go-imap-1.2.0/search.go000066400000000000000000000250611412725504300146460ustar00rootroot00000000000000package imap import ( "errors" "fmt" "io" "net/textproto" "strings" "time" ) func maybeString(mystery interface{}) string { if s, ok := mystery.(string); ok { return s } return "" } func convertField(f interface{}, charsetReader func(io.Reader) io.Reader) string { // An IMAP string contains only 7-bit data, no need to decode it if s, ok := f.(string); ok { return s } // If no charset is provided, getting directly the string is faster if charsetReader == nil { if stringer, ok := f.(fmt.Stringer); ok { return stringer.String() } } // Not a string, it must be a literal l, ok := f.(Literal) if !ok { return "" } var r io.Reader = l if charsetReader != nil { if dec := charsetReader(r); dec != nil { r = dec } } b := make([]byte, l.Len()) if _, err := io.ReadFull(r, b); err != nil { return "" } return string(b) } func popSearchField(fields []interface{}) (interface{}, []interface{}, error) { if len(fields) == 0 { return nil, nil, errors.New("imap: no enough fields for search key") } return fields[0], fields[1:], nil } // SearchCriteria is a search criteria. A message matches the criteria if and // only if it matches each one of its fields. type SearchCriteria struct { SeqNum *SeqSet // Sequence number is in sequence set Uid *SeqSet // UID is in sequence set // Time and timezone are ignored Since time.Time // Internal date is since this date Before time.Time // Internal date is before this date SentSince time.Time // Date header field is since this date SentBefore time.Time // Date header field is before this date Header textproto.MIMEHeader // Each header field value is present Body []string // Each string is in the body Text []string // Each string is in the text (header + body) WithFlags []string // Each flag is present WithoutFlags []string // Each flag is not present Larger uint32 // Size is larger than this number Smaller uint32 // Size is smaller than this number Not []*SearchCriteria // Each criteria doesn't match Or [][2]*SearchCriteria // Each criteria pair has at least one match of two } // NewSearchCriteria creates a new search criteria. func NewSearchCriteria() *SearchCriteria { return &SearchCriteria{Header: make(textproto.MIMEHeader)} } func (c *SearchCriteria) parseField(fields []interface{}, charsetReader func(io.Reader) io.Reader) ([]interface{}, error) { if len(fields) == 0 { return nil, nil } f := fields[0] fields = fields[1:] if subfields, ok := f.([]interface{}); ok { return fields, c.ParseWithCharset(subfields, charsetReader) } key, ok := f.(string) if !ok { return nil, fmt.Errorf("imap: invalid search criteria field type: %T", f) } key = strings.ToUpper(key) var err error switch key { case "ALL": // Nothing to do case "ANSWERED", "DELETED", "DRAFT", "FLAGGED", "RECENT", "SEEN": c.WithFlags = append(c.WithFlags, CanonicalFlag("\\"+key)) case "BCC", "CC", "FROM", "SUBJECT", "TO": if f, fields, err = popSearchField(fields); err != nil { return nil, err } if c.Header == nil { c.Header = make(textproto.MIMEHeader) } c.Header.Add(key, convertField(f, charsetReader)) case "BEFORE": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { return nil, err } else if c.Before.IsZero() || t.Before(c.Before) { c.Before = t } case "BODY": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else { c.Body = append(c.Body, convertField(f, charsetReader)) } case "HEADER": var f1, f2 interface{} if f1, fields, err = popSearchField(fields); err != nil { return nil, err } else if f2, fields, err = popSearchField(fields); err != nil { return nil, err } else { if c.Header == nil { c.Header = make(textproto.MIMEHeader) } c.Header.Add(maybeString(f1), convertField(f2, charsetReader)) } case "KEYWORD": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else { c.WithFlags = append(c.WithFlags, CanonicalFlag(maybeString(f))) } case "LARGER": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else if n, err := ParseNumber(f); err != nil { return nil, err } else if c.Larger == 0 || n > c.Larger { c.Larger = n } case "NEW": c.WithFlags = append(c.WithFlags, RecentFlag) c.WithoutFlags = append(c.WithoutFlags, SeenFlag) case "NOT": not := new(SearchCriteria) if fields, err = not.parseField(fields, charsetReader); err != nil { return nil, err } c.Not = append(c.Not, not) case "OLD": c.WithoutFlags = append(c.WithoutFlags, RecentFlag) case "ON": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { return nil, err } else { c.Since = t c.Before = t.Add(24 * time.Hour) } case "OR": c1, c2 := new(SearchCriteria), new(SearchCriteria) if fields, err = c1.parseField(fields, charsetReader); err != nil { return nil, err } else if fields, err = c2.parseField(fields, charsetReader); err != nil { return nil, err } c.Or = append(c.Or, [2]*SearchCriteria{c1, c2}) case "SENTBEFORE": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { return nil, err } else if c.SentBefore.IsZero() || t.Before(c.SentBefore) { c.SentBefore = t } case "SENTON": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { return nil, err } else { c.SentSince = t c.SentBefore = t.Add(24 * time.Hour) } case "SENTSINCE": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { return nil, err } else if c.SentSince.IsZero() || t.After(c.SentSince) { c.SentSince = t } case "SINCE": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { return nil, err } else if c.Since.IsZero() || t.After(c.Since) { c.Since = t } case "SMALLER": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else if n, err := ParseNumber(f); err != nil { return nil, err } else if c.Smaller == 0 || n < c.Smaller { c.Smaller = n } case "TEXT": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else { c.Text = append(c.Text, convertField(f, charsetReader)) } case "UID": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else if c.Uid, err = ParseSeqSet(maybeString(f)); err != nil { return nil, err } case "UNANSWERED", "UNDELETED", "UNDRAFT", "UNFLAGGED", "UNSEEN": unflag := strings.TrimPrefix(key, "UN") c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag("\\"+unflag)) case "UNKEYWORD": if f, fields, err = popSearchField(fields); err != nil { return nil, err } else { c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag(maybeString(f))) } default: // Try to parse a sequence set if c.SeqNum, err = ParseSeqSet(key); err != nil { return nil, err } } return fields, nil } // ParseWithCharset parses a search criteria from the provided fields. // charsetReader is an optional function that converts from the fields charset // to UTF-8. func (c *SearchCriteria) ParseWithCharset(fields []interface{}, charsetReader func(io.Reader) io.Reader) error { for len(fields) > 0 { var err error if fields, err = c.parseField(fields, charsetReader); err != nil { return err } } return nil } // Format formats search criteria to fields. UTF-8 is used. func (c *SearchCriteria) Format() []interface{} { var fields []interface{} if c.SeqNum != nil { fields = append(fields, c.SeqNum) } if c.Uid != nil { fields = append(fields, RawString("UID"), c.Uid) } if !c.Since.IsZero() && !c.Before.IsZero() && c.Before.Sub(c.Since) == 24*time.Hour { fields = append(fields, RawString("ON"), searchDate(c.Since)) } else { if !c.Since.IsZero() { fields = append(fields, RawString("SINCE"), searchDate(c.Since)) } if !c.Before.IsZero() { fields = append(fields, RawString("BEFORE"), searchDate(c.Before)) } } if !c.SentSince.IsZero() && !c.SentBefore.IsZero() && c.SentBefore.Sub(c.SentSince) == 24*time.Hour { fields = append(fields, RawString("SENTON"), searchDate(c.SentSince)) } else { if !c.SentSince.IsZero() { fields = append(fields, RawString("SENTSINCE"), searchDate(c.SentSince)) } if !c.SentBefore.IsZero() { fields = append(fields, RawString("SENTBEFORE"), searchDate(c.SentBefore)) } } for key, values := range c.Header { var prefields []interface{} switch key { case "Bcc", "Cc", "From", "Subject", "To": prefields = []interface{}{RawString(strings.ToUpper(key))} default: prefields = []interface{}{RawString("HEADER"), key} } for _, value := range values { fields = append(fields, prefields...) fields = append(fields, value) } } for _, value := range c.Body { fields = append(fields, RawString("BODY"), value) } for _, value := range c.Text { fields = append(fields, RawString("TEXT"), value) } for _, flag := range c.WithFlags { var subfields []interface{} switch flag { case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, RecentFlag, SeenFlag: subfields = []interface{}{RawString(strings.ToUpper(strings.TrimPrefix(flag, "\\")))} default: subfields = []interface{}{RawString("KEYWORD"), RawString(flag)} } fields = append(fields, subfields...) } for _, flag := range c.WithoutFlags { var subfields []interface{} switch flag { case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, SeenFlag: subfields = []interface{}{RawString("UN" + strings.ToUpper(strings.TrimPrefix(flag, "\\")))} case RecentFlag: subfields = []interface{}{RawString("OLD")} default: subfields = []interface{}{RawString("UNKEYWORD"), RawString(flag)} } fields = append(fields, subfields...) } if c.Larger > 0 { fields = append(fields, RawString("LARGER"), c.Larger) } if c.Smaller > 0 { fields = append(fields, RawString("SMALLER"), c.Smaller) } for _, not := range c.Not { fields = append(fields, RawString("NOT"), not.Format()) } for _, or := range c.Or { fields = append(fields, RawString("OR"), or[0].Format(), or[1].Format()) } // Not a single criteria given, add ALL criteria as fallback if len(fields) == 0 { fields = append(fields, RawString("ALL")) } return fields } go-imap-1.2.0/search_test.go000066400000000000000000000076211412725504300157070ustar00rootroot00000000000000package imap import ( "bytes" "io" "net/textproto" "reflect" "strings" "testing" "time" ) // Note to myself: writing these boring tests actually fixed 2 bugs :P var searchSeqSet1, _ = ParseSeqSet("1:42") var searchSeqSet2, _ = ParseSeqSet("743:938") var searchDate1 = time.Date(1997, 11, 21, 0, 0, 0, 0, time.UTC) var searchDate2 = time.Date(1984, 11, 5, 0, 0, 0, 0, time.UTC) var searchCriteriaTests = []struct { expected string criteria *SearchCriteria }{ { expected: `(1:42 UID 743:938 ` + `SINCE "5-Nov-1984" BEFORE "21-Nov-1997" SENTSINCE "5-Nov-1984" SENTBEFORE "21-Nov-1997" ` + `FROM "root@protonmail.com" BODY "hey there" TEXT "DILLE" ` + `ANSWERED DELETED KEYWORD cc UNKEYWORD microsoft ` + `LARGER 4242 SMALLER 4342 ` + `NOT (SENTON "21-Nov-1997" HEADER "Content-Type" "text/csv") ` + `OR (ON "5-Nov-1984" DRAFT FLAGGED UNANSWERED UNDELETED OLD) (UNDRAFT UNFLAGGED UNSEEN))`, criteria: &SearchCriteria{ SeqNum: searchSeqSet1, Uid: searchSeqSet2, Since: searchDate2, Before: searchDate1, SentSince: searchDate2, SentBefore: searchDate1, Header: textproto.MIMEHeader{ "From": {"root@protonmail.com"}, }, Body: []string{"hey there"}, Text: []string{"DILLE"}, WithFlags: []string{AnsweredFlag, DeletedFlag, "cc"}, WithoutFlags: []string{"microsoft"}, Larger: 4242, Smaller: 4342, Not: []*SearchCriteria{{ SentSince: searchDate1, SentBefore: searchDate1.Add(24 * time.Hour), Header: textproto.MIMEHeader{ "Content-Type": {"text/csv"}, }, }}, Or: [][2]*SearchCriteria{{ { Since: searchDate2, Before: searchDate2.Add(24 * time.Hour), WithFlags: []string{DraftFlag, FlaggedFlag}, WithoutFlags: []string{AnsweredFlag, DeletedFlag, RecentFlag}, }, { WithoutFlags: []string{DraftFlag, FlaggedFlag, SeenFlag}, }, }}, }, }, { expected: "(ALL)", criteria: &SearchCriteria{}, }, } func TestSearchCriteria_Format(t *testing.T) { for i, test := range searchCriteriaTests { fields := test.criteria.Format() got, err := formatFields(fields) if err != nil { t.Fatal("Unexpected no error while formatting fields, got:", err) } if got != test.expected { t.Errorf("Invalid search criteria fields for #%v: got \n%v\n instead of \n%v", i+1, got, test.expected) } } } func TestSearchCriteria_Parse(t *testing.T) { for i, test := range searchCriteriaTests { criteria := new(SearchCriteria) b := bytes.NewBuffer([]byte(test.expected)) r := NewReader(b) fields, _ := r.ReadFields() if err := criteria.ParseWithCharset(fields[0].([]interface{}), nil); err != nil { t.Errorf("Cannot parse search criteria for #%v: %v", i+1, err) } else if !reflect.DeepEqual(criteria, test.criteria) { t.Errorf("Invalid search criteria for #%v: got \n%+v\n instead of \n%+v", i+1, criteria, test.criteria) } } } var searchCriteriaParseTests = []struct { fields []interface{} criteria *SearchCriteria charset func(io.Reader) io.Reader }{ { fields: []interface{}{"ALL"}, criteria: &SearchCriteria{}, }, { fields: []interface{}{"NEW"}, criteria: &SearchCriteria{ WithFlags: []string{RecentFlag}, WithoutFlags: []string{SeenFlag}, }, }, { fields: []interface{}{"SUBJECT", strings.NewReader("café")}, criteria: &SearchCriteria{ Header: textproto.MIMEHeader{"Subject": {"café"}}, }, charset: func(r io.Reader) io.Reader { return r }, }, } func TestSearchCriteria_Parse_others(t *testing.T) { for i, test := range searchCriteriaParseTests { criteria := new(SearchCriteria) if err := criteria.ParseWithCharset(test.fields, test.charset); err != nil { t.Errorf("Cannot parse search criteria for #%v: %v", i+1, err) } else if !reflect.DeepEqual(criteria, test.criteria) { t.Errorf("Invalid search criteria for #%v: got \n%+v\n instead of \n%+v", i+1, criteria, test.criteria) } } } go-imap-1.2.0/seqset.go000066400000000000000000000200371412725504300147030ustar00rootroot00000000000000package imap import ( "fmt" "strconv" "strings" ) // ErrBadSeqSet is used to report problems with the format of a sequence set // value. type ErrBadSeqSet string func (err ErrBadSeqSet) Error() string { return fmt.Sprintf("imap: bad sequence set value %q", string(err)) } // Seq represents a single seq-number or seq-range value (RFC 3501 ABNF). Values // may be static (e.g. "1", "2:4") or dynamic (e.g. "*", "1:*"). A seq-number is // represented by setting Start = Stop. Zero is used to represent "*", which is // safe because seq-number uses nz-number rule. The order of values is always // Start <= Stop, except when representing "n:*", where Start = n and Stop = 0. type Seq struct { Start, Stop uint32 } // parseSeqNumber parses a single seq-number value (non-zero uint32 or "*"). func parseSeqNumber(v string) (uint32, error) { if n, err := strconv.ParseUint(v, 10, 32); err == nil && v[0] != '0' { return uint32(n), nil } else if v == "*" { return 0, nil } return 0, ErrBadSeqSet(v) } // parseSeq creates a new seq instance by parsing strings in the format "n" or // "n:m", where n and/or m may be "*". An error is returned for invalid values. func parseSeq(v string) (s Seq, err error) { if sep := strings.IndexRune(v, ':'); sep < 0 { s.Start, err = parseSeqNumber(v) s.Stop = s.Start return } else if s.Start, err = parseSeqNumber(v[:sep]); err == nil { if s.Stop, err = parseSeqNumber(v[sep+1:]); err == nil { if (s.Stop < s.Start && s.Stop != 0) || s.Start == 0 { s.Start, s.Stop = s.Stop, s.Start } return } } return s, ErrBadSeqSet(v) } // Contains returns true if the seq-number q is contained in sequence value s. // The dynamic value "*" contains only other "*" values, the dynamic range "n:*" // contains "*" and all numbers >= n. func (s Seq) Contains(q uint32) bool { if q == 0 { return s.Stop == 0 // "*" is contained only in "*" and "n:*" } return s.Start != 0 && s.Start <= q && (q <= s.Stop || s.Stop == 0) } // Less returns true if s precedes and does not contain seq-number q. func (s Seq) Less(q uint32) bool { return (s.Stop < q || q == 0) && s.Stop != 0 } // Merge combines sequence values s and t into a single union if the two // intersect or one is a superset of the other. The order of s and t does not // matter. If the values cannot be merged, s is returned unmodified and ok is // set to false. func (s Seq) Merge(t Seq) (union Seq, ok bool) { if union = s; s == t { ok = true return } if s.Start != 0 && t.Start != 0 { // s and t are any combination of "n", "n:m", or "n:*" if s.Start > t.Start { s, t = t, s } // s starts at or before t, check where it ends if (s.Stop >= t.Stop && t.Stop != 0) || s.Stop == 0 { return s, true // s is a superset of t } // s is "n" or "n:m", if m == ^uint32(0) then t is "n:*" if s.Stop+1 >= t.Start || s.Stop == ^uint32(0) { return Seq{s.Start, t.Stop}, true // s intersects or touches t } return } // exactly one of s and t is "*" if s.Start == 0 { if t.Stop == 0 { return t, true // s is "*", t is "n:*" } } else if s.Stop == 0 { return s, true // s is "n:*", t is "*" } return } // String returns sequence value s as a seq-number or seq-range string. func (s Seq) String() string { if s.Start == s.Stop { if s.Start == 0 { return "*" } return strconv.FormatUint(uint64(s.Start), 10) } b := strconv.AppendUint(make([]byte, 0, 24), uint64(s.Start), 10) if s.Stop == 0 { return string(append(b, ':', '*')) } return string(strconv.AppendUint(append(b, ':'), uint64(s.Stop), 10)) } // SeqSet is used to represent a set of message sequence numbers or UIDs (see // sequence-set ABNF rule). The zero value is an empty set. type SeqSet struct { Set []Seq } // ParseSeqSet returns a new SeqSet instance after parsing the set string. func ParseSeqSet(set string) (s *SeqSet, err error) { s = new(SeqSet) return s, s.Add(set) } // Add inserts new sequence values into the set. The string format is described // by RFC 3501 sequence-set ABNF rule. If an error is encountered, all values // inserted successfully prior to the error remain in the set. func (s *SeqSet) Add(set string) error { for _, sv := range strings.Split(set, ",") { v, err := parseSeq(sv) if err != nil { return err } s.insert(v) } return nil } // AddNum inserts new sequence numbers into the set. The value 0 represents "*". func (s *SeqSet) AddNum(q ...uint32) { for _, v := range q { s.insert(Seq{v, v}) } } // AddRange inserts a new sequence range into the set. func (s *SeqSet) AddRange(Start, Stop uint32) { if (Stop < Start && Stop != 0) || Start == 0 { s.insert(Seq{Stop, Start}) } else { s.insert(Seq{Start, Stop}) } } // AddSet inserts all values from t into s. func (s *SeqSet) AddSet(t *SeqSet) { for _, v := range t.Set { s.insert(v) } } // Clear removes all values from the set. func (s *SeqSet) Clear() { s.Set = s.Set[:0] } // Empty returns true if the sequence set does not contain any values. func (s SeqSet) Empty() bool { return len(s.Set) == 0 } // Dynamic returns true if the set contains "*" or "n:*" values. func (s SeqSet) Dynamic() bool { return len(s.Set) > 0 && s.Set[len(s.Set)-1].Stop == 0 } // Contains returns true if the non-zero sequence number or UID q is contained // in the set. The dynamic range "n:*" contains all q >= n. It is the caller's // responsibility to handle the special case where q is the maximum UID in the // mailbox and q < n (i.e. the set cannot match UIDs against "*:n" or "*" since // it doesn't know what the maximum value is). func (s SeqSet) Contains(q uint32) bool { if _, ok := s.search(q); ok { return q != 0 } return false } // String returns a sorted representation of all contained sequence values. func (s SeqSet) String() string { if len(s.Set) == 0 { return "" } b := make([]byte, 0, 64) for _, v := range s.Set { b = append(b, ',') if v.Start == 0 { b = append(b, '*') continue } b = strconv.AppendUint(b, uint64(v.Start), 10) if v.Start != v.Stop { if v.Stop == 0 { b = append(b, ':', '*') continue } b = strconv.AppendUint(append(b, ':'), uint64(v.Stop), 10) } } return string(b[1:]) } // insert adds sequence value v to the set. func (s *SeqSet) insert(v Seq) { i, _ := s.search(v.Start) merged := false if i > 0 { // try merging with the preceding entry (e.g. "1,4".insert(2), i == 1) s.Set[i-1], merged = s.Set[i-1].Merge(v) } if i == len(s.Set) { // v was either merged with the last entry or needs to be appended if !merged { s.insertAt(i, v) } return } else if merged { i-- } else if s.Set[i], merged = s.Set[i].Merge(v); !merged { s.insertAt(i, v) // insert in the middle (e.g. "1,5".insert(3), i == 1) return } // v was merged with s.Set[i], continue trying to merge until the end for j := i + 1; j < len(s.Set); j++ { if s.Set[i], merged = s.Set[i].Merge(s.Set[j]); !merged { if j > i+1 { // cut out all entries between i and j that were merged s.Set = append(s.Set[:i+1], s.Set[j:]...) } return } } // everything after s.Set[i] was merged s.Set = s.Set[:i+1] } // insertAt inserts a new sequence value v at index i, resizing s.Set as needed. func (s *SeqSet) insertAt(i int, v Seq) { if n := len(s.Set); i == n { // insert at the end s.Set = append(s.Set, v) return } else if n < cap(s.Set) { // enough space, shift everything at and after i to the right s.Set = s.Set[:n+1] copy(s.Set[i+1:], s.Set[i:]) } else { // allocate new slice and copy everything, n is at least 1 set := make([]Seq, n+1, n*2) copy(set, s.Set[:i]) copy(set[i+1:], s.Set[i:]) s.Set = set } s.Set[i] = v } // search attempts to find the index of the sequence set value that contains q. // If no values contain q, the returned index is the position where q should be // inserted and ok is set to false. func (s SeqSet) search(q uint32) (i int, ok bool) { min, max := 0, len(s.Set)-1 for min < max { if mid := (min + max) >> 1; s.Set[mid].Less(q) { min = mid + 1 } else { max = mid } } if max < 0 || s.Set[min].Less(q) { return len(s.Set), false // q is the new largest value } return min, s.Set[min].Contains(q) } go-imap-1.2.0/seqset_test.go000066400000000000000000000441111412725504300157410ustar00rootroot00000000000000package imap import ( "math/rand" "strings" "testing" ) const max = ^uint32(0) func TestSeqParser(t *testing.T) { tests := []struct { in string out Seq ok bool }{ // Invalid number {"", Seq{}, false}, {" ", Seq{}, false}, {"A", Seq{}, false}, {"0", Seq{}, false}, {" 1", Seq{}, false}, {"1 ", Seq{}, false}, {"*1", Seq{}, false}, {"1*", Seq{}, false}, {"-1", Seq{}, false}, {"01", Seq{}, false}, {"0x1", Seq{}, false}, {"1 2", Seq{}, false}, {"1,2", Seq{}, false}, {"1.2", Seq{}, false}, {"4294967296", Seq{}, false}, // Valid number {"*", Seq{0, 0}, true}, {"1", Seq{1, 1}, true}, {"42", Seq{42, 42}, true}, {"1000", Seq{1000, 1000}, true}, {"4294967295", Seq{max, max}, true}, // Invalid range {":", Seq{}, false}, {"*:", Seq{}, false}, {":*", Seq{}, false}, {"1:", Seq{}, false}, {":1", Seq{}, false}, {"0:0", Seq{}, false}, {"0:*", Seq{}, false}, {"0:1", Seq{}, false}, {"1:0", Seq{}, false}, {"1:2 ", Seq{}, false}, {"1: 2", Seq{}, false}, {"1:2:", Seq{}, false}, {"1:2,", Seq{}, false}, {"1:2:3", Seq{}, false}, {"1:2,3", Seq{}, false}, {"*:4294967296", Seq{}, false}, {"0:4294967295", Seq{}, false}, {"1:4294967296", Seq{}, false}, {"4294967296:*", Seq{}, false}, {"4294967295:0", Seq{}, false}, {"4294967296:1", Seq{}, false}, {"4294967295:4294967296", Seq{}, false}, // Valid range {"*:*", Seq{0, 0}, true}, {"1:*", Seq{1, 0}, true}, {"*:1", Seq{1, 0}, true}, {"2:2", Seq{2, 2}, true}, {"2:42", Seq{2, 42}, true}, {"42:2", Seq{2, 42}, true}, {"*:4294967294", Seq{max - 1, 0}, true}, {"*:4294967295", Seq{max, 0}, true}, {"4294967294:*", Seq{max - 1, 0}, true}, {"4294967295:*", Seq{max, 0}, true}, {"1:4294967294", Seq{1, max - 1}, true}, {"1:4294967295", Seq{1, max}, true}, {"4294967295:1000", Seq{1000, max}, true}, {"4294967294:4294967295", Seq{max - 1, max}, true}, {"4294967295:4294967295", Seq{max, max}, true}, } for _, test := range tests { out, err := parseSeq(test.in) if !test.ok { if err == nil { t.Errorf("parseSeq(%q) expected error; got %q", test.in, out) } } else if err != nil { t.Errorf("parseSeq(%q) expected %q; got %v", test.in, test.out, err) } else if out != test.out { t.Errorf("parseSeq(%q) expected %q; got %q", test.in, test.out, out) } } } func TestSeqContainsLess(t *testing.T) { tests := []struct { s string q uint32 contains bool less bool }{ {"2", 0, false, true}, {"2", 1, false, false}, {"2", 2, true, false}, {"2", 3, false, true}, {"2", max, false, true}, {"*", 0, true, false}, {"*", 1, false, false}, {"*", 2, false, false}, {"*", 3, false, false}, {"*", max, false, false}, {"2:3", 0, false, true}, {"2:3", 1, false, false}, {"2:3", 2, true, false}, {"2:3", 3, true, false}, {"2:3", 4, false, true}, {"2:3", 5, false, true}, {"2:4", 0, false, true}, {"2:4", 1, false, false}, {"2:4", 2, true, false}, {"2:4", 3, true, false}, {"2:4", 4, true, false}, {"2:4", 5, false, true}, {"4:4294967295", 0, false, true}, {"4:4294967295", 1, false, false}, {"4:4294967295", 2, false, false}, {"4:4294967295", 3, false, false}, {"4:4294967295", 4, true, false}, {"4:4294967295", 5, true, false}, {"4:4294967295", max, true, false}, {"4:*", 0, true, false}, {"4:*", 1, false, false}, {"4:*", 2, false, false}, {"4:*", 3, false, false}, {"4:*", 4, true, false}, {"4:*", 5, true, false}, {"4:*", max, true, false}, } for _, test := range tests { s, err := parseSeq(test.s) if err != nil { t.Errorf("parseSeq(%q) unexpected error; %v", test.s, err) continue } if s.Contains(test.q) != test.contains { t.Errorf("%q.Contains(%d) expected %v", test.s, test.q, test.contains) } if s.Less(test.q) != test.less { t.Errorf("%q.Less(%d) expected %v", test.s, test.q, test.less) } } } func TestSeqMerge(T *testing.T) { tests := []struct { s, t, out string }{ // Number with number {"1", "1", "1"}, {"1", "2", "1:2"}, {"1", "3", ""}, {"1", "4294967295", ""}, {"1", "*", ""}, {"4", "1", ""}, {"4", "2", ""}, {"4", "3", "3:4"}, {"4", "4", "4"}, {"4", "5", "4:5"}, {"4", "6", ""}, {"4294967295", "4294967293", ""}, {"4294967295", "4294967294", "4294967294:4294967295"}, {"4294967295", "4294967295", "4294967295"}, {"4294967295", "*", ""}, {"*", "1", ""}, {"*", "2", ""}, {"*", "4294967294", ""}, {"*", "4294967295", ""}, {"*", "*", "*"}, // Range with number {"1:3", "1", "1:3"}, {"1:3", "2", "1:3"}, {"1:3", "3", "1:3"}, {"1:3", "4", "1:4"}, {"1:3", "5", ""}, {"1:3", "*", ""}, {"3:4", "1", ""}, {"3:4", "2", "2:4"}, {"3:4", "3", "3:4"}, {"3:4", "4", "3:4"}, {"3:4", "5", "3:5"}, {"3:4", "6", ""}, {"3:4", "*", ""}, {"2:3", "5", ""}, {"2:4", "5", "2:5"}, {"2:5", "5", "2:5"}, {"2:6", "5", "2:6"}, {"2:7", "5", "2:7"}, {"2:*", "5", "2:*"}, {"3:4", "5", "3:5"}, {"3:5", "5", "3:5"}, {"3:6", "5", "3:6"}, {"3:7", "5", "3:7"}, {"3:*", "5", "3:*"}, {"4:5", "5", "4:5"}, {"4:6", "5", "4:6"}, {"4:7", "5", "4:7"}, {"4:*", "5", "4:*"}, {"5:6", "5", "5:6"}, {"5:7", "5", "5:7"}, {"5:*", "5", "5:*"}, {"6:7", "5", "5:7"}, {"6:*", "5", "5:*"}, {"7:8", "5", ""}, {"7:*", "5", ""}, {"3:4294967294", "1", ""}, {"3:4294967294", "2", "2:4294967294"}, {"3:4294967294", "3", "3:4294967294"}, {"3:4294967294", "4", "3:4294967294"}, {"3:4294967294", "4294967293", "3:4294967294"}, {"3:4294967294", "4294967294", "3:4294967294"}, {"3:4294967294", "4294967295", "3:4294967295"}, {"3:4294967294", "*", ""}, {"3:4294967295", "1", ""}, {"3:4294967295", "2", "2:4294967295"}, {"3:4294967295", "3", "3:4294967295"}, {"3:4294967295", "4", "3:4294967295"}, {"3:4294967295", "4294967294", "3:4294967295"}, {"3:4294967295", "4294967295", "3:4294967295"}, {"3:4294967295", "*", ""}, {"1:4294967295", "1", "1:4294967295"}, {"1:4294967295", "4294967295", "1:4294967295"}, {"1:4294967295", "*", ""}, {"1:*", "1", "1:*"}, {"1:*", "2", "1:*"}, {"1:*", "4294967294", "1:*"}, {"1:*", "4294967295", "1:*"}, {"1:*", "*", "1:*"}, // Range with range {"5:8", "1:2", ""}, {"5:8", "1:3", ""}, {"5:8", "1:4", "1:8"}, {"5:8", "1:5", "1:8"}, {"5:8", "1:6", "1:8"}, {"5:8", "1:7", "1:8"}, {"5:8", "1:8", "1:8"}, {"5:8", "1:9", "1:9"}, {"5:8", "1:10", "1:10"}, {"5:8", "1:11", "1:11"}, {"5:8", "1:*", "1:*"}, {"5:8", "2:3", ""}, {"5:8", "2:4", "2:8"}, {"5:8", "2:5", "2:8"}, {"5:8", "2:6", "2:8"}, {"5:8", "2:7", "2:8"}, {"5:8", "2:8", "2:8"}, {"5:8", "2:9", "2:9"}, {"5:8", "2:10", "2:10"}, {"5:8", "2:11", "2:11"}, {"5:8", "2:*", "2:*"}, {"5:8", "3:4", "3:8"}, {"5:8", "3:5", "3:8"}, {"5:8", "3:6", "3:8"}, {"5:8", "3:7", "3:8"}, {"5:8", "3:8", "3:8"}, {"5:8", "3:9", "3:9"}, {"5:8", "3:10", "3:10"}, {"5:8", "3:11", "3:11"}, {"5:8", "3:*", "3:*"}, {"5:8", "4:5", "4:8"}, {"5:8", "4:6", "4:8"}, {"5:8", "4:7", "4:8"}, {"5:8", "4:8", "4:8"}, {"5:8", "4:9", "4:9"}, {"5:8", "4:10", "4:10"}, {"5:8", "4:11", "4:11"}, {"5:8", "4:*", "4:*"}, {"5:8", "5:6", "5:8"}, {"5:8", "5:7", "5:8"}, {"5:8", "5:8", "5:8"}, {"5:8", "5:9", "5:9"}, {"5:8", "5:10", "5:10"}, {"5:8", "5:11", "5:11"}, {"5:8", "5:*", "5:*"}, {"5:8", "6:7", "5:8"}, {"5:8", "6:8", "5:8"}, {"5:8", "6:9", "5:9"}, {"5:8", "6:10", "5:10"}, {"5:8", "6:11", "5:11"}, {"5:8", "6:*", "5:*"}, {"5:8", "7:8", "5:8"}, {"5:8", "7:9", "5:9"}, {"5:8", "7:10", "5:10"}, {"5:8", "7:11", "5:11"}, {"5:8", "7:*", "5:*"}, {"5:8", "8:9", "5:9"}, {"5:8", "8:10", "5:10"}, {"5:8", "8:11", "5:11"}, {"5:8", "8:*", "5:*"}, {"5:8", "9:10", "5:10"}, {"5:8", "9:11", "5:11"}, {"5:8", "9:*", "5:*"}, {"5:8", "10:11", ""}, {"5:8", "10:*", ""}, {"1:*", "1:*", "1:*"}, {"1:*", "2:*", "1:*"}, {"1:*", "1:4294967294", "1:*"}, {"1:*", "1:4294967295", "1:*"}, {"1:*", "2:4294967295", "1:*"}, {"1:4294967295", "1:4294967294", "1:4294967295"}, {"1:4294967295", "1:4294967295", "1:4294967295"}, {"1:4294967295", "2:4294967295", "1:4294967295"}, {"1:4294967295", "2:*", "1:*"}, } for _, test := range tests { s, err := parseSeq(test.s) if err != nil { T.Errorf("parseSeq(%q) unexpected error; %v", test.s, err) continue } t, err := parseSeq(test.t) if err != nil { T.Errorf("parseSeq(%q) unexpected error; %v", test.t, err) continue } test_ok := test.out != "" for i := 0; i < 2; i++ { if !test_ok { test.out = test.s } out, ok := s.Merge(t) if out.String() != test.out || ok != test_ok { T.Errorf("%q.Merge(%q) expected %q; got %q", test.s, test.t, test.out, out) } // Swap s & t, result should be identical test.s, test.t = test.t, test.s s, t = t, s } } } func checkSeqSet(s *SeqSet, t *testing.T) { n := len(s.Set) for i, v := range s.Set { if v.Start == 0 { if v.Stop != 0 { t.Errorf(`SeqSet(%q) index %d: "*:n" range`, s, i) } else if i != n-1 { t.Errorf(`SeqSet(%q) index %d: "*" not at the end`, s, i) } continue } if i > 0 && s.Set[i-1].Stop >= v.Start-1 { t.Errorf(`SeqSet(%q) index %d: overlap`, s, i) } if v.Stop < v.Start { if v.Stop != 0 { t.Errorf(`SeqSet(%q) index %d: reversed range`, s, i) } else if i != n-1 { t.Errorf(`SeqSet(%q) index %d: "n:*" not at the end`, s, i) } } } } func TestSeqSetInfo(t *testing.T) { tests := []struct { s string q uint32 contains bool }{ {"", 0, false}, {"", 1, false}, {"", 2, false}, {"", 3, false}, {"", max, false}, {"2", 0, false}, {"2", 1, false}, {"2", 2, true}, {"2", 3, false}, {"2", max, false}, {"*", 0, false}, // Contains("*") is always false, use Dynamic() instead {"*", 1, false}, {"*", 2, false}, {"*", 3, false}, {"*", max, false}, {"1:*", 0, false}, {"1:*", 1, true}, {"1:*", max, true}, {"2:4", 0, false}, {"2:4", 1, false}, {"2:4", 2, true}, {"2:4", 3, true}, {"2:4", 4, true}, {"2:4", 5, false}, {"2:4", max, false}, {"2,4", 0, false}, {"2,4", 1, false}, {"2,4", 2, true}, {"2,4", 3, false}, {"2,4", 4, true}, {"2,4", 5, false}, {"2,4", max, false}, {"2:4,6", 0, false}, {"2:4,6", 1, false}, {"2:4,6", 2, true}, {"2:4,6", 3, true}, {"2:4,6", 4, true}, {"2:4,6", 5, false}, {"2:4,6", 6, true}, {"2:4,6", 7, false}, {"2,4:6", 0, false}, {"2,4:6", 1, false}, {"2,4:6", 2, true}, {"2,4:6", 3, false}, {"2,4:6", 4, true}, {"2,4:6", 5, true}, {"2,4:6", 6, true}, {"2,4:6", 7, false}, {"2,4,6", 0, false}, {"2,4,6", 1, false}, {"2,4,6", 2, true}, {"2,4,6", 3, false}, {"2,4,6", 4, true}, {"2,4,6", 5, false}, {"2,4,6", 6, true}, {"2,4,6", 7, false}, {"1,3:5,7,9:*", 0, false}, {"1,3:5,7,9:*", 1, true}, {"1,3:5,7,9:*", 2, false}, {"1,3:5,7,9:*", 3, true}, {"1,3:5,7,9:*", 4, true}, {"1,3:5,7,9:*", 5, true}, {"1,3:5,7,9:*", 6, false}, {"1,3:5,7,9:*", 7, true}, {"1,3:5,7,9:*", 8, false}, {"1,3:5,7,9:*", 9, true}, {"1,3:5,7,9:*", 10, true}, {"1,3:5,7,9:*", max, true}, {"1,3:5,7,9,42", 0, false}, {"1,3:5,7,9,42", 1, true}, {"1,3:5,7,9,42", 2, false}, {"1,3:5,7,9,42", 3, true}, {"1,3:5,7,9,42", 4, true}, {"1,3:5,7,9,42", 5, true}, {"1,3:5,7,9,42", 6, false}, {"1,3:5,7,9,42", 7, true}, {"1,3:5,7,9,42", 8, false}, {"1,3:5,7,9,42", 9, true}, {"1,3:5,7,9,42", 10, false}, {"1,3:5,7,9,42", 41, false}, {"1,3:5,7,9,42", 42, true}, {"1,3:5,7,9,42", 43, false}, {"1,3:5,7,9,42", max, false}, {"1,3:5,7,9,42,*", 0, false}, {"1,3:5,7,9,42,*", 1, true}, {"1,3:5,7,9,42,*", 2, false}, {"1,3:5,7,9,42,*", 3, true}, {"1,3:5,7,9,42,*", 4, true}, {"1,3:5,7,9,42,*", 5, true}, {"1,3:5,7,9,42,*", 6, false}, {"1,3:5,7,9,42,*", 7, true}, {"1,3:5,7,9,42,*", 8, false}, {"1,3:5,7,9,42,*", 9, true}, {"1,3:5,7,9,42,*", 10, false}, {"1,3:5,7,9,42,*", 41, false}, {"1,3:5,7,9,42,*", 42, true}, {"1,3:5,7,9,42,*", 43, false}, {"1,3:5,7,9,42,*", max, false}, {"1,3:5,7,9,42,60:70,100:*", 0, false}, {"1,3:5,7,9,42,60:70,100:*", 1, true}, {"1,3:5,7,9,42,60:70,100:*", 2, false}, {"1,3:5,7,9,42,60:70,100:*", 3, true}, {"1,3:5,7,9,42,60:70,100:*", 4, true}, {"1,3:5,7,9,42,60:70,100:*", 5, true}, {"1,3:5,7,9,42,60:70,100:*", 6, false}, {"1,3:5,7,9,42,60:70,100:*", 7, true}, {"1,3:5,7,9,42,60:70,100:*", 8, false}, {"1,3:5,7,9,42,60:70,100:*", 9, true}, {"1,3:5,7,9,42,60:70,100:*", 10, false}, {"1,3:5,7,9,42,60:70,100:*", 41, false}, {"1,3:5,7,9,42,60:70,100:*", 42, true}, {"1,3:5,7,9,42,60:70,100:*", 43, false}, {"1,3:5,7,9,42,60:70,100:*", 59, false}, {"1,3:5,7,9,42,60:70,100:*", 60, true}, {"1,3:5,7,9,42,60:70,100:*", 65, true}, {"1,3:5,7,9,42,60:70,100:*", 70, true}, {"1,3:5,7,9,42,60:70,100:*", 71, false}, {"1,3:5,7,9,42,60:70,100:*", 99, false}, {"1,3:5,7,9,42,60:70,100:*", 100, true}, {"1,3:5,7,9,42,60:70,100:*", 1000, true}, {"1,3:5,7,9,42,60:70,100:*", max, true}, } for _, test := range tests { s, _ := ParseSeqSet(test.s) checkSeqSet(s, t) if s.Contains(test.q) != test.contains { t.Errorf("%q.Contains(%v) expected %v", test.s, test.q, test.contains) } if str := s.String(); str != test.s { t.Errorf("%q.String() expected %q; got %q", test.s, test.s, str) } test_empty := len(test.s) == 0 if s.Empty() != test_empty { t.Errorf("%q.Empty() expected %v", test.s, test_empty) } test_dynamic := !test_empty && test.s[len(test.s)-1] == '*' if s.Dynamic() != test_dynamic { t.Errorf("%q.Dynamic() expected %v", test.s, test_dynamic) } } } func TestSeqSetAdd(t *testing.T) { tests := []struct { in string out string }{ {"1,1", "1"}, {"1,2", "1:2"}, {"1,3", "1,3"}, {"1,*", "1,*"}, {"1,1,1", "1"}, {"1,1,2", "1:2"}, {"1,1:2", "1:2"}, {"1,1,3", "1,3"}, {"1,1:3", "1:3"}, {"1,2,2", "1:2"}, {"1,2,3", "1:3"}, {"1,2:3", "1:3"}, {"1,2,4", "1:2,4"}, {"1,3,3", "1,3"}, {"1,3,4", "1,3:4"}, {"1,3:4", "1,3:4"}, {"1,3,5", "1,3,5"}, {"1,3:5", "1,3:5"}, {"1:3,5", "1:3,5"}, {"1:5,3", "1:5"}, {"1,2,3,4", "1:4"}, {"1,2,4,5", "1:2,4:5"}, {"1,2,4:5", "1:2,4:5"}, {"1:2,4:5", "1:2,4:5"}, {"1,2,3,4,5", "1:5"}, {"1,2:3,4:5", "1:5"}, {"1,2,4,5,7,9", "1:2,4:5,7,9"}, {"1,2,4,5,7:9", "1:2,4:5,7:9"}, {"1:2,4:5,7:9", "1:2,4:5,7:9"}, {"1,2,4,5,7,8,9", "1:2,4:5,7:9"}, {"1:2,4:5,7,8,9", "1:2,4:5,7:9"}, {"3,5:10,15:20", "3,5:10,15:20"}, {"4,5:10,15:20", "4:10,15:20"}, {"5,5:10,15:20", "5:10,15:20"}, {"7,5:10,15:20", "5:10,15:20"}, {"10,5:10,15:20", "5:10,15:20"}, {"11,5:10,15:20", "5:11,15:20"}, {"12,5:10,15:20", "5:10,12,15:20"}, {"14,5:10,15:20", "5:10,14:20"}, {"17,5:10,15:20", "5:10,15:20"}, {"21,5:10,15:20", "5:10,15:21"}, {"22,5:10,15:20", "5:10,15:20,22"}, {"*,5:10,15:20", "5:10,15:20,*"}, {"1:3,5:10,15:20", "1:3,5:10,15:20"}, {"1:4,5:10,15:20", "1:10,15:20"}, {"1:8,5:10,15:20", "1:10,15:20"}, {"1:13,5:10,15:20", "1:13,15:20"}, {"1:14,5:10,15:20", "1:20"}, {"7:17,5:10,15:20", "5:20"}, {"11:14,5:10,15:20", "5:20"}, {"12,13,5:10,15:20", "5:10,12:13,15:20"}, {"12:13,5:10,15:20", "5:10,12:13,15:20"}, {"12:14,5:10,15:20", "5:10,12:20"}, {"11:13,5:10,15:20", "5:13,15:20"}, {"11,12,13,14,5:10,15:20", "5:20"}, {"1:*,5:10,15:20", "1:*"}, {"4:*,5:10,15:20", "4:*"}, {"6:*,5:10,15:20", "5:*"}, {"12:*,5:10,15:20", "5:10,12:*"}, {"19:*,5:10,15:20", "5:10,15:*"}, {"5:8,6,7:10,15,16,17,18:20,19,21:*", "5:10,15:*"}, {"4:13,1,5,10,15,20", "1,4:13,15,20"}, {"4:14,1,5,10,15,20", "1,4:15,20"}, {"4:15,1,5,10,15,20", "1,4:15,20"}, {"4:16,1,5,10,15,20", "1,4:16,20"}, {"4:17,1,5,10,15,20", "1,4:17,20"}, {"4:18,1,5,10,15,20", "1,4:18,20"}, {"4:19,1,5,10,15,20", "1,4:20"}, {"4:20,1,5,10,15,20", "1,4:20"}, {"4:21,1,5,10,15,20", "1,4:21"}, {"4:*,1,5,10,15,20", "1,4:*"}, {"1,3,5,7,9,11,13,15,17,19", "1,3,5,7,9,11,13,15,17,19"}, {"1,3,5,7,9,11:13,15,17,19", "1,3,5,7,9,11:13,15,17,19"}, {"1,3,5,7,9:11,13:15,17,19", "1,3,5,7,9:11,13:15,17,19"}, {"1,3,5,7:9,11:13,15:17,19", "1,3,5,7:9,11:13,15:17,19"}, {"1,3,5,7,9,11,13,15,17,19,*", "1,3,5,7,9,11,13,15,17,19,*"}, {"1,3,5,7,9,11,13,15,17,19:*", "1,3,5,7,9,11,13,15,17,19:*"}, {"1:20,3,5,7,9,11,13,15,17,19,*", "1:20,*"}, {"1:20,3,5,7,9,11,13,15,17,19:*", "1:*"}, {"4294967295,*", "4294967295,*"}, {"1,4294967295,*", "1,4294967295,*"}, {"1:4294967295,*", "1:4294967295,*"}, {"1,4294967295:*", "1,4294967295:*"}, {"1:*,4294967295", "1:*"}, {"1:*,4294967295:*", "1:*"}, {"1:4294967295,4294967295:*", "1:*"}, } prng := rand.New(rand.NewSource(19860201)) done := make(map[string]bool) permute := func(in string) string { v := strings.Split(in, ",") r := make([]string, len(v)) // Try to find a permutation that hasn't been checked already for i := 0; i < 50; i++ { for i, j := range prng.Perm(len(v)) { r[i] = v[j] } if s := strings.Join(r, ","); !done[s] { done[s] = true return s } } return "" } for _, test := range tests { for i := 0; i < 100 && test.in != ""; i++ { s := &SeqSet{} if err := s.Add(test.in); err != nil { t.Errorf("Add(%q) unexpected error; %v", test.in, err) i = 100 } checkSeqSet(s, t) if out := s.String(); out != test.out { t.Errorf("%q.String() expected %q; got %q", test.in, test.out, out) i = 100 } test.in = permute(test.in) } } } func TestSeqSetAddNumRangeSet(t *testing.T) { type num []uint32 tests := []struct { num num rng Seq set string out string }{ {num{5}, Seq{1, 3}, "1:2,5,7:13,15,17:*", "1:3,5,7:13,15,17:*"}, {num{5}, Seq{3, 1}, "2:3,7:13,15,17:*", "1:3,5,7:13,15,17:*"}, {num{15}, Seq{17, 0}, "1:3,5,7:13", "1:3,5,7:13,15,17:*"}, {num{15}, Seq{0, 17}, "1:3,5,7:13", "1:3,5,7:13,15,17:*"}, {num{1, 3, 5, 7, 9, 11, 0}, Seq{8, 13}, "2,15,17:*", "1:3,5,7:13,15,17:*"}, {num{5, 1, 7, 3, 9, 0, 11}, Seq{8, 13}, "2,15,17:*", "1:3,5,7:13,15,17:*"}, {num{5, 1, 7, 3, 9, 0, 11}, Seq{13, 8}, "2,15,17:*", "1:3,5,7:13,15,17:*"}, } for _, test := range tests { other, _ := ParseSeqSet(test.set) s := &SeqSet{} s.AddNum(test.num...) checkSeqSet(s, t) s.AddRange(test.rng.Start, test.rng.Stop) checkSeqSet(s, t) s.AddSet(other) checkSeqSet(s, t) if out := s.String(); out != test.out { t.Errorf("(%v + %v + %q).String() expected %q; got %q", test.num, test.rng, test.set, test.out, out) } } } go-imap-1.2.0/server/000077500000000000000000000000001412725504300143545ustar00rootroot00000000000000go-imap-1.2.0/server/cmd_any.go000066400000000000000000000017471412725504300163260ustar00rootroot00000000000000package server import ( "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" ) type Capability struct { commands.Capability } func (cmd *Capability) Handle(conn Conn) error { res := &responses.Capability{Caps: conn.Capabilities()} return conn.WriteResp(res) } type Noop struct { commands.Noop } func (cmd *Noop) Handle(conn Conn) error { ctx := conn.Context() if ctx.Mailbox != nil { // If a mailbox is selected, NOOP can be used to poll for server updates if mbox, ok := ctx.Mailbox.(backend.MailboxPoller); ok { return mbox.Poll() } } return nil } type Logout struct { commands.Logout } func (cmd *Logout) Handle(conn Conn) error { res := &imap.StatusResp{ Type: imap.StatusRespBye, Info: "Closing connection", } if err := conn.WriteResp(res); err != nil { return err } // Request to close the connection conn.Context().State = imap.LogoutState return nil } go-imap-1.2.0/server/cmd_any_test.go000066400000000000000000000053561412725504300173650ustar00rootroot00000000000000package server_test import ( "bufio" "io" "net" "strings" "testing" "github.com/emersion/go-imap/server" "github.com/emersion/go-sasl" ) func testServerGreeted(t *testing.T) (s *server.Server, c net.Conn, scanner *bufio.Scanner) { s, c = testServer(t) scanner = bufio.NewScanner(c) scanner.Scan() // Greeting return } func TestCapability(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 CAPABILITY\r\n") scanner.Scan() if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN" { t.Fatal("Bad capability:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } } func TestNoop(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 NOOP\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } } func TestLogout(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 LOGOUT\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "* BYE ") { t.Fatal("Bad BYE response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } } type xnoop struct{} func (ext *xnoop) Capabilities(server.Conn) []string { return []string{"XNOOP"} } func (ext *xnoop) Command(string) server.HandlerFactory { return nil } func TestServer_Enable(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() s.Enable(&xnoop{}) io.WriteString(c, "a001 CAPABILITY\r\n") scanner.Scan() if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN XNOOP" { t.Fatal("Bad capability:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } } type xnoopAuth struct{} func (ext *xnoopAuth) Next(response []byte) (challenge []byte, done bool, err error) { done = true return } func TestServer_EnableAuth(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() s.EnableAuth("XNOOP", func(server.Conn) sasl.Server { return &xnoopAuth{} }) io.WriteString(c, "a001 CAPABILITY\r\n") scanner.Scan() if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN AUTH=XNOOP" && scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=XNOOP AUTH=PLAIN" { t.Fatal("Bad capability:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } } go-imap-1.2.0/server/cmd_auth.go000066400000000000000000000145011412725504300164700ustar00rootroot00000000000000package server import ( "bufio" "errors" "strings" "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" ) // imap errors in Authenticated state. var ( ErrNotAuthenticated = errors.New("Not authenticated") ) type Select struct { commands.Select } func (cmd *Select) Handle(conn Conn) error { ctx := conn.Context() // As per RFC1730#6.3.1, // The SELECT command automatically deselects any // currently selected mailbox before attempting the new selection. // Consequently, if a mailbox is selected and a SELECT command that // fails is attempted, no mailbox is selected. // For example, some clients (e.g. Apple Mail) perform SELECT "" when the // server doesn't announce the UNSELECT capability. ctx.Mailbox = nil ctx.MailboxReadOnly = false if ctx.User == nil { return ErrNotAuthenticated } mbox, err := ctx.User.GetMailbox(cmd.Mailbox) if err != nil { return err } items := []imap.StatusItem{ imap.StatusMessages, imap.StatusRecent, imap.StatusUnseen, imap.StatusUidNext, imap.StatusUidValidity, } status, err := mbox.Status(items) if err != nil { return err } ctx.Mailbox = mbox ctx.MailboxReadOnly = cmd.ReadOnly || status.ReadOnly res := &responses.Select{Mailbox: status} if err := conn.WriteResp(res); err != nil { return err } var code imap.StatusRespCode = imap.CodeReadWrite if ctx.MailboxReadOnly { code = imap.CodeReadOnly } return ErrStatusResp(&imap.StatusResp{ Type: imap.StatusRespOk, Code: code, }) } type Create struct { commands.Create } func (cmd *Create) Handle(conn Conn) error { ctx := conn.Context() if ctx.User == nil { return ErrNotAuthenticated } return ctx.User.CreateMailbox(cmd.Mailbox) } type Delete struct { commands.Delete } func (cmd *Delete) Handle(conn Conn) error { ctx := conn.Context() if ctx.User == nil { return ErrNotAuthenticated } return ctx.User.DeleteMailbox(cmd.Mailbox) } type Rename struct { commands.Rename } func (cmd *Rename) Handle(conn Conn) error { ctx := conn.Context() if ctx.User == nil { return ErrNotAuthenticated } return ctx.User.RenameMailbox(cmd.Existing, cmd.New) } type Subscribe struct { commands.Subscribe } func (cmd *Subscribe) Handle(conn Conn) error { ctx := conn.Context() if ctx.User == nil { return ErrNotAuthenticated } mbox, err := ctx.User.GetMailbox(cmd.Mailbox) if err != nil { return err } return mbox.SetSubscribed(true) } type Unsubscribe struct { commands.Unsubscribe } func (cmd *Unsubscribe) Handle(conn Conn) error { ctx := conn.Context() if ctx.User == nil { return ErrNotAuthenticated } mbox, err := ctx.User.GetMailbox(cmd.Mailbox) if err != nil { return err } return mbox.SetSubscribed(false) } type List struct { commands.List } func (cmd *List) Handle(conn Conn) error { ctx := conn.Context() if ctx.User == nil { return ErrNotAuthenticated } ch := make(chan *imap.MailboxInfo) res := &responses.List{Mailboxes: ch, Subscribed: cmd.Subscribed} done := make(chan error, 1) go (func() { done <- conn.WriteResp(res) // Make sure to drain the channel. for range ch { } })() mailboxes, err := ctx.User.ListMailboxes(cmd.Subscribed) if err != nil { // Close channel to signal end of results close(ch) return err } for _, mbox := range mailboxes { info, err := mbox.Info() if err != nil { // Close channel to signal end of results close(ch) return err } // An empty ("" string) mailbox name argument is a special request to return // the hierarchy delimiter and the root name of the name given in the // reference. if cmd.Mailbox == "" { ch <- &imap.MailboxInfo{ Attributes: []string{imap.NoSelectAttr}, Delimiter: info.Delimiter, Name: info.Delimiter, } break } if info.Match(cmd.Reference, cmd.Mailbox) { ch <- info } } // Close channel to signal end of results close(ch) return <-done } type Status struct { commands.Status } func (cmd *Status) Handle(conn Conn) error { ctx := conn.Context() if ctx.User == nil { return ErrNotAuthenticated } mbox, err := ctx.User.GetMailbox(cmd.Mailbox) if err != nil { return err } status, err := mbox.Status(cmd.Items) if err != nil { return err } // Only keep items thqat have been requested items := make(map[imap.StatusItem]interface{}) for _, k := range cmd.Items { items[k] = status.Items[k] } status.Items = items res := &responses.Status{Mailbox: status} return conn.WriteResp(res) } type Append struct { commands.Append } func (cmd *Append) Handle(conn Conn) error { ctx := conn.Context() if ctx.User == nil { return ErrNotAuthenticated } mbox, err := ctx.User.GetMailbox(cmd.Mailbox) if err == backend.ErrNoSuchMailbox { return ErrStatusResp(&imap.StatusResp{ Type: imap.StatusRespNo, Code: imap.CodeTryCreate, Info: err.Error(), }) } else if err != nil { return err } if err := mbox.CreateMessage(cmd.Flags, cmd.Date, cmd.Message); err != nil { if err == backend.ErrTooBig { return ErrStatusResp(&imap.StatusResp{ Type: imap.StatusRespNo, Code: "TOOBIG", Info: "Message size exceeding limit", }) } return err } // If APPEND targets the currently selected mailbox, send an untagged EXISTS // Do this only if the backend doesn't send updates itself if conn.Server().Updates == nil && ctx.Mailbox != nil && ctx.Mailbox.Name() == mbox.Name() { status, err := mbox.Status([]imap.StatusItem{imap.StatusMessages}) if err != nil { return err } status.Flags = nil status.PermanentFlags = nil status.UnseenSeqNum = 0 res := &responses.Select{Mailbox: status} if err := conn.WriteResp(res); err != nil { return err } } return nil } type Unselect struct { commands.Unselect } func (cmd *Unselect) Handle(conn Conn) error { ctx := conn.Context() if ctx.Mailbox == nil { return ErrNoMailboxSelected } ctx.Mailbox = nil ctx.MailboxReadOnly = false return nil } type Idle struct { commands.Idle } func (cmd *Idle) Handle(conn Conn) error { cont := &imap.ContinuationReq{Info: "idling"} if err := conn.WriteResp(cont); err != nil { return err } // Wait for DONE scanner := bufio.NewScanner(conn) scanner.Scan() if err := scanner.Err(); err != nil { return err } if strings.ToUpper(scanner.Text()) != "DONE" { return errors.New("Expected DONE") } return nil } go-imap-1.2.0/server/cmd_auth_test.go000066400000000000000000000351361412725504300175360ustar00rootroot00000000000000package server_test import ( "bufio" "io" "net" "strings" "testing" "github.com/emersion/go-imap/server" ) func testServerAuthenticated(t *testing.T) (s *server.Server, c net.Conn, scanner *bufio.Scanner) { s, c, scanner = testServerGreeted(t) io.WriteString(c, "a000 LOGIN username password\r\n") scanner.Scan() // OK response return } func TestSelect_Ok(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 SELECT INBOX\r\n") got := map[string]bool{ "OK": false, "FLAGS": false, "EXISTS": false, "RECENT": false, "PERMANENTFLAGS": false, "UIDNEXT": false, "UIDVALIDITY": false, } for scanner.Scan() { res := scanner.Text() if res == "* FLAGS (\\Seen)" { got["FLAGS"] = true } else if res == "* 1 EXISTS" { got["EXISTS"] = true } else if res == "* 0 RECENT" { got["RECENT"] = true } else if strings.HasPrefix(res, "* OK [PERMANENTFLAGS (\\*)]") { got["PERMANENTFLAGS"] = true } else if strings.HasPrefix(res, "* OK [UIDNEXT 7]") { got["UIDNEXT"] = true } else if strings.HasPrefix(res, "* OK [UIDVALIDITY 1]") { got["UIDVALIDITY"] = true } else if strings.HasPrefix(res, "a001 OK [READ-WRITE] ") { got["OK"] = true break } else { t.Fatal("Unexpected response:", res) } } for name, val := range got { if !val { t.Error("Did not got response:", name) } } } func TestSelect_ReadOnly(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 EXAMINE INBOX\r\n") gotOk := true for scanner.Scan() { res := scanner.Text() if strings.HasPrefix(res, "a001 OK [READ-ONLY]") { gotOk = true break } } if !gotOk { t.Error("Did not get a correct OK response") } } func TestSelect_InvalidMailbox(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 SELECT idontexist\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestSelect_NotAuthenticated(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 SELECT INBOX\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestCreate(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 CREATE test\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestCreate_NotAuthenticated(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 CREATE test\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestDelete(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 CREATE test\r\n") scanner.Scan() io.WriteString(c, "a001 DELETE test\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestDelete_InvalidMailbox(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 DELETE test\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestDelete_NotAuthenticated(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 DELETE INBOX\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestRename(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 CREATE test\r\n") scanner.Scan() io.WriteString(c, "a001 RENAME test test2\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestRename_InvalidMailbox(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 RENAME test test2\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestRename_NotAuthenticated(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 RENAME test test2\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestSubscribe(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 SUBSCRIBE INBOX\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 SUBSCRIBE idontexist\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestSubscribe_NotAuthenticated(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 SUBSCRIBE INBOX\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestUnsubscribe(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 SUBSCRIBE INBOX\r\n") scanner.Scan() io.WriteString(c, "a001 UNSUBSCRIBE INBOX\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 UNSUBSCRIBE idontexist\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestUnsubscribe_NotAuthenticated(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 UNSUBSCRIBE INBOX\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestList(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 LIST \"\" *\r\n") scanner.Scan() if scanner.Text() != "* LIST () \"/\" INBOX" { t.Fatal("Invalid LIST response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestList_Nested(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 CREATE first\r\n") scanner.Scan() io.WriteString(c, "a001 CREATE first/second\r\n") scanner.Scan() io.WriteString(c, "a001 CREATE first/second/third\r\n") scanner.Scan() io.WriteString(c, "a001 CREATE first/second/third2\r\n") scanner.Scan() check := func(mailboxes []string) { checked := map[string]bool{} for scanner.Scan() { if strings.HasPrefix(scanner.Text(), "a001 OK ") { break } else if strings.HasPrefix(scanner.Text(), "* LIST ") { found := false for _, name := range mailboxes { if strings.HasSuffix(scanner.Text(), " \""+name+"\"") || strings.HasSuffix(scanner.Text(), " "+name) { checked[name] = true found = true break } } if !found { t.Fatal("Unexpected mailbox:", scanner.Text()) } } else { t.Fatal("Invalid LIST response:", scanner.Text()) } } for _, name := range mailboxes { if !checked[name] { t.Fatal("Missing mailbox:", name) } } } io.WriteString(c, "a001 LIST \"\" *\r\n") check([]string{"INBOX", "first", "first/second", "first/second/third", "first/second/third2"}) io.WriteString(c, "a001 LIST \"\" %\r\n") check([]string{"INBOX", "first"}) io.WriteString(c, "a001 LIST first *\r\n") check([]string{"first/second", "first/second/third", "first/second/third2"}) io.WriteString(c, "a001 LIST first %\r\n") check([]string{"first/second"}) io.WriteString(c, "a001 LIST first/second *\r\n") check([]string{"first/second/third", "first/second/third2"}) io.WriteString(c, "a001 LIST first/second %\r\n") check([]string{"first/second/third", "first/second/third2"}) io.WriteString(c, "a001 LIST first second\r\n") check([]string{"first/second"}) io.WriteString(c, "a001 LIST first/second third\r\n") check([]string{"first/second/third"}) } func TestList_Subscribed(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 LSUB \"\" *\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 SUBSCRIBE INBOX\r\n") scanner.Scan() io.WriteString(c, "a001 LSUB \"\" *\r\n") scanner.Scan() if scanner.Text() != "* LSUB () \"/\" INBOX" { t.Fatal("Invalid LSUB response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestTLS_AlreadyAuthenticated(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 STARTTLS\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestList_NotAuthenticated(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 LIST \"\" *\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestList_Delimiter(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 LIST \"\" \"\"\r\n") scanner.Scan() if scanner.Text() != "* LIST (\\Noselect) \"/\" \"/\"" { t.Fatal("Invalid LIST response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestStatus(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 STATUS INBOX (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN)\r\n") scanner.Scan() line := scanner.Text() if !strings.HasPrefix(line, "* STATUS INBOX (") { t.Fatal("Invalid STATUS response:", line) } parts := []string{"MESSAGES 1", "RECENT 0", "UIDNEXT 7", "UIDVALIDITY 1", "UNSEEN 0"} for _, p := range parts { if !strings.Contains(line, p) { t.Fatal("Invalid STATUS response:", line) } } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestStatus_InvalidMailbox(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 STATUS idontexist (MESSAGES)\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestStatus_NotAuthenticated(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 STATUS INBOX (MESSAGES)\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestAppend(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 APPEND INBOX {80}\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "+ ") { t.Fatal("Invalid continuation request:", scanner.Text()) } io.WriteString(c, "From: Edward Snowden \r\n") io.WriteString(c, "To: Julian Assange \r\n") io.WriteString(c, "\r\n") io.WriteString(c, "<3\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestAppend_WithFlags(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 APPEND INBOX (\\Draft) {11}\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "+ ") { t.Fatal("Invalid continuation request:", scanner.Text()) } io.WriteString(c, "Hello World\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestAppend_WithFlagsAndDate(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 APPEND INBOX (\\Draft) \"5-Nov-1984 13:37:00 -0700\" {11}\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "+ ") { t.Fatal("Invalid continuation request:", scanner.Text()) } io.WriteString(c, "Hello World\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestAppend_Selected(t *testing.T) { s, c, scanner := testServerSelected(t, true) defer s.Close() defer c.Close() io.WriteString(c, "a001 APPEND INBOX {11}\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "+ ") { t.Fatal("Invalid continuation request:", scanner.Text()) } io.WriteString(c, "Hello World\r\n") scanner.Scan() if scanner.Text() != "* 2 EXISTS" { t.Fatal("Invalid untagged response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestAppend_InvalidMailbox(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 APPEND idontexist {11}\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "+ ") { t.Fatal("Invalid continuation request:", scanner.Text()) } io.WriteString(c, "Hello World\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestAppend_NotAuthenticated(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 APPEND INBOX {11}\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "+ ") { t.Fatal("Invalid continuation request:", scanner.Text()) } io.WriteString(c, "Hello World\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } go-imap-1.2.0/server/cmd_noauth.go000066400000000000000000000051671412725504300170350ustar00rootroot00000000000000package server import ( "crypto/tls" "errors" "net" "github.com/emersion/go-imap" "github.com/emersion/go-imap/commands" "github.com/emersion/go-sasl" ) // IMAP errors in Not Authenticated state. var ( ErrAlreadyAuthenticated = errors.New("Already authenticated") ErrAuthDisabled = errors.New("Authentication disabled") ) type StartTLS struct { commands.StartTLS } func (cmd *StartTLS) Handle(conn Conn) error { ctx := conn.Context() if ctx.State != imap.NotAuthenticatedState { return ErrAlreadyAuthenticated } if conn.IsTLS() { return errors.New("TLS is already enabled") } if conn.Server().TLSConfig == nil { return errors.New("TLS support not enabled") } // Send an OK status response to let the client know that the TLS handshake // can begin return ErrStatusResp(&imap.StatusResp{ Type: imap.StatusRespOk, Info: "Begin TLS negotiation now", }) } func (cmd *StartTLS) Upgrade(conn Conn) error { tlsConfig := conn.Server().TLSConfig var tlsConn *tls.Conn err := conn.Upgrade(func(sock net.Conn) (net.Conn, error) { conn.WaitReady() tlsConn = tls.Server(sock, tlsConfig) err := tlsConn.Handshake() return tlsConn, err }) if err != nil { return err } conn.setTLSConn(tlsConn) return nil } func afterAuthStatus(conn Conn) error { caps := conn.Capabilities() capAtoms := make([]interface{}, 0, len(caps)) for _, cap := range caps { capAtoms = append(capAtoms, imap.RawString(cap)) } return ErrStatusResp(&imap.StatusResp{ Type: imap.StatusRespOk, Code: imap.CodeCapability, Arguments: capAtoms, }) } func canAuth(conn Conn) bool { for _, cap := range conn.Capabilities() { if cap == "AUTH=PLAIN" { return true } } return false } type Login struct { commands.Login } func (cmd *Login) Handle(conn Conn) error { ctx := conn.Context() if ctx.State != imap.NotAuthenticatedState { return ErrAlreadyAuthenticated } if !canAuth(conn) { return ErrAuthDisabled } user, err := conn.Server().Backend.Login(conn.Info(), cmd.Username, cmd.Password) if err != nil { return err } ctx.State = imap.AuthenticatedState ctx.User = user return afterAuthStatus(conn) } type Authenticate struct { commands.Authenticate } func (cmd *Authenticate) Handle(conn Conn) error { ctx := conn.Context() if ctx.State != imap.NotAuthenticatedState { return ErrAlreadyAuthenticated } if !canAuth(conn) { return ErrAuthDisabled } mechanisms := map[string]sasl.Server{} for name, newSasl := range conn.Server().auths { mechanisms[name] = newSasl(conn) } err := cmd.Authenticate.Handle(mechanisms, conn) if err != nil { return err } return afterAuthStatus(conn) } go-imap-1.2.0/server/cmd_noauth_test.go000066400000000000000000000121011412725504300200560ustar00rootroot00000000000000package server_test import ( "bufio" "crypto/tls" "io" "net" "strings" "testing" "github.com/emersion/go-imap/internal" "github.com/emersion/go-imap/server" ) func testServerTLS(t *testing.T) (s *server.Server, c net.Conn, scanner *bufio.Scanner) { s, c, scanner = testServerGreeted(t) cert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey) if err != nil { t.Fatal(err) } tlsConfig := &tls.Config{ InsecureSkipVerify: true, Certificates: []tls.Certificate{cert}, } s.AllowInsecureAuth = false s.TLSConfig = tlsConfig io.WriteString(c, "a001 CAPABILITY\r\n") scanner.Scan() if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" STARTTLS LOGINDISABLED" { t.Fatal("Bad CAPABILITY response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } io.WriteString(c, "a001 STARTTLS\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } sc := tls.Client(c, tlsConfig) if err = sc.Handshake(); err != nil { t.Fatal(err) } c = sc scanner = bufio.NewScanner(c) return } func TestStartTLS(t *testing.T) { s, c, scanner := testServerTLS(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 CAPABILITY\r\n") scanner.Scan() if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN" { t.Fatal("Bad CAPABILITY response:", scanner.Text()) } } func TestStartTLS_AlreadyEnabled(t *testing.T) { s, c, scanner := testServerTLS(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 STARTTLS\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Bad status response:", scanner.Text()) } } func TestStartTLS_NotSupported(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 STARTTLS\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Bad status response:", scanner.Text()) } } func TestLogin_Ok(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 LOGIN username password\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } } func TestLogin_AlreadyAuthenticated(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 LOGIN username password\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } io.WriteString(c, "a001 LOGIN username password\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Bad status response:", scanner.Text()) } } func TestLogin_No(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 LOGIN username wrongpassword\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Bad status response:", scanner.Text()) } } func TestAuthenticate_Plain_Ok(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 AUTHENTICATE PLAIN\r\n") scanner.Scan() if scanner.Text() != "+" { t.Fatal("Bad continuation request:", scanner.Text()) } // :usename:password io.WriteString(c, "AHVzZXJuYW1lAHBhc3N3b3Jk\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } } func TestAuthenticate_Plain_Cancel(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 AUTHENTICATE PLAIN\r\n") scanner.Scan() if scanner.Text() != "+" { t.Fatal("Bad continuation request:", scanner.Text()) } io.WriteString(c, "*\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 BAD negotiation cancelled") { t.Fatal("Bad status response:", scanner.Text()) } } func TestAuthenticate_Plain_InitialResponse(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } } func TestAuthenticate_Plain_No(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 AUTHENTICATE PLAIN\r\n") scanner.Scan() if scanner.Text() != "+" { t.Fatal("Bad continuation request:", scanner.Text()) } // Invalid challenge io.WriteString(c, "BHVzZXJuYW1lAHBhc3N3b6Jk\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Bad status response:", scanner.Text()) } } func TestAuthenticate_No(t *testing.T) { s, c, scanner := testServerGreeted(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 AUTHENTICATE XIDONTEXIST\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Bad status response:", scanner.Text()) } } go-imap-1.2.0/server/cmd_selected.go000066400000000000000000000155621412725504300173270ustar00rootroot00000000000000package server import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" ) // imap errors in Selected state. var ( ErrNoMailboxSelected = errors.New("No mailbox selected") ErrMailboxReadOnly = errors.New("Mailbox opened in read-only mode") ) // A command handler that supports UIDs. type UidHandler interface { Handler // Handle this command using UIDs for a given connection. UidHandle(conn Conn) error } type Check struct { commands.Check } func (cmd *Check) Handle(conn Conn) error { ctx := conn.Context() if ctx.Mailbox == nil { return ErrNoMailboxSelected } if ctx.MailboxReadOnly { return ErrMailboxReadOnly } return ctx.Mailbox.Check() } type Close struct { commands.Close } func (cmd *Close) Handle(conn Conn) error { ctx := conn.Context() if ctx.Mailbox == nil { return ErrNoMailboxSelected } mailbox := ctx.Mailbox ctx.Mailbox = nil ctx.MailboxReadOnly = false // No need to send expunge updates here, since the mailbox is already unselected return mailbox.Expunge() } type Expunge struct { commands.Expunge } func (cmd *Expunge) Handle(conn Conn) error { ctx := conn.Context() if ctx.Mailbox == nil { return ErrNoMailboxSelected } if ctx.MailboxReadOnly { return ErrMailboxReadOnly } // Get a list of messages that will be deleted // That will allow us to send expunge updates if the backend doesn't support it var seqnums []uint32 if conn.Server().Updates == nil { criteria := &imap.SearchCriteria{ WithFlags: []string{imap.DeletedFlag}, } var err error seqnums, err = ctx.Mailbox.SearchMessages(false, criteria) if err != nil { return err } } if err := ctx.Mailbox.Expunge(); err != nil { return err } // If the backend doesn't support expunge updates, let's do it ourselves if conn.Server().Updates == nil { done := make(chan error, 1) ch := make(chan uint32) res := &responses.Expunge{SeqNums: ch} go (func() { done <- conn.WriteResp(res) // Don't need to drain 'ch', sender will stop sending when error written to 'done. })() // Iterate sequence numbers from the last one to the first one, as deleting // messages changes their respective numbers for i := len(seqnums) - 1; i >= 0; i-- { // Send sequence numbers to channel, and check if conn.WriteResp() finished early. select { case ch <- seqnums[i]: // Send next seq. number case err := <-done: // Check for errors close(ch) return err } } close(ch) if err := <-done; err != nil { return err } } return nil } type Search struct { commands.Search } func (cmd *Search) handle(uid bool, conn Conn) error { ctx := conn.Context() if ctx.Mailbox == nil { return ErrNoMailboxSelected } ids, err := ctx.Mailbox.SearchMessages(uid, cmd.Criteria) if err != nil { return err } res := &responses.Search{Ids: ids} return conn.WriteResp(res) } func (cmd *Search) Handle(conn Conn) error { return cmd.handle(false, conn) } func (cmd *Search) UidHandle(conn Conn) error { return cmd.handle(true, conn) } type Fetch struct { commands.Fetch } func (cmd *Fetch) handle(uid bool, conn Conn) error { ctx := conn.Context() if ctx.Mailbox == nil { return ErrNoMailboxSelected } ch := make(chan *imap.Message) res := &responses.Fetch{Messages: ch} done := make(chan error, 1) go (func() { done <- conn.WriteResp(res) // Make sure to drain the message channel. for _ = range ch { } })() err := ctx.Mailbox.ListMessages(uid, cmd.SeqSet, cmd.Items, ch) if err != nil { return err } return <-done } func (cmd *Fetch) Handle(conn Conn) error { return cmd.handle(false, conn) } func (cmd *Fetch) UidHandle(conn Conn) error { // Append UID to the list of requested items if it isn't already present hasUid := false for _, item := range cmd.Items { if item == "UID" { hasUid = true break } } if !hasUid { cmd.Items = append(cmd.Items, "UID") } return cmd.handle(true, conn) } type Store struct { commands.Store } func (cmd *Store) handle(uid bool, conn Conn) error { ctx := conn.Context() if ctx.Mailbox == nil { return ErrNoMailboxSelected } if ctx.MailboxReadOnly { return ErrMailboxReadOnly } // Only flags operations are supported op, silent, err := imap.ParseFlagsOp(cmd.Item) if err != nil { return err } var flags []string if flagsList, ok := cmd.Value.([]interface{}); ok { // Parse list of flags if strs, err := imap.ParseStringList(flagsList); err == nil { flags = strs } else { return err } } else { // Parse single flag if str, err := imap.ParseString(cmd.Value); err == nil { flags = []string{str} } else { return err } } for i, flag := range flags { flags[i] = imap.CanonicalFlag(flag) } // If the backend supports message updates, this will prevent this connection // from receiving them // TODO: find a better way to do this, without conn.silent *conn.silent() = silent err = ctx.Mailbox.UpdateMessagesFlags(uid, cmd.SeqSet, op, flags) *conn.silent() = false if err != nil { return err } // Not silent: send FETCH updates if the backend doesn't support message // updates if conn.Server().Updates == nil && !silent { inner := &Fetch{} inner.SeqSet = cmd.SeqSet inner.Items = []imap.FetchItem{imap.FetchFlags} if uid { inner.Items = append(inner.Items, "UID") } if err := inner.handle(uid, conn); err != nil { return err } } return nil } func (cmd *Store) Handle(conn Conn) error { return cmd.handle(false, conn) } func (cmd *Store) UidHandle(conn Conn) error { return cmd.handle(true, conn) } type Copy struct { commands.Copy } func (cmd *Copy) handle(uid bool, conn Conn) error { ctx := conn.Context() if ctx.Mailbox == nil { return ErrNoMailboxSelected } return ctx.Mailbox.CopyMessages(uid, cmd.SeqSet, cmd.Mailbox) } func (cmd *Copy) Handle(conn Conn) error { return cmd.handle(false, conn) } func (cmd *Copy) UidHandle(conn Conn) error { return cmd.handle(true, conn) } type Move struct { commands.Move } func (h *Move) handle(uid bool, conn Conn) error { mailbox := conn.Context().Mailbox if mailbox == nil { return ErrNoMailboxSelected } if m, ok := mailbox.(backend.MoveMailbox); ok { return m.MoveMessages(uid, h.SeqSet, h.Mailbox) } return errors.New("MOVE extension not supported") } func (h *Move) Handle(conn Conn) error { return h.handle(false, conn) } func (h *Move) UidHandle(conn Conn) error { return h.handle(true, conn) } type Uid struct { commands.Uid } func (cmd *Uid) Handle(conn Conn) error { inner := cmd.Cmd.Command() hdlr, err := conn.commandHandler(inner) if err != nil { return err } uidHdlr, ok := hdlr.(UidHandler) if !ok { return errors.New("Command unsupported with UID") } if err := uidHdlr.UidHandle(conn); err != nil { return err } return ErrStatusResp(&imap.StatusResp{ Type: imap.StatusRespOk, Info: "UID " + inner.Name + " completed", }) } go-imap-1.2.0/server/cmd_selected_test.go000066400000000000000000000340741412725504300203650ustar00rootroot00000000000000package server_test import ( "bufio" "io" "net" "strings" "testing" "github.com/emersion/go-imap/server" ) func testServerSelected(t *testing.T, readOnly bool) (s *server.Server, c net.Conn, scanner *bufio.Scanner) { s, c, scanner = testServerAuthenticated(t) if readOnly { io.WriteString(c, "a000 EXAMINE INBOX\r\n") } else { io.WriteString(c, "a000 SELECT INBOX\r\n") } for scanner.Scan() { if strings.HasPrefix(scanner.Text(), "a000 ") { break } } return } func TestNoop_Selected(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer s.Close() defer c.Close() io.WriteString(c, "a001 NOOP\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Bad status response:", scanner.Text()) } } func TestCheck(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer s.Close() defer c.Close() io.WriteString(c, "a001 CHECK\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestCheck_ReadOnly(t *testing.T) { s, c, scanner := testServerSelected(t, true) defer s.Close() defer c.Close() io.WriteString(c, "a001 CHECK\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestCheck_NotSelected(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 CHECK\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestClose(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer s.Close() defer c.Close() io.WriteString(c, "a001 CLOSE\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestClose_NotSelected(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 CLOSE\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestExpunge(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer s.Close() defer c.Close() io.WriteString(c, "a001 EXPUNGE\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 STORE 1 +FLAGS.SILENT (\\Deleted)\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 EXPUNGE\r\n") scanner.Scan() if scanner.Text() != "* 1 EXPUNGE" { t.Fatal("Invalid EXPUNGE response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestExpunge_ReadOnly(t *testing.T) { s, c, scanner := testServerSelected(t, true) defer s.Close() defer c.Close() io.WriteString(c, "a001 EXPUNGE\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestExpunge_NotSelected(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 EXPUNGE\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestSearch(t *testing.T) { s, c, scanner := testServerSelected(t, true) defer s.Close() defer c.Close() io.WriteString(c, "a001 SEARCH UNDELETED\r\n") scanner.Scan() if scanner.Text() != "* SEARCH 1" { t.Fatal("Invalid SEARCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 SEARCH DELETED\r\n") scanner.Scan() if scanner.Text() != "* SEARCH" { t.Fatal("Invalid SEARCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestSearch_NotSelected(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 SEARCH UNDELETED\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestSearch_Uid(t *testing.T) { s, c, scanner := testServerSelected(t, true) defer s.Close() defer c.Close() io.WriteString(c, "a001 UID SEARCH UNDELETED\r\n") scanner.Scan() if scanner.Text() != "* SEARCH 6" { t.Fatal("Invalid SEARCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestFetch(t *testing.T) { s, c, scanner := testServerSelected(t, true) defer s.Close() defer c.Close() io.WriteString(c, "a001 FETCH 1 (UID FLAGS)\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (UID 6 FLAGS (\\Seen))" { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 FETCH 1 (BODY.PEEK[TEXT])\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (BODY[TEXT] {11}" { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "Hi there :))") { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestFetch_NotSelected(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 FETCH 1 (UID FLAGS)\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestFetch_Uid(t *testing.T) { s, c, scanner := testServerSelected(t, true) defer s.Close() defer c.Close() io.WriteString(c, "a001 UID FETCH 6 (UID)\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (UID 6)" { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestFetch_Uid_UidNotRequested(t *testing.T) { s, c, scanner := testServerSelected(t, true) defer s.Close() defer c.Close() io.WriteString(c, "a001 UID FETCH 6 (FLAGS)\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (FLAGS (\\Seen) UID 6)" { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestStore(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer s.Close() defer c.Close() io.WriteString(c, "a001 STORE 1 +FLAGS (\\Flagged)\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (FLAGS (\\Seen \\Flagged))" { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 STORE 1 FLAGS (\\Answered)\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (FLAGS (\\Answered))" { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 STORE 1 -FLAGS (\\Answered)\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (FLAGS ())" { t.Fatal("Invalid status response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 STORE 1 +FLAGS.SILENT (\\Flagged \\Seen)\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestStore_NotSelected(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 STORE 1 +FLAGS (\\Flagged)\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestStore_ReadOnly(t *testing.T) { s, c, scanner := testServerSelected(t, true) defer s.Close() defer c.Close() io.WriteString(c, "a001 STORE 1 +FLAGS (\\Flagged)\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestStore_InvalidOperation(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer s.Close() defer c.Close() io.WriteString(c, "a001 STORE 1 IDONTEXIST (\\Flagged)\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestStore_InvalidFlags(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer s.Close() defer c.Close() io.WriteString(c, "a001 STORE 1 +FLAGS ((nested)(lists))\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestStore_SingleFlagNonList(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer c.Close() defer s.Close() io.WriteString(c, "a001 STORE 1 FLAGS somestring\r\n") gotOK := false gotFetch := false for scanner.Scan() { res := scanner.Text() if res == "* 1 FETCH (FLAGS (somestring))" { gotFetch = true } else if strings.HasPrefix(res, "a001 OK ") { gotOK = true break } else { t.Fatal("Unexpected response:", res) } } if !gotFetch { t.Fatal("Missing FETCH response.") } if !gotOK { t.Fatal("Missing status response.") } } func TestStore_NonList(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer c.Close() defer s.Close() io.WriteString(c, "a001 STORE 1 FLAGS somestring someanotherstring\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (FLAGS (somestring someanotherstring))" { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestStore_RecentFlag(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer c.Close() defer s.Close() // Add Recent flag io.WriteString(c, "a001 STORE 1 FLAGS \\Recent\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (FLAGS (\\Recent))" { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } // Set flags to: something // Should still get Recent flag back io.WriteString(c, "a001 STORE 1 FLAGS something\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (FLAGS (\\Recent something))" { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } // Try adding Recent flag again io.WriteString(c, "a001 STORE 1 FLAGS \\Recent anotherflag\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (FLAGS (\\Recent anotherflag))" { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestStore_Uid(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer s.Close() defer c.Close() io.WriteString(c, "a001 UID STORE 6 +FLAGS (\\Flagged)\r\n") scanner.Scan() if scanner.Text() != "* 1 FETCH (FLAGS (\\Seen \\Flagged) UID 6)" { t.Fatal("Invalid FETCH response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestCopy(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer s.Close() defer c.Close() io.WriteString(c, "a001 CREATE CopyDest\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 COPY 1 CopyDest\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 STATUS CopyDest (MESSAGES)\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "* STATUS \"CopyDest\" (MESSAGES 1)") { t.Fatal("Invalid status response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestCopy_NotSelected(t *testing.T) { s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "a001 CREATE CopyDest\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 COPY 1 CopyDest\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestCopy_Uid(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer s.Close() defer c.Close() io.WriteString(c, "a001 CREATE CopyDest\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 UID COPY 6 CopyDest\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 OK ") { t.Fatal("Invalid status response:", scanner.Text()) } } func TestUid_InvalidCommand(t *testing.T) { s, c, scanner := testServerSelected(t, false) defer s.Close() defer c.Close() io.WriteString(c, "a001 UID IDONTEXIST\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } io.WriteString(c, "a001 UID CLOSE\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "a001 NO ") { t.Fatal("Invalid status response:", scanner.Text()) } } go-imap-1.2.0/server/conn.go000066400000000000000000000211601412725504300156400ustar00rootroot00000000000000package server import ( "crypto/tls" "errors" "fmt" "io" "net" "runtime/debug" "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" ) // Conn is a connection to a client. type Conn interface { io.Reader // Server returns this connection's server. Server() *Server // Context returns this connection's context. Context() *Context // Capabilities returns a list of capabilities enabled for this connection. Capabilities() []string // WriteResp writes a response to this connection. WriteResp(res imap.WriterTo) error // IsTLS returns true if TLS is enabled. IsTLS() bool // TLSState returns the TLS connection state if TLS is enabled, nil otherwise. TLSState() *tls.ConnectionState // Upgrade upgrades a connection, e.g. wrap an unencrypted connection with an // encrypted tunnel. Upgrade(upgrader imap.ConnUpgrader) error // Close closes this connection. Close() error WaitReady() Info() *imap.ConnInfo setTLSConn(*tls.Conn) silent() *bool // TODO: remove this serve(Conn) error commandHandler(cmd *imap.Command) (hdlr Handler, err error) } // Context stores a connection's metadata. type Context struct { // This connection's current state. State imap.ConnState // If the client is logged in, the user. User backend.User // If the client has selected a mailbox, the mailbox. Mailbox backend.Mailbox // True if the currently selected mailbox has been opened in read-only mode. MailboxReadOnly bool // Responses to send to the client. Responses chan<- imap.WriterTo // Closed when the client is logged out. LoggedOut <-chan struct{} } type conn struct { *imap.Conn conn Conn // With extensions overrides s *Server ctx *Context tlsConn *tls.Conn continues chan bool upgrade chan bool responses chan imap.WriterTo loggedOut chan struct{} silentVal bool } func newConn(s *Server, c net.Conn) *conn { // Create an imap.Reader and an imap.Writer continues := make(chan bool) r := imap.NewServerReader(nil, continues) w := imap.NewWriter(nil) responses := make(chan imap.WriterTo) loggedOut := make(chan struct{}) tlsConn, _ := c.(*tls.Conn) conn := &conn{ Conn: imap.NewConn(c, r, w), s: s, ctx: &Context{ State: imap.ConnectingState, Responses: responses, LoggedOut: loggedOut, }, tlsConn: tlsConn, continues: continues, upgrade: make(chan bool), responses: responses, loggedOut: loggedOut, } if s.Debug != nil { conn.Conn.SetDebug(s.Debug) } if s.MaxLiteralSize > 0 { conn.Conn.MaxLiteralSize = s.MaxLiteralSize } go conn.send() return conn } func (c *conn) Server() *Server { return c.s } func (c *conn) Context() *Context { return c.ctx } type response struct { response imap.WriterTo done chan struct{} } func (r *response) WriteTo(w *imap.Writer) error { err := r.response.WriteTo(w) close(r.done) return err } func (c *conn) setDeadline() { if c.s.AutoLogout == 0 { return } dur := c.s.AutoLogout if dur < MinAutoLogout { dur = MinAutoLogout } t := time.Now().Add(dur) c.Conn.SetDeadline(t) } func (c *conn) WriteResp(r imap.WriterTo) error { done := make(chan struct{}) c.responses <- &response{r, done} <-done c.setDeadline() return nil } func (c *conn) Close() error { if c.ctx.User != nil { c.ctx.User.Logout() } return c.Conn.Close() } func (c *conn) Capabilities() []string { caps := []string{"IMAP4rev1", "LITERAL+", "SASL-IR", "CHILDREN", "UNSELECT", "MOVE"} appendLimitSet := false if c.ctx.State == imap.AuthenticatedState { if u, ok := c.ctx.User.(backend.AppendLimitUser); ok { if limit := u.CreateMessageLimit(); limit != nil { caps = append(caps, fmt.Sprintf("APPENDLIMIT=%v", *limit)) appendLimitSet = true } } } else if be, ok := c.Server().Backend.(backend.AppendLimitBackend); ok { if limit := be.CreateMessageLimit(); limit != nil { caps = append(caps, fmt.Sprintf("APPENDLIMIT=%v", *limit)) appendLimitSet = true } } if !appendLimitSet { caps = append(caps, "APPENDLIMIT") } if c.ctx.State == imap.NotAuthenticatedState { if !c.IsTLS() && c.s.TLSConfig != nil { caps = append(caps, "STARTTLS") } if !c.canAuth() { caps = append(caps, "LOGINDISABLED") } else { for name := range c.s.auths { caps = append(caps, "AUTH="+name) } } } for _, ext := range c.s.extensions { caps = append(caps, ext.Capabilities(c)...) } return caps } func (c *conn) writeAndFlush(w imap.WriterTo) error { if err := w.WriteTo(c.Writer); err != nil { return err } return c.Writer.Flush() } func (c *conn) send() { // Send responses for { select { case <-c.upgrade: // Wait until upgrade is finished. c.Wait() case needCont := <-c.continues: // Send continuation requests if needCont { resp := &imap.ContinuationReq{Info: "send literal"} if err := c.writeAndFlush(resp); err != nil { c.Server().ErrorLog.Println("cannot send continuation request: ", err) } } case res := <-c.responses: // Got a response that needs to be sent // Request to send the response if err := c.writeAndFlush(res); err != nil { c.Server().ErrorLog.Println("cannot send response: ", err) } case <-c.loggedOut: return } } } func (c *conn) greet() error { c.ctx.State = imap.NotAuthenticatedState caps := c.Capabilities() args := make([]interface{}, len(caps)) for i, cap := range caps { args[i] = imap.RawString(cap) } greeting := &imap.StatusResp{ Type: imap.StatusRespOk, Code: imap.CodeCapability, Arguments: args, Info: "IMAP4rev1 Service Ready", } return c.WriteResp(greeting) } func (c *conn) setTLSConn(tlsConn *tls.Conn) { c.tlsConn = tlsConn } func (c *conn) IsTLS() bool { return c.tlsConn != nil } func (c *conn) TLSState() *tls.ConnectionState { if c.tlsConn != nil { state := c.tlsConn.ConnectionState() return &state } return nil } // canAuth checks if the client can use plain text authentication. func (c *conn) canAuth() bool { return c.IsTLS() || c.s.AllowInsecureAuth } func (c *conn) silent() *bool { return &c.silentVal } func (c *conn) serve(conn Conn) (err error) { c.conn = conn defer func() { c.ctx.State = imap.LogoutState close(c.loggedOut) }() defer func() { if r := recover(); r != nil { c.WriteResp(&imap.StatusResp{ Type: imap.StatusRespBye, Info: "Internal server error, closing connection.", }) stack := debug.Stack() c.s.ErrorLog.Printf("panic serving %v: %v\n%s", c.Info().RemoteAddr, r, stack) err = fmt.Errorf("%v", r) } }() // Send greeting if err := c.greet(); err != nil { return err } for { if c.ctx.State == imap.LogoutState { return nil } var res *imap.StatusResp var up Upgrader fields, err := c.ReadLine() if err == io.EOF || c.ctx.State == imap.LogoutState { return nil } c.setDeadline() if err != nil { if imap.IsParseError(err) { res = &imap.StatusResp{ Type: imap.StatusRespBad, Info: err.Error(), } } else { c.s.ErrorLog.Println("cannot read command:", err) return err } } else { cmd := &imap.Command{} if err := cmd.Parse(fields); err != nil { res = &imap.StatusResp{ Tag: cmd.Tag, Type: imap.StatusRespBad, Info: err.Error(), } } else { var err error res, up, err = c.handleCommand(cmd) if err != nil { res = &imap.StatusResp{ Tag: cmd.Tag, Type: imap.StatusRespBad, Info: err.Error(), } } } } if res != nil { if err := c.WriteResp(res); err != nil { c.s.ErrorLog.Println("cannot write response:", err) continue } if up != nil && res.Type == imap.StatusRespOk { if err := up.Upgrade(c.conn); err != nil { c.s.ErrorLog.Println("cannot upgrade connection:", err) return err } } } } } func (c *conn) WaitReady() { c.upgrade <- true c.Conn.WaitReady() } func (c *conn) commandHandler(cmd *imap.Command) (hdlr Handler, err error) { newHandler := c.s.Command(cmd.Name) if newHandler == nil { err = errors.New("Unknown command") return } hdlr = newHandler() err = hdlr.Parse(cmd.Arguments) return } func (c *conn) handleCommand(cmd *imap.Command) (res *imap.StatusResp, up Upgrader, err error) { hdlr, err := c.commandHandler(cmd) if err != nil { return } hdlrErr := hdlr.Handle(c.conn) if statusErr, ok := hdlrErr.(*imap.ErrStatusResp); ok { res = statusErr.Resp } else if hdlrErr != nil { res = &imap.StatusResp{ Type: imap.StatusRespNo, Info: hdlrErr.Error(), } } else { res = &imap.StatusResp{ Type: imap.StatusRespOk, } } if res != nil { res.Tag = cmd.Tag if res.Type == imap.StatusRespOk && res.Info == "" { res.Info = cmd.Name + " completed" } } up, _ = hdlr.(Upgrader) return } go-imap-1.2.0/server/server.go000066400000000000000000000246641412725504300162250ustar00rootroot00000000000000// Package server provides an IMAP server. package server import ( "crypto/tls" "errors" "io" "log" "net" "os" "sync" "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/responses" "github.com/emersion/go-sasl" ) // The minimum autologout duration defined in RFC 3501 section 5.4. const MinAutoLogout = 30 * time.Minute // A command handler. type Handler interface { imap.Parser // Handle this command for a given connection. // // By default, after this function has returned a status response is sent. To // prevent this behavior handlers can use imap.ErrStatusResp. Handle(conn Conn) error } // A connection upgrader. If a Handler is also an Upgrader, the connection will // be upgraded after the Handler succeeds. // // This should only be used by libraries implementing an IMAP extension (e.g. // COMPRESS). type Upgrader interface { // Upgrade the connection. This method should call conn.Upgrade(). Upgrade(conn Conn) error } // A function that creates handlers. type HandlerFactory func() Handler // A function that creates SASL servers. type SASLServerFactory func(conn Conn) sasl.Server // An IMAP extension. type Extension interface { // Get capabilities provided by this extension for a given connection. Capabilities(c Conn) []string // Get the command handler factory for the provided command name. Command(name string) HandlerFactory } // An extension that provides additional features to each connection. type ConnExtension interface { Extension // This function will be called when a client connects to the server. It can // be used to add new features to the default Conn interface by implementing // new methods. NewConn(c Conn) Conn } // ErrStatusResp can be returned by a Handler to replace the default status // response. The response tag must be empty. // // Deprecated: Use imap.ErrStatusResp{res} instead. // // To disable the default status response, use imap.ErrStatusResp{nil} instead. func ErrStatusResp(res *imap.StatusResp) error { return &imap.ErrStatusResp{res} } // ErrNoStatusResp can be returned by a Handler to prevent the default status // response from being sent. // // Deprecated: Use imap.ErrStatusResp{nil} instead func ErrNoStatusResp() error { return &imap.ErrStatusResp{nil} } // An IMAP server. type Server struct { locker sync.Mutex listeners map[net.Listener]struct{} conns map[Conn]struct{} commands map[string]HandlerFactory auths map[string]SASLServerFactory extensions []Extension // TCP address to listen on. Addr string // This server's TLS configuration. TLSConfig *tls.Config // This server's backend. Backend backend.Backend // Backend updates that will be sent to connected clients. Updates <-chan backend.Update // Automatically logout clients after a duration. To do not logout users // automatically, set this to zero. The duration MUST be at least // MinAutoLogout (as stated in RFC 3501 section 5.4). AutoLogout time.Duration // Allow authentication over unencrypted connections. AllowInsecureAuth bool // An io.Writer to which all network activity will be mirrored. Debug io.Writer // ErrorLog specifies an optional logger for errors accepting // connections and unexpected behavior from handlers. // If nil, logging goes to os.Stderr via the log package's // standard logger. ErrorLog imap.Logger // The maximum literal size, in bytes. Literals exceeding this size will be // rejected. A value of zero disables the limit (this is the default). MaxLiteralSize uint32 } // Create a new IMAP server from an existing listener. func New(bkd backend.Backend) *Server { s := &Server{ listeners: make(map[net.Listener]struct{}), conns: make(map[Conn]struct{}), Backend: bkd, ErrorLog: log.New(os.Stderr, "imap/server: ", log.LstdFlags), } s.auths = map[string]SASLServerFactory{ sasl.Plain: func(conn Conn) sasl.Server { return sasl.NewPlainServer(func(identity, username, password string) error { if identity != "" && identity != username { return errors.New("Identities not supported") } user, err := bkd.Login(conn.Info(), username, password) if err != nil { return err } ctx := conn.Context() ctx.State = imap.AuthenticatedState ctx.User = user return nil }) }, } s.commands = map[string]HandlerFactory{ "NOOP": func() Handler { return &Noop{} }, "CAPABILITY": func() Handler { return &Capability{} }, "LOGOUT": func() Handler { return &Logout{} }, "STARTTLS": func() Handler { return &StartTLS{} }, "LOGIN": func() Handler { return &Login{} }, "AUTHENTICATE": func() Handler { return &Authenticate{} }, "SELECT": func() Handler { return &Select{} }, "EXAMINE": func() Handler { hdlr := &Select{} hdlr.ReadOnly = true return hdlr }, "CREATE": func() Handler { return &Create{} }, "DELETE": func() Handler { return &Delete{} }, "RENAME": func() Handler { return &Rename{} }, "SUBSCRIBE": func() Handler { return &Subscribe{} }, "UNSUBSCRIBE": func() Handler { return &Unsubscribe{} }, "LIST": func() Handler { return &List{} }, "LSUB": func() Handler { hdlr := &List{} hdlr.Subscribed = true return hdlr }, "STATUS": func() Handler { return &Status{} }, "APPEND": func() Handler { return &Append{} }, "UNSELECT": func() Handler { return &Unselect{} }, "IDLE": func() Handler { return &Idle{} }, "CHECK": func() Handler { return &Check{} }, "CLOSE": func() Handler { return &Close{} }, "EXPUNGE": func() Handler { return &Expunge{} }, "SEARCH": func() Handler { return &Search{} }, "FETCH": func() Handler { return &Fetch{} }, "STORE": func() Handler { return &Store{} }, "COPY": func() Handler { return &Copy{} }, "MOVE": func() Handler { return &Move{} }, "UID": func() Handler { return &Uid{} }, } return s } // Serve accepts incoming connections on the Listener l. func (s *Server) Serve(l net.Listener) error { s.locker.Lock() s.listeners[l] = struct{}{} s.locker.Unlock() defer func() { s.locker.Lock() defer s.locker.Unlock() l.Close() delete(s.listeners, l) }() updater, ok := s.Backend.(backend.BackendUpdater) if ok { s.Updates = updater.Updates() go s.listenUpdates() } for { c, err := l.Accept() if err != nil { return err } var conn Conn = newConn(s, c) for _, ext := range s.extensions { if ext, ok := ext.(ConnExtension); ok { conn = ext.NewConn(conn) } } go s.serveConn(conn) } } // ListenAndServe listens on the TCP network address s.Addr and then calls Serve // to handle requests on incoming connections. // // If s.Addr is blank, ":imap" is used. func (s *Server) ListenAndServe() error { addr := s.Addr if addr == "" { addr = ":imap" } l, err := net.Listen("tcp", addr) if err != nil { return err } return s.Serve(l) } // ListenAndServeTLS listens on the TCP network address s.Addr and then calls // Serve to handle requests on incoming TLS connections. // // If s.Addr is blank, ":imaps" is used. func (s *Server) ListenAndServeTLS() error { addr := s.Addr if addr == "" { addr = ":imaps" } l, err := tls.Listen("tcp", addr, s.TLSConfig) if err != nil { return err } return s.Serve(l) } func (s *Server) serveConn(conn Conn) error { s.locker.Lock() s.conns[conn] = struct{}{} s.locker.Unlock() defer func() { s.locker.Lock() defer s.locker.Unlock() conn.Close() delete(s.conns, conn) }() return conn.serve(conn) } // Command gets a command handler factory for the provided command name. func (s *Server) Command(name string) HandlerFactory { // Extensions can override builtin commands for _, ext := range s.extensions { if h := ext.Command(name); h != nil { return h } } return s.commands[name] } func (s *Server) listenUpdates() { for { update := <-s.Updates var res imap.WriterTo switch update := update.(type) { case *backend.StatusUpdate: res = update.StatusResp case *backend.MailboxUpdate: res = &responses.Select{Mailbox: update.MailboxStatus} case *backend.MailboxInfoUpdate: ch := make(chan *imap.MailboxInfo, 1) ch <- update.MailboxInfo close(ch) res = &responses.List{Mailboxes: ch} case *backend.MessageUpdate: ch := make(chan *imap.Message, 1) ch <- update.Message close(ch) res = &responses.Fetch{Messages: ch} case *backend.ExpungeUpdate: ch := make(chan uint32, 1) ch <- update.SeqNum close(ch) res = &responses.Expunge{SeqNums: ch} default: s.ErrorLog.Printf("unhandled update: %T\n", update) } if res == nil { continue } sends := make(chan struct{}) wait := 0 s.locker.Lock() for conn := range s.conns { ctx := conn.Context() if update.Username() != "" && (ctx.User == nil || ctx.User.Username() != update.Username()) { continue } if update.Mailbox() != "" && (ctx.Mailbox == nil || ctx.Mailbox.Name() != update.Mailbox()) { continue } if *conn.silent() { // If silent is set, do not send message updates if _, ok := res.(*responses.Fetch); ok { continue } } conn := conn // Copy conn to a local variable go func() { done := make(chan struct{}) conn.Context().Responses <- &response{ response: res, done: done, } <-done sends <- struct{}{} }() wait++ } s.locker.Unlock() if wait > 0 { go func() { for done := 0; done < wait; done++ { <-sends } close(update.Done()) }() } else { close(update.Done()) } } } // ForEachConn iterates through all opened connections. func (s *Server) ForEachConn(f func(Conn)) { s.locker.Lock() defer s.locker.Unlock() for conn := range s.conns { f(conn) } } // Stops listening and closes all current connections. func (s *Server) Close() error { s.locker.Lock() defer s.locker.Unlock() for l := range s.listeners { l.Close() } for conn := range s.conns { conn.Close() } return nil } // Enable some IMAP extensions on this server. // Wiki entry: https://github.com/emersion/go-imap/wiki/Using-extensions func (s *Server) Enable(extensions ...Extension) { for _, ext := range extensions { // Ignore built-in extensions if ext.Command("UNSELECT") != nil || ext.Command("MOVE") != nil || ext.Command("IDLE") != nil { continue } s.extensions = append(s.extensions, ext) } } // Enable an authentication mechanism on this server. // Wiki entry: https://github.com/emersion/go-imap/wiki/Using-authentication-mechanisms func (s *Server) EnableAuth(name string, f SASLServerFactory) { s.auths[name] = f } go-imap-1.2.0/server/server_test.go000066400000000000000000000017701412725504300172550ustar00rootroot00000000000000package server_test import ( "bufio" "net" "testing" "github.com/emersion/go-imap/backend/memory" "github.com/emersion/go-imap/server" ) // Extnesions that are always advertised by go-imap server. const builtinExtensions = "LITERAL+ SASL-IR CHILDREN UNSELECT MOVE APPENDLIMIT" func testServer(t *testing.T) (s *server.Server, conn net.Conn) { bkd := memory.New() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal("Cannot listen:", err) } s = server.New(bkd) s.AllowInsecureAuth = true go s.Serve(l) conn, err = net.Dial("tcp", l.Addr().String()) if err != nil { t.Fatal("Cannot connect to server:", err) } return } func TestServer_greeting(t *testing.T) { s, conn := testServer(t) defer s.Close() defer conn.Close() scanner := bufio.NewScanner(conn) scanner.Scan() // Wait for greeting greeting := scanner.Text() if greeting != "* OK [CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN] IMAP4rev1 Service Ready" { t.Fatal("Bad greeting:", greeting) } } go-imap-1.2.0/status.go000066400000000000000000000075611412725504300147310ustar00rootroot00000000000000package imap import ( "errors" ) // A status response type. type StatusRespType string // Status response types defined in RFC 3501 section 7.1. const ( // The OK response indicates an information message from the server. When // tagged, it indicates successful completion of the associated command. // The untagged form indicates an information-only message. StatusRespOk StatusRespType = "OK" // The NO response indicates an operational error message from the // server. When tagged, it indicates unsuccessful completion of the // associated command. The untagged form indicates a warning; the // command can still complete successfully. StatusRespNo StatusRespType = "NO" // The BAD response indicates an error message from the server. When // tagged, it reports a protocol-level error in the client's command; // the tag indicates the command that caused the error. The untagged // form indicates a protocol-level error for which the associated // command can not be determined; it can also indicate an internal // server failure. StatusRespBad StatusRespType = "BAD" // The PREAUTH response is always untagged, and is one of three // possible greetings at connection startup. It indicates that the // connection has already been authenticated by external means; thus // no LOGIN command is needed. StatusRespPreauth StatusRespType = "PREAUTH" // The BYE response is always untagged, and indicates that the server // is about to close the connection. StatusRespBye StatusRespType = "BYE" ) type StatusRespCode string // Status response codes defined in RFC 3501 section 7.1. const ( CodeAlert StatusRespCode = "ALERT" CodeBadCharset StatusRespCode = "BADCHARSET" CodeCapability StatusRespCode = "CAPABILITY" CodeParse StatusRespCode = "PARSE" CodePermanentFlags StatusRespCode = "PERMANENTFLAGS" CodeReadOnly StatusRespCode = "READ-ONLY" CodeReadWrite StatusRespCode = "READ-WRITE" CodeTryCreate StatusRespCode = "TRYCREATE" CodeUidNext StatusRespCode = "UIDNEXT" CodeUidValidity StatusRespCode = "UIDVALIDITY" CodeUnseen StatusRespCode = "UNSEEN" ) // A status response. // See RFC 3501 section 7.1 type StatusResp struct { // The response tag. If empty, it defaults to *. Tag string // The status type. Type StatusRespType // The status code. // See https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml Code StatusRespCode // Arguments provided with the status code. Arguments []interface{} // The status info. Info string } func (r *StatusResp) resp() {} // If this status is NO or BAD, returns an error with the status info. // Otherwise, returns nil. func (r *StatusResp) Err() error { if r == nil { // No status response, connection closed before we get one return errors.New("imap: connection closed during command execution") } if r.Type == StatusRespNo || r.Type == StatusRespBad { return errors.New(r.Info) } return nil } func (r *StatusResp) WriteTo(w *Writer) error { tag := RawString(r.Tag) if tag == "" { tag = "*" } if err := w.writeFields([]interface{}{RawString(tag), RawString(r.Type)}); err != nil { return err } if err := w.writeString(string(sp)); err != nil { return err } if r.Code != "" { if err := w.writeRespCode(r.Code, r.Arguments); err != nil { return err } if err := w.writeString(string(sp)); err != nil { return err } } if err := w.writeString(r.Info); err != nil { return err } return w.writeCrlf() } // ErrStatusResp can be returned by a server.Handler to replace the default status // response. The response tag must be empty. // // To suppress default response, Resp should be set to nil. type ErrStatusResp struct { // Response to send instead of default. Resp *StatusResp } func (err *ErrStatusResp) Error() string { if err.Resp == nil { return "imap: suppressed response" } return err.Resp.Info } go-imap-1.2.0/status_test.go000066400000000000000000000041611412725504300157610ustar00rootroot00000000000000package imap_test import ( "bytes" "testing" "github.com/emersion/go-imap" ) func TestStatusResp_WriteTo(t *testing.T) { tests := []struct { input *imap.StatusResp expected string }{ { input: &imap.StatusResp{ Tag: "*", Type: imap.StatusRespOk, }, expected: "* OK \r\n", }, { input: &imap.StatusResp{ Tag: "*", Type: imap.StatusRespOk, Info: "LOGIN completed", }, expected: "* OK LOGIN completed\r\n", }, { input: &imap.StatusResp{ Tag: "42", Type: imap.StatusRespBad, Info: "Invalid arguments", }, expected: "42 BAD Invalid arguments\r\n", }, { input: &imap.StatusResp{ Tag: "a001", Type: imap.StatusRespOk, Code: "READ-ONLY", Info: "EXAMINE completed", }, expected: "a001 OK [READ-ONLY] EXAMINE completed\r\n", }, { input: &imap.StatusResp{ Tag: "*", Type: imap.StatusRespOk, Code: "CAPABILITY", Arguments: []interface{}{imap.RawString("IMAP4rev1")}, Info: "IMAP4rev1 service ready", }, expected: "* OK [CAPABILITY IMAP4rev1] IMAP4rev1 service ready\r\n", }, } for i, test := range tests { b := &bytes.Buffer{} w := imap.NewWriter(b) if err := test.input.WriteTo(w); err != nil { t.Errorf("Cannot write status #%v, got error: %v", i, err) continue } o := b.String() if o != test.expected { t.Errorf("Invalid output for status #%v: %v", i, o) } } } func TestStatus_Err(t *testing.T) { status := &imap.StatusResp{Type: imap.StatusRespOk, Info: "All green"} if err := status.Err(); err != nil { t.Error("OK status returned error:", err) } status = &imap.StatusResp{Type: imap.StatusRespBad, Info: "BAD!"} if err := status.Err(); err == nil { t.Error("BAD status didn't returned error:", err) } else if err.Error() != "BAD!" { t.Error("BAD status returned incorrect error message:", err) } status = &imap.StatusResp{Type: imap.StatusRespNo, Info: "NO!"} if err := status.Err(); err == nil { t.Error("NO status didn't returned error:", err) } else if err.Error() != "NO!" { t.Error("NO status returned incorrect error message:", err) } } go-imap-1.2.0/utf7/000077500000000000000000000000001412725504300137335ustar00rootroot00000000000000go-imap-1.2.0/utf7/decoder.go000066400000000000000000000055631412725504300157000ustar00rootroot00000000000000package utf7 import ( "errors" "unicode/utf16" "unicode/utf8" "golang.org/x/text/transform" ) // ErrInvalidUTF7 means that a transformer encountered invalid UTF-7. var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7") type decoder struct { ascii bool } func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { for i := 0; i < len(src); i++ { ch := src[i] if ch < min || ch > max { // Illegal code point in ASCII mode err = ErrInvalidUTF7 return } if ch != '&' { if nDst+1 > len(dst) { err = transform.ErrShortDst return } nSrc++ dst[nDst] = ch nDst++ d.ascii = true continue } // Find the end of the Base64 or "&-" segment start := i + 1 for i++; i < len(src) && src[i] != '-'; i++ { if src[i] == '\r' || src[i] == '\n' { // base64 package ignores CR and LF err = ErrInvalidUTF7 return } } if i == len(src) { // Implicit shift ("&...") if atEOF { err = ErrInvalidUTF7 } else { err = transform.ErrShortSrc } return } var b []byte if i == start { // Escape sequence "&-" b = []byte{'&'} d.ascii = true } else { // Control or non-ASCII code points in base64 if !d.ascii { // Null shift ("&...-&...-") err = ErrInvalidUTF7 return } b = decode(src[start:i]) d.ascii = false } if len(b) == 0 { // Bad encoding err = ErrInvalidUTF7 return } if nDst+len(b) > len(dst) { d.ascii = true err = transform.ErrShortDst return } nSrc = i + 1 for _, ch := range b { dst[nDst] = ch nDst++ } } if atEOF { d.ascii = true } return } func (d *decoder) Reset() { d.ascii = true } // Extracts UTF-16-BE bytes from base64 data and converts them to UTF-8. // A nil slice is returned if the encoding is invalid. func decode(b64 []byte) []byte { var b []byte // Allocate a single block of memory large enough to store the Base64 data // (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes. // Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence, // double the space allocation for UTF-8. if n := len(b64); b64[n-1] == '=' { return nil } else if n&3 == 0 { b = make([]byte, b64Enc.DecodedLen(n)*3) } else { n += 4 - n&3 b = make([]byte, n+b64Enc.DecodedLen(n)*3) copy(b[copy(b, b64):n], []byte("==")) b64, b = b[:n], b[n:] } // Decode Base64 into the first 1/3rd of b n, err := b64Enc.Decode(b, b64) if err != nil || n&1 == 1 { return nil } // Decode UTF-16-BE into the remaining 2/3rds of b b, s := b[:n], b[n:] j := 0 for i := 0; i < n; i += 2 { r := rune(b[i])<<8 | rune(b[i+1]) if utf16.IsSurrogate(r) { if i += 2; i == n { return nil } r2 := rune(b[i])<<8 | rune(b[i+1]) if r = utf16.DecodeRune(r, r2); r == repl { return nil } } else if min <= r && r <= max { return nil } j += utf8.EncodeRune(s[j:], r) } return s[:j] } go-imap-1.2.0/utf7/decoder_test.go000066400000000000000000000061471412725504300167360ustar00rootroot00000000000000package utf7_test import ( "strings" "testing" "github.com/emersion/go-imap/utf7" ) var decode = []struct { in string out string ok bool }{ // Basics (the inverse test on encode checks other valid inputs) {"", "", true}, {"abc", "abc", true}, {"&-abc", "&abc", true}, {"abc&-", "abc&", true}, {"a&-b&-c", "a&b&c", true}, {"&ABk-", "\x19", true}, {"&AB8-", "\x1F", true}, {"ABk-", "ABk-", true}, {"&-,&-&AP8-&-", "&,&\u00FF&", true}, {"&-&-,&AP8-&-", "&&,\u00FF&", true}, {"abc &- &AP8A,wD,- &- xyz", "abc & \u00FF\u00FF\u00FF & xyz", true}, // Illegal code point in ASCII {"\x00", "", false}, {"\x1F", "", false}, {"abc\n", "", false}, {"abc\x7Fxyz", "", false}, {"\uFFFD", "", false}, {"\u041C", "", false}, // Invalid Base64 alphabet {"&/+8-", "", false}, {"&*-", "", false}, {"&ZeVnLIqe -", "", false}, // CR and LF in Base64 {"&ZeVnLIqe\r\n-", "", false}, {"&ZeVnLIqe\r\n\r\n-", "", false}, {"&ZeVn\r\n\r\nLIqe-", "", false}, // Padding not stripped {"&AAAAHw=-", "", false}, {"&AAAAHw==-", "", false}, {"&AAAAHwB,AIA=-", "", false}, {"&AAAAHwB,AIA==-", "", false}, // One byte short {"&2A-", "", false}, {"&2ADc-", "", false}, {"&AAAAHwB,A-", "", false}, {"&AAAAHwB,A=-", "", false}, {"&AAAAHwB,A==-", "", false}, {"&AAAAHwB,A===-", "", false}, {"&AAAAHwB,AI-", "", false}, {"&AAAAHwB,AI=-", "", false}, {"&AAAAHwB,AI==-", "", false}, // Implicit shift {"&", "", false}, {"&Jjo", "", false}, {"Jjo&", "", false}, {"&Jjo&", "", false}, {"&Jjo!", "", false}, {"&Jjo+", "", false}, {"abc&Jjo", "", false}, // Null shift {"&AGE-&Jjo-", "", false}, {"&U,BTFw-&ZeVnLIqe-", "", false}, // Long input with Base64 at the end {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa &2D3eCg- &2D3eCw- &2D3eDg-", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \U0001f60a \U0001f60b \U0001f60e", true}, // Long input in Base64 between short ASCII {"00000000000000000000 &MEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEI- 00000000000000000000", "00000000000000000000 " + strings.Repeat("\U00003042", 37) + " 00000000000000000000", true}, // ASCII in Base64 {"&AGE-", "", false}, // "a" {"&ACY-", "", false}, // "&" {"&AGgAZQBsAGwAbw-", "", false}, // "hello" {"&JjoAIQ-", "", false}, // "\u263a!" // Bad surrogate {"&2AA-", "", false}, // U+D800 {"&2AD-", "", false}, // U+D800 {"&3AA-", "", false}, // U+DC00 {"&2AAAQQ-", "", false}, // U+D800 'A' {"&2AD,,w-", "", false}, // U+D800 U+FFFF {"&3ADYAA-", "", false}, // U+DC00 U+D800 } func TestDecoder(t *testing.T) { dec := utf7.Encoding.NewDecoder() for _, test := range decode { out, err := dec.String(test.in) if out != test.out { t.Errorf("UTF7Decode(%+q) expected %+q; got %+q", test.in, test.out, out) } if test.ok { if err != nil { t.Errorf("UTF7Decode(%+q) unexpected error; %v", test.in, err) } } else if err == nil { t.Errorf("UTF7Decode(%+q) expected error", test.in) } } } go-imap-1.2.0/utf7/encoder.go000066400000000000000000000031511412725504300157010ustar00rootroot00000000000000package utf7 import ( "unicode/utf16" "unicode/utf8" "golang.org/x/text/transform" ) type encoder struct{} func (e *encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { for i := 0; i < len(src); { ch := src[i] var b []byte if min <= ch && ch <= max { b = []byte{ch} if ch == '&' { b = append(b, '-') } i++ } else { start := i // Find the next printable ASCII code point i++ for i < len(src) && (src[i] < min || src[i] > max) { i++ } if !atEOF && i == len(src) { err = transform.ErrShortSrc return } b = encode(src[start:i]) } if nDst+len(b) > len(dst) { err = transform.ErrShortDst return } nSrc = i for _, ch := range b { dst[nDst] = ch nDst++ } } return } func (e *encoder) Reset() {} // Converts string s from UTF-8 to UTF-16-BE, encodes the result as base64, // removes the padding, and adds UTF-7 shifts. func encode(s []byte) []byte { // len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no // control code points (see table below). b := make([]byte, 0, len(s)+4) for len(s) > 0 { r, size := utf8.DecodeRune(s) if r > utf8.MaxRune { r, size = utf8.RuneError, 1 // Bug fix (issue 3785) } s = s[size:] if r1, r2 := utf16.EncodeRune(r); r1 != repl { b = append(b, byte(r1>>8), byte(r1)) r = r2 } b = append(b, byte(r>>8), byte(r)) } // Encode as base64 n := b64Enc.EncodedLen(len(b)) + 2 b64 := make([]byte, n) b64Enc.Encode(b64[1:], b) // Strip padding n -= 2 - (len(b)+2)%3 b64 = b64[:n] // Add UTF-7 shifts b64[0] = '&' b64[n-1] = '-' return b64 } go-imap-1.2.0/utf7/encoder_test.go000066400000000000000000000076431412725504300167520ustar00rootroot00000000000000package utf7_test import ( "testing" "github.com/emersion/go-imap/utf7" ) var encode = []struct { in string out string ok bool }{ // Printable ASCII {"", "", true}, {"a", "a", true}, {"ab", "ab", true}, {"-", "-", true}, {"&", "&-", true}, {"&&", "&-&-", true}, {"&&&-&", "&-&-&--&-", true}, {"-&*&-", "-&-*&--", true}, {"a&b", "a&-b", true}, {"a&", "a&-", true}, {"&b", "&-b", true}, {"-a&", "-a&-", true}, {"&b-", "&-b-", true}, // Unicode range {"\u0000", "&AAA-", true}, {"\n", "&AAo-", true}, {"\r", "&AA0-", true}, {"\u001F", "&AB8-", true}, {"\u0020", " ", true}, {"\u0025", "%", true}, {"\u0026", "&-", true}, {"\u0027", "'", true}, {"\u007E", "~", true}, {"\u007F", "&AH8-", true}, {"\u0080", "&AIA-", true}, {"\u00FF", "&AP8-", true}, {"\u07FF", "&B,8-", true}, {"\u0800", "&CAA-", true}, {"\uFFEF", "&,+8-", true}, {"\uFFFF", "&,,8-", true}, {"\U00010000", "&2ADcAA-", true}, {"\U0010FFFF", "&2,,f,w-", true}, // Padding {"\x00\x1F", "&AAAAHw-", true}, // 2 {"\x00\x1F\x7F", "&AAAAHwB,-", true}, // 0 {"\x00\x1F\x7F\u0080", "&AAAAHwB,AIA-", true}, // 1 {"\x00\x1F\x7F\u0080\u00FF", "&AAAAHwB,AIAA,w-", true}, // 2 // Mix {"a\x00", "a&AAA-", true}, {"\x00a", "&AAA-a", true}, {"&\x00", "&-&AAA-", true}, {"\x00&", "&AAA-&-", true}, {"a\x00&", "a&AAA-&-", true}, {"a&\x00", "a&-&AAA-", true}, {"&a\x00", "&-a&AAA-", true}, {"&\x00a", "&-&AAA-a", true}, {"\x00&a", "&AAA-&-a", true}, {"\x00a&", "&AAA-a&-", true}, {"ab&\uFFFF", "ab&-&,,8-", true}, {"a&b\uFFFF", "a&-b&,,8-", true}, {"&ab\uFFFF", "&-ab&,,8-", true}, {"ab\uFFFF&", "ab&,,8-&-", true}, {"a\uFFFFb&", "a&,,8-b&-", true}, {"\uFFFFab&", "&,,8-ab&-", true}, {"\x20\x25&\x27\x7E", " %&-'~", true}, {"\x1F\x20&\x7E\x7F", "&AB8- &-~&AH8-", true}, {"&\x00\x19\x7F\u0080", "&-&AAAAGQB,AIA-", true}, {"\x00&\x19\x7F\u0080", "&AAA-&-&ABkAfwCA-", true}, {"\x00\x19&\x7F\u0080", "&AAAAGQ-&-&AH8AgA-", true}, {"\x00\x19\x7F&\u0080", "&AAAAGQB,-&-&AIA-", true}, {"\x00\x19\x7F\u0080&", "&AAAAGQB,AIA-&-", true}, {"&\x00\x1F\x7F\u0080", "&-&AAAAHwB,AIA-", true}, {"\x00&\x1F\x7F\u0080", "&AAA-&-&AB8AfwCA-", true}, {"\x00\x1F&\x7F\u0080", "&AAAAHw-&-&AH8AgA-", true}, {"\x00\x1F\x7F&\u0080", "&AAAAHwB,-&-&AIA-", true}, {"\x00\x1F\x7F\u0080&", "&AAAAHwB,AIA-&-", true}, // Russian {"\u041C\u0430\u043A\u0441\u0438\u043C \u0425\u0438\u0442\u0440\u043E\u0432", "&BBwEMAQ6BEEEOAQ8- &BCUEOARCBEAEPgQy-", true}, // RFC 3501 {"~peter/mail/\u53F0\u5317/\u65E5\u672C\u8A9E", "~peter/mail/&U,BTFw-/&ZeVnLIqe-", true}, {"~peter/mail/\u53F0\u5317/\u65E5\u672C\u8A9E", "~peter/mail/&U,BTFw-/&ZeVnLIqe-", true}, {"\u263A!", "&Jjo-!", true}, {"\u53F0\u5317\u65E5\u672C\u8A9E", "&U,BTF2XlZyyKng-", true}, // RFC 2152 (modified) {"\u0041\u2262\u0391\u002E", "A&ImIDkQ-.", true}, {"Hi Mom -\u263A-!", "Hi Mom -&Jjo--!", true}, {"\u65E5\u672C\u8A9E", "&ZeVnLIqe-", true}, // 8->16 and 24->16 byte UTF-8 to UTF-16 conversion {"\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007", "&AAAAAQACAAMABAAFAAYABw-", true}, {"\u0800\u0801\u0802\u0803\u0804\u0805\u0806\u0807", "&CAAIAQgCCAMIBAgFCAYIBw-", true}, // Invalid UTF-8 (bad bytes are converted to U+FFFD) {"\xC0\x80", "&,,3,,Q-", false}, // U+0000 {"\xF4\x90\x80\x80", "&,,3,,f,9,,0-", false}, // U+110000 {"\xF7\xBF\xBF\xBF", "&,,3,,f,9,,0-", false}, // U+1FFFFF {"\xF8\x88\x80\x80\x80", "&,,3,,f,9,,3,,Q-", false}, // U+200000 {"\xF4\x8F\xBF\x3F", "&,,3,,f,9-?", false}, // U+10FFFF (bad byte) {"\xF4\x8F\xBF", "&,,3,,f,9-", false}, // U+10FFFF (short) {"\xF4\x8F", "&,,3,,Q-", false}, {"\xF4", "&,,0-", false}, {"\x00\xF4\x00", "&AAD,,QAA-", false}, } func TestEncoder(t *testing.T) { enc := utf7.Encoding.NewEncoder() for _, test := range encode { out, _ := enc.String(test.in) if out != test.out { t.Errorf("UTF7Encode(%+q) expected %+q; got %+q", test.in, test.out, out) } } } go-imap-1.2.0/utf7/utf7.go000066400000000000000000000013551412725504300151530ustar00rootroot00000000000000// Package utf7 implements modified UTF-7 encoding defined in RFC 3501 section 5.1.3 package utf7 import ( "encoding/base64" "golang.org/x/text/encoding" ) const ( min = 0x20 // Minimum self-representing UTF-7 value max = 0x7E // Maximum self-representing UTF-7 value repl = '\uFFFD' // Unicode replacement code point ) var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,") type enc struct{} func (e enc) NewDecoder() *encoding.Decoder { return &encoding.Decoder{ Transformer: &decoder{true}, } } func (e enc) NewEncoder() *encoding.Encoder { return &encoding.Encoder{ Transformer: &encoder{}, } } // Encoding is the modified UTF-7 encoding. var Encoding encoding.Encoding = enc{} go-imap-1.2.0/write.go000066400000000000000000000125341412725504300145340ustar00rootroot00000000000000package imap import ( "bytes" "fmt" "io" "io/ioutil" "strconv" "time" "unicode" ) type flusher interface { Flush() error } type ( // A raw string. RawString string ) type WriterTo interface { WriteTo(w *Writer) error } func formatNumber(num uint32) string { return strconv.FormatUint(uint64(num), 10) } // Convert a string list to a field list. func FormatStringList(list []string) (fields []interface{}) { fields = make([]interface{}, len(list)) for i, v := range list { fields[i] = v } return } // Check if a string is 8-bit clean. func isAscii(s string) bool { for _, c := range s { if c > unicode.MaxASCII || unicode.IsControl(c) { return false } } return true } // An IMAP writer. type Writer struct { io.Writer AllowAsyncLiterals bool continues <-chan bool } // Helper function to write a string to w. func (w *Writer) writeString(s string) error { _, err := io.WriteString(w.Writer, s) return err } func (w *Writer) writeCrlf() error { if err := w.writeString(crlf); err != nil { return err } return w.Flush() } func (w *Writer) writeNumber(num uint32) error { return w.writeString(formatNumber(num)) } func (w *Writer) writeQuoted(s string) error { return w.writeString(strconv.Quote(s)) } func (w *Writer) writeQuotedOrLiteral(s string) error { if !isAscii(s) { // IMAP doesn't allow 8-bit data outside literals return w.writeLiteral(bytes.NewBufferString(s)) } return w.writeQuoted(s) } func (w *Writer) writeDateTime(t time.Time, layout string) error { if t.IsZero() { return w.writeString(nilAtom) } return w.writeQuoted(t.Format(layout)) } func (w *Writer) writeFields(fields []interface{}) error { for i, field := range fields { if i > 0 { // Write separator if err := w.writeString(string(sp)); err != nil { return err } } if err := w.writeField(field); err != nil { return err } } return nil } func (w *Writer) writeList(fields []interface{}) error { if err := w.writeString(string(listStart)); err != nil { return err } if err := w.writeFields(fields); err != nil { return err } return w.writeString(string(listEnd)) } // LiteralLengthErr is returned when the Len() of the Literal object does not // match the actual length of the byte stream. type LiteralLengthErr struct { Actual int Expected int } func (e LiteralLengthErr) Error() string { return fmt.Sprintf("imap: size of Literal is not equal to Len() (%d != %d)", e.Expected, e.Actual) } func (w *Writer) writeLiteral(l Literal) error { if l == nil { return w.writeString(nilAtom) } unsyncLiteral := w.AllowAsyncLiterals && l.Len() <= 4096 header := string(literalStart) + strconv.Itoa(l.Len()) if unsyncLiteral { header += string('+') } header += string(literalEnd) + crlf if err := w.writeString(header); err != nil { return err } // If a channel is available, wait for a continuation request before sending data if !unsyncLiteral && w.continues != nil { // Make sure to flush the writer, otherwise we may never receive a continuation request if err := w.Flush(); err != nil { return err } if !<-w.continues { return fmt.Errorf("imap: cannot send literal: no continuation request received") } } // In case of bufio.Buffer, it will be 0 after io.Copy. literalLen := int64(l.Len()) n, err := io.CopyN(w, l, literalLen) if err != nil { if err == io.EOF && n != literalLen { return LiteralLengthErr{int(n), l.Len()} } return err } extra, _ := io.Copy(ioutil.Discard, l) if extra != 0 { return LiteralLengthErr{int(n + extra), l.Len()} } return nil } func (w *Writer) writeField(field interface{}) error { if field == nil { return w.writeString(nilAtom) } switch field := field.(type) { case RawString: return w.writeString(string(field)) case string: return w.writeQuotedOrLiteral(field) case int: return w.writeNumber(uint32(field)) case uint32: return w.writeNumber(field) case Literal: return w.writeLiteral(field) case []interface{}: return w.writeList(field) case envelopeDateTime: return w.writeDateTime(time.Time(field), envelopeDateTimeLayout) case searchDate: return w.writeDateTime(time.Time(field), searchDateLayout) case Date: return w.writeDateTime(time.Time(field), DateLayout) case DateTime: return w.writeDateTime(time.Time(field), DateTimeLayout) case time.Time: return w.writeDateTime(field, DateTimeLayout) case *SeqSet: return w.writeString(field.String()) case *BodySectionName: // Can contain spaces - that's why we don't just pass it as a string return w.writeString(string(field.FetchItem())) } return fmt.Errorf("imap: cannot format field: %v", field) } func (w *Writer) writeRespCode(code StatusRespCode, args []interface{}) error { if err := w.writeString(string(respCodeStart)); err != nil { return err } fields := []interface{}{RawString(code)} fields = append(fields, args...) if err := w.writeFields(fields); err != nil { return err } return w.writeString(string(respCodeEnd)) } func (w *Writer) writeLine(fields ...interface{}) error { if err := w.writeFields(fields); err != nil { return err } return w.writeCrlf() } func (w *Writer) Flush() error { if f, ok := w.Writer.(flusher); ok { return f.Flush() } return nil } func NewWriter(w io.Writer) *Writer { return &Writer{Writer: w} } func NewClientWriter(w io.Writer, continues <-chan bool) *Writer { return &Writer{Writer: w, continues: continues} } go-imap-1.2.0/write_test.go000066400000000000000000000137221412725504300155730ustar00rootroot00000000000000package imap import ( "bytes" "strings" "testing" "time" ) func newWriter() (w *Writer, b *bytes.Buffer) { b = &bytes.Buffer{} w = NewWriter(b) return } func TestWriter_WriteCrlf(t *testing.T) { w, b := newWriter() if err := w.writeCrlf(); err != nil { t.Error(err) } if b.String() != "\r\n" { t.Error("Not a CRLF") } } func TestWriter_WriteField_Nil(t *testing.T) { w, b := newWriter() if err := w.writeField(nil); err != nil { t.Error(err) } if b.String() != "NIL" { t.Error("Not NIL") } } func TestWriter_WriteField_Number(t *testing.T) { w, b := newWriter() if err := w.writeField(uint32(42)); err != nil { t.Error(err) } if b.String() != "42" { t.Error("Not the expected number") } } func TestWriter_WriteField_Atom(t *testing.T) { w, b := newWriter() if err := w.writeField(RawString("BODY[]")); err != nil { t.Error(err) } if b.String() != "BODY[]" { t.Error("Not the expected atom") } } func TestWriter_WriteString_Quoted(t *testing.T) { w, b := newWriter() if err := w.writeField("I love potatoes!"); err != nil { t.Error(err) } if b.String() != "\"I love potatoes!\"" { t.Error("Not the expected quoted string") } } func TestWriter_WriteString_Quoted_WithSpecials(t *testing.T) { w, b := newWriter() if err := w.writeField("I love \"1984\"!"); err != nil { t.Error(err) } if b.String() != "\"I love \\\"1984\\\"!\"" { t.Error("Not the expected quoted string") } } func TestWriter_WriteField_ForcedQuoted(t *testing.T) { w, b := newWriter() if err := w.writeField("dille"); err != nil { t.Error(err) } if b.String() != "\"dille\"" { t.Error("Not the expected atom:", b.String()) } } func TestWriter_WriteField_8bitString(t *testing.T) { w, b := newWriter() if err := w.writeField("☺"); err != nil { t.Error(err) } if b.String() != "{3}\r\n☺" { t.Error("Not the expected atom") } } func TestWriter_WriteField_NilString(t *testing.T) { w, b := newWriter() if err := w.writeField("NIL"); err != nil { t.Error(err) } if b.String() != "\"NIL\"" { t.Error("Not the expected quoted string") } } func TestWriter_WriteField_EmptyString(t *testing.T) { w, b := newWriter() if err := w.writeField(""); err != nil { t.Error(err) } if b.String() != "\"\"" { t.Error("Not the expected quoted string") } } func TestWriter_WriteField_DateTime(t *testing.T) { w, b := newWriter() dt := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) if err := w.writeField(dt); err != nil { t.Error(err) } if b.String() != `"10-Nov-2009 23:00:00 +0000"` { t.Error("Invalid date:", b.String()) } } func TestWriter_WriteField_ZeroDateTime(t *testing.T) { w, b := newWriter() dt := time.Time{} if err := w.writeField(dt); err != nil { t.Error(err) } if b.String() != "NIL" { t.Error("Invalid nil date:", b.String()) } } func TestWriter_WriteFields(t *testing.T) { w, b := newWriter() if err := w.writeFields([]interface{}{RawString("hey"), 42}); err != nil { t.Error(err) } if b.String() != "hey 42" { t.Error("Not the expected fields") } } func TestWriter_WriteField_SimpleList(t *testing.T) { w, b := newWriter() if err := w.writeField([]interface{}{RawString("hey"), 42}); err != nil { t.Error(err) } if b.String() != "(hey 42)" { t.Error("Not the expected list") } } func TestWriter_WriteField_NestedList(t *testing.T) { w, b := newWriter() list := []interface{}{ RawString("toplevel"), []interface{}{ RawString("nested"), 0, }, 22, } if err := w.writeField(list); err != nil { t.Error(err) } if b.String() != "(toplevel (nested 0) 22)" { t.Error("Not the expected list:", b.String()) } } func TestWriter_WriteField_Literal(t *testing.T) { w, b := newWriter() literal := bytes.NewBufferString("hello world") if err := w.writeField(literal); err != nil { t.Error(err) } if b.String() != "{11}\r\nhello world" { t.Error("Not the expected literal") } } func TestWriter_WriteField_NonSyncLiteral(t *testing.T) { w, b := newWriter() w.AllowAsyncLiterals = true literal := bytes.NewBufferString("hello world") if err := w.writeField(literal); err != nil { t.Error(err) } if b.String() != "{11+}\r\nhello world" { t.Error("Not the expected literal") } } func TestWriter_WriteField_LargeNonSyncLiteral(t *testing.T) { w, b := newWriter() w.AllowAsyncLiterals = true s := strings.Repeat("A", 4097) literal := bytes.NewBufferString(s) if err := w.writeField(literal); err != nil { t.Error(err) } if b.String() != "{4097}\r\n"+s { t.Error("Not the expected literal") } } func TestWriter_WriteField_SeqSet(t *testing.T) { w, b := newWriter() seqSet, _ := ParseSeqSet("3:4,6,42:*") if err := w.writeField(seqSet); err != nil { t.Error(err) } if s := b.String(); s != "3:4,6,42:*" { t.Error("Not the expected sequence set", s) } } func TestWriter_WriteField_BodySectionName(t *testing.T) { w, b := newWriter() name, _ := ParseBodySectionName("BODY.PEEK[HEADER.FIELDS (date subject from to cc)]") if err := w.writeField(name.resp()); err != nil { t.Error(err) } if s := b.String(); s != "BODY[HEADER.FIELDS (date subject from to cc)]" { t.Error("Not the expected body section name", s) } } func TestWriter_WriteRespCode_NoArgs(t *testing.T) { w, b := newWriter() if err := w.writeRespCode("READ-ONLY", nil); err != nil { t.Error(err) } if b.String() != "[READ-ONLY]" { t.Error("Not the expected response code:", b.String()) } } func TestWriter_WriteRespCode_WithArgs(t *testing.T) { w, b := newWriter() args := []interface{}{RawString("IMAP4rev1"), RawString("STARTTLS"), RawString("LOGINDISABLED")} if err := w.writeRespCode("CAPABILITY", args); err != nil { t.Error(err) } if b.String() != "[CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED]" { t.Error("Not the expected response code:", b.String()) } } func TestWriter_WriteLine(t *testing.T) { w, b := newWriter() if err := w.writeLine(RawString("*"), RawString("OK")); err != nil { t.Error(err) } if b.String() != "* OK\r\n" { t.Error("Not the expected line:", b.String()) } }