├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── reader.go └── reader_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | 6 | install: 7 | - go get -t -v . 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Miguel Molina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-imapreader [![Build Status](https://travis-ci.org/erizocosmico/go-imapreader.svg?branch=master)](https://travis-ci.org/erizocosmico/go-imapreader) [![GoDoc](https://godoc.org/github.com/erizocosmico/go-imapreader?status.svg)](http://godoc.org/github.com/erizocosmico/go-imapreader) 2 | Simple interface for reading IMAP emails in Golang. 3 | 4 | ## Install 5 | 6 | ``` 7 | go get github.com/erizocosmico/go-imapreader 8 | ``` 9 | 10 | ## Usage 11 | 12 | ```go 13 | import ( 14 | "github.com/erizocosmico/go-imapreader" 15 | ) 16 | 17 | func main() { 18 | r, err := imapreader.NewReader(imapreader.Options{ 19 | Addr: os.Getenv("TEST_ADDR"), 20 | Username: os.Getenv("TEST_USER"), 21 | Password: os.Getenv("TEST_PWD"), 22 | TLS: true, 23 | Timeout: 60 * time.Second, 24 | MarkSeen: true, 25 | }) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | if err := r.Login(); err != nil { 31 | panic(err) 32 | } 33 | defer r.Logout() 34 | 35 | // Search for all the emails in "all mail" that are unseen 36 | // read the docs for more search filters 37 | messages, err := r.List(imapreader.GMailAllMail, imapreader.SearchUnseen) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | // do stuff with messages 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/erizocosmico/go-imapreader 2 | 3 | require ( 4 | github.com/kr/pretty v0.1.0 // indirect 5 | github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d 6 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 7 | ) 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 2 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 3 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 4 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 5 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 6 | github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d h1:+DgqA2tuWi/8VU+gVgBAa7+WZrnFbPKhQWbKBB54cVs= 7 | github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d/go.mod h1:xacC5qXZnL/ooiitVoe3BtI1OotFTqi5zICBs9J5Fyk= 8 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 9 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package imapreader 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/mail" 7 | "time" 8 | 9 | "github.com/mxk/go-imap/imap" 10 | ) 11 | 12 | const ( 13 | // GMail inbox mailbox 14 | GMailInbox string = "INBOX" 15 | // GMail all mail mailbox 16 | GMailAllMail string = "[Gmail]/All Mail" 17 | ) 18 | 19 | var ( 20 | // Search only the unseen messages 21 | SearchUnseen = []imap.Field{"UNSEEN"} 22 | // Search all messages 23 | SearchAll = []imap.Field{"ALL"} 24 | // Search only answered messages 25 | SearchAnswered = []imap.Field{"ANSWERED"} 26 | // Search only unanswered messages 27 | SearchUnanswered = []imap.Field{"UNANSWERED"} 28 | // Search only deleted messages 29 | SearchDeleted = []imap.Field{"DELETED"} 30 | // Search only not deleted messages 31 | SearchUndeleted = []imap.Field{"UNDELETED"} 32 | // Search only flagged messages 33 | SearchFlagged = []imap.Field{"FLAGGED"} 34 | // Search only not flagged messages 35 | SearchUnflagged = []imap.Field{"UNFLAGGED"} 36 | // Search only new messages 37 | SearchNew = []imap.Field{"NEW"} 38 | // Search only old messages 39 | SearchOld = []imap.Field{"OLD"} 40 | // Search only recent messages 41 | SearchRecent = []imap.Field{"RECENT"} 42 | // Search only seen messages 43 | SearchSeen = []imap.Field{"SEEN"} 44 | ) 45 | 46 | // Reader is a client to read messages from an IMAP server 47 | // only contains the List operation as well as login and logout operations 48 | type Reader interface { 49 | // Login logs in with the options provided using the connection established 50 | // when the reader was created 51 | Login() error 52 | // Logout terminates the current "session" 53 | Logout() error 54 | // List retrieves the list of emails in a mailbox that satisfy the given search criteria 55 | List(string, []imap.Field) ([]*Email, error) 56 | } 57 | 58 | type reader struct { 59 | opts Options 60 | client *imap.Client 61 | } 62 | 63 | // Options define the settings to perform all the reader operations 64 | type Options struct { 65 | // IMAP server address with port 66 | Addr string 67 | // Username 68 | Username string 69 | // Password 70 | Password string 71 | // Use TLS for the connection 72 | TLS bool 73 | // Max timeout for logging out 74 | Timeout time.Duration 75 | // Mark all the retrieved messages as seen when retrieved 76 | MarkSeen bool 77 | } 78 | 79 | type Email struct { 80 | // Array of flags the message has 81 | Flags []string 82 | // Contains all the message headers 83 | Header mail.Header 84 | // Contains the message body 85 | Body []byte 86 | } 87 | 88 | // NewReader constructs a new Reader instance with the given Options 89 | func NewReader(opts Options) (Reader, error) { 90 | client, err := connect(opts) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | return &reader{ 96 | opts: opts, 97 | client: client, 98 | }, nil 99 | } 100 | 101 | // Login initiates the IMAP session 102 | func (r *reader) Login() error { 103 | cmd, err := r.client.Login(r.opts.Username, r.opts.Password) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | _, err = cmd.Result(imap.OK) 109 | return err 110 | } 111 | 112 | // Logout terminates the IMAP session 113 | func (r *reader) Logout() error { 114 | cmd, err := r.client.Logout(r.opts.Timeout) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | _, err = cmd.Result(imap.OK) 120 | return err 121 | } 122 | 123 | // List performs a search with the given params in the given mailbox and 124 | // returns the list of emails matching that criteria 125 | func (r *reader) List(mailbox string, params []imap.Field) ([]*Email, error) { 126 | if err := r.mailbox(mailbox, true); err != nil { 127 | return nil, err 128 | } 129 | 130 | set, err := r.search(params) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | emails, err := r.fetch(set) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | if err := r.closeMailbox(); err != nil { 141 | return nil, err 142 | } 143 | 144 | if r.opts.MarkSeen && len(emails) > 0 { 145 | if err := r.mailbox(mailbox, false); err != nil { 146 | return nil, err 147 | } 148 | 149 | if err := r.markSeen(set); err != nil { 150 | return nil, err 151 | } 152 | 153 | if err := r.closeMailbox(); err != nil { 154 | return nil, err 155 | } 156 | } 157 | 158 | return emails, nil 159 | } 160 | 161 | func (r *reader) mailbox(mailbox string, readOnly bool) error { 162 | _, _, err := r.exec(r.client.Select(mailbox, readOnly)) 163 | return err 164 | } 165 | 166 | func (r *reader) closeMailbox() error { 167 | _, _, err := r.exec(r.client.Close(false)) 168 | return err 169 | } 170 | 171 | func (r *reader) search(params []imap.Field) (*imap.SeqSet, error) { 172 | if len(params) > 1 { 173 | for i, p := range params[1:] { 174 | params[i+1] = r.client.Quote(p) 175 | } 176 | } 177 | 178 | cmd, _, err := r.exec(r.client.UIDSearch(params...)) 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | set, _ := imap.NewSeqSet("") 184 | results := cmd.Data[0].SearchResults() 185 | if len(results) == 0 { 186 | return nil, nil 187 | } 188 | 189 | set.AddNum(results...) 190 | return set, nil 191 | } 192 | 193 | func (r *reader) fetch(set *imap.SeqSet) ([]*Email, error) { 194 | if set == nil { 195 | return nil, nil 196 | } 197 | 198 | cmd, _, err := r.exec(r.client.UIDFetch(set, "FLAGS", "BODY[]")) 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | emails, err := r.emailsFromResponse(cmd.Data) 204 | if err != nil { 205 | return nil, err 206 | } 207 | 208 | return emails, nil 209 | } 210 | 211 | func (r *reader) markSeen(set *imap.SeqSet) error { 212 | _, _, err := r.exec(r.client.UIDStore(set, "+FLAGS.SILENT", imap.NewFlagSet(`\Seen`))) 213 | return err 214 | } 215 | 216 | func (r *reader) emailsFromResponse(data []*imap.Response) ([]*Email, error) { 217 | var emails []*Email 218 | 219 | for _, d := range data { 220 | email, err := newEmail(d) 221 | if err != nil { 222 | return nil, err 223 | } 224 | emails = append(emails, email) 225 | } 226 | 227 | return emails, nil 228 | } 229 | 230 | func (r *reader) exec(cmd *imap.Command, err error) (*imap.Command, *imap.Response, error) { 231 | if err != nil { 232 | return nil, nil, err 233 | } 234 | 235 | resp, err := cmd.Result(imap.OK) 236 | if err != nil { 237 | return nil, nil, err 238 | } 239 | 240 | return cmd, resp, nil 241 | } 242 | 243 | func newEmail(resp *imap.Response) (*Email, error) { 244 | var buf bytes.Buffer 245 | info := resp.MessageInfo() 246 | if _, err := info.Attrs["BODY[]"].(imap.Literal).WriteTo(&buf); err != nil { 247 | return nil, err 248 | } 249 | 250 | msg, err := mail.ReadMessage(&buf) 251 | if err != nil { 252 | return nil, err 253 | } 254 | 255 | var flags []string 256 | for k := range info.Flags { 257 | flags = append(flags, k) 258 | } 259 | 260 | body, err := ioutil.ReadAll(msg.Body) 261 | if err != nil { 262 | return nil, err 263 | } 264 | 265 | return &Email{ 266 | Header: msg.Header, 267 | Body: body, 268 | Flags: flags, 269 | }, nil 270 | } 271 | 272 | func connect(opts Options) (*imap.Client, error) { 273 | if opts.TLS { 274 | return imap.DialTLS(opts.Addr, nil) 275 | } else { 276 | return imap.Dial(opts.Addr) 277 | } 278 | } 279 | 280 | // BySubject returns the search paramters to perform a search by subject 281 | func BySubject(subject string) []imap.Field { 282 | return []imap.Field{"SUBJECT", subject} 283 | } 284 | 285 | // ByFrom returns the search parameters to perform a search by FROM address 286 | func ByFrom(from string) []imap.Field { 287 | return []imap.Field{"FROM", from} 288 | } 289 | 290 | // TODO: Implement more search filters 291 | -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | package imapreader 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/mxk/go-imap/imap" 11 | 12 | . "gopkg.in/check.v1" 13 | ) 14 | 15 | type ReaderSuite struct { 16 | } 17 | 18 | func Test(t *testing.T) { TestingT(t) } 19 | 20 | var _ = Suite(&ReaderSuite{}) 21 | 22 | const Msg = ` 23 | Subject: Fancy ponies 24 | From: Fancy pony 25 | 26 | Hello, ponies 27 | ` 28 | 29 | func (s *ReaderSuite) setupFixtures(c *C, markSeen bool) (*reader, string) { 30 | opts := Options{ 31 | Addr: os.Getenv("TEST_ADDR"), 32 | Username: os.Getenv("TEST_USER"), 33 | Password: os.Getenv("TEST_PWD"), 34 | TLS: true, 35 | Timeout: 60 * time.Second, 36 | MarkSeen: markSeen, 37 | } 38 | 39 | _r, err := NewReader(opts) 40 | c.Assert(err, IsNil) 41 | r := _r.(*reader) 42 | c.Assert(r.Login(), IsNil) 43 | 44 | mailbox := fmt.Sprintf("MBOX_%d", time.Now().UnixNano()) 45 | if cmd, err := imap.Wait(r.client.Create(mailbox)); err != nil { 46 | if rsp, ok := err.(imap.ResponseError); ok && rsp.Status == imap.NO { 47 | _, _, err := r.exec(r.client.Delete(mailbox)) 48 | c.Assert(err, IsNil) 49 | } 50 | } else { 51 | _, err := cmd.Result(imap.OK) 52 | c.Assert(err, IsNil) 53 | } 54 | 55 | msg := []byte(strings.Replace(Msg[1:], "\n", "\r\n", -1)) 56 | _, _, err = r.exec(r.client.Append(mailbox, nil, nil, imap.NewLiteral(msg))) 57 | c.Assert(err, IsNil) 58 | 59 | return r, mailbox 60 | } 61 | 62 | func (s *ReaderSuite) teardownFixtures(c *C, r *reader, mailbox string) { 63 | _, _, err := r.exec(r.client.Delete(mailbox)) 64 | c.Assert(err, IsNil) 65 | c.Assert(r.Logout(), IsNil) 66 | } 67 | 68 | func (s *ReaderSuite) TestListNoMarkSeen(c *C) { 69 | r, mailbox := s.setupFixtures(c, false) 70 | 71 | msgs, err := r.List(mailbox, SearchUnseen) 72 | c.Assert(err, IsNil) 73 | c.Assert(msgs, HasLen, 1) 74 | 75 | msg := msgs[0] 76 | c.Assert(string(msg.Body), Equals, "Hello, ponies\r\n") 77 | c.Assert(msg.Header.Get("Subject"), Equals, "Fancy ponies") 78 | c.Assert(msg.Header.Get("From"), Equals, "Fancy pony ") 79 | c.Assert(msg.Flags, HasLen, 0) 80 | 81 | s.teardownFixtures(c, r, mailbox) 82 | } 83 | 84 | func (s *ReaderSuite) TestListMarkSeen(c *C) { 85 | r, mailbox := s.setupFixtures(c, true) 86 | 87 | msgs, err := r.List(mailbox, SearchUnseen) 88 | c.Assert(err, IsNil) 89 | c.Assert(msgs, HasLen, 1) 90 | 91 | msg := msgs[0] 92 | c.Assert(string(msg.Body), Equals, "Hello, ponies\r\n") 93 | c.Assert(msg.Header.Get("Subject"), Equals, "Fancy ponies") 94 | c.Assert(msg.Header.Get("From"), Equals, "Fancy pony ") 95 | c.Assert(msg.Flags, HasLen, 0) 96 | 97 | msgs, err = r.List(mailbox, SearchUnseen) 98 | c.Assert(err, IsNil) 99 | c.Assert(msgs, HasLen, 0) 100 | 101 | s.teardownFixtures(c, r, mailbox) 102 | } 103 | --------------------------------------------------------------------------------