├── .github └── workflows │ └── default.yaml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── pop3.go └── pop3_test.go /.github/workflows/default.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | services: 8 | mail-server: 9 | image: inbucket/inbucket 10 | ports: 11 | - 9000:9000 12 | - 2500:2500 13 | - 1100:1100 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: testing 17 | run: go test . -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021, Kailash Nadh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-pop3 2 | 3 | A simple Go POP3 client library for connecting and reading mails from POP3 servers. This is a full rewrite of [TheCreeper/go-pop3](https://github.com/TheCreeper/go-pop3) with bug fixes and new features. 4 | 5 | 6 | ## Install 7 | `go get -u github.com/knadh/go-pop3` 8 | 9 | 10 | ## Example 11 | ```go 12 | import ( 13 | "fmt" 14 | "github.com/knadh/go-pop3" 15 | ) 16 | 17 | func main() { 18 | // Initialize the client. 19 | p := pop3.New(pop3.Opt{ 20 | Host: "pop.gmail.com", 21 | Port: 995, 22 | TLSEnabled: true, 23 | }) 24 | 25 | // Create a new connection. POP3 connections are stateful and should end 26 | // with a Quit() once the opreations are done. 27 | c, err := p.NewConn() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | defer c.Quit() 32 | 33 | // Authenticate. 34 | if err := c.Auth("myuser", "mypassword"); err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | // Print the total number of messages and their size. 39 | count, size, _ := c.Stat() 40 | fmt.Println("total messages=", count, "size=", size) 41 | 42 | // Pull the list of all message IDs and their sizes. 43 | msgs, _ := c.List(0) 44 | for _, m := range msgs { 45 | fmt.Println("id=", m.ID, "size=", m.Size) 46 | } 47 | 48 | // Pull all messages on the server. Message IDs go from 1 to N. 49 | for id := 1; id <= count; id++ { 50 | m, _ := c.Retr(id) 51 | 52 | fmt.Println(id, "=", m.Header.Get("subject")) 53 | 54 | // To read the multi-part e-mail bodies, see: 55 | // https://github.com/emersion/go-message/blob/master/example_test.go#L12 56 | } 57 | 58 | // Delete all the messages. Server only executes deletions after a successful Quit() 59 | for id := 1; id <= count; id++ { 60 | c.Dele(id) 61 | } 62 | } 63 | ``` 64 | 65 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/knadh/go-pop3)](https://pkg.go.dev/github.com/knadh/go-pop3) 66 | 67 | 68 | ### To-do: tests 69 | Setup a Docker test environment that runs [InBucket](https://github.com/inbucket/inbucket) POP3 + SMTP server to run a dummy POP3 server and test all the commands in the lib. 70 | 71 | Licensed under the MIT License. 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/go-pop3 2 | 3 | go 1.16 4 | 5 | require github.com/emersion/go-message v0.15.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY= 2 | github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= 3 | github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= 4 | github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= 5 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 6 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 7 | -------------------------------------------------------------------------------- /pop3.go: -------------------------------------------------------------------------------- 1 | // Package pop3 is a simple POP3 e-mail client library. 2 | package pop3 3 | 4 | import ( 5 | "bufio" 6 | "bytes" 7 | "crypto/tls" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/emersion/go-message" 16 | ) 17 | 18 | // Client implements a Client e-mail client. 19 | type Client struct { 20 | opt Opt 21 | dialer Dialer 22 | } 23 | 24 | // Conn is a stateful connection with the POP3 server/ 25 | type Conn struct { 26 | conn net.Conn 27 | r *bufio.Reader 28 | w *bufio.Writer 29 | } 30 | 31 | // Opt represents the client configuration. 32 | type Opt struct { 33 | Host string `json:"host"` 34 | Port int `json:"port"` 35 | 36 | // Default is 3 seconds. 37 | DialTimeout time.Duration `json:"dial_timeout"` 38 | Dialer Dialer `json:"-"` 39 | 40 | TLSEnabled bool `json:"tls_enabled"` 41 | TLSSkipVerify bool `json:"tls_skip_verify"` 42 | } 43 | 44 | type Dialer interface { 45 | Dial(network, address string) (net.Conn, error) 46 | } 47 | 48 | // MessageID contains the ID and size of an individual message. 49 | type MessageID struct { 50 | // ID is the numerical index (non-unique) of the message. 51 | ID int 52 | Size int 53 | 54 | // UID is only present if the response is to the UIDL command. 55 | UID string 56 | } 57 | 58 | var ( 59 | lineBreak = []byte("\r\n") 60 | 61 | respOK = []byte("+OK") // `+OK` without additional info 62 | respOKInfo = []byte("+OK ") // `+OK ` 63 | respErr = []byte("-ERR") // `-ERR` without additional info 64 | respErrInfo = []byte("-ERR ") // `-ERR ` 65 | ) 66 | 67 | // New returns a new client object using an existing connection. 68 | func New(opt Opt) *Client { 69 | if opt.DialTimeout < time.Millisecond { 70 | opt.DialTimeout = time.Second * 3 71 | } 72 | 73 | c := &Client{ 74 | opt: opt, 75 | dialer: opt.Dialer, 76 | } 77 | 78 | if c.dialer == nil { 79 | c.dialer = &net.Dialer{Timeout: opt.DialTimeout} 80 | } 81 | 82 | return c 83 | } 84 | 85 | // NewConn creates and returns live POP3 server connection. 86 | func (c *Client) NewConn() (*Conn, error) { 87 | var ( 88 | addr = fmt.Sprintf("%s:%d", c.opt.Host, c.opt.Port) 89 | ) 90 | 91 | conn, err := c.dialer.Dial("tcp", addr) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | // No TLS. 97 | if c.opt.TLSEnabled { 98 | // Skip TLS host verification. 99 | tlsCfg := tls.Config{} 100 | if c.opt.TLSSkipVerify { 101 | tlsCfg.InsecureSkipVerify = c.opt.TLSSkipVerify 102 | } else { 103 | tlsCfg.ServerName = c.opt.Host 104 | } 105 | 106 | conn = tls.Client(conn, &tlsCfg) 107 | } 108 | 109 | pCon := &Conn{ 110 | conn: conn, 111 | r: bufio.NewReader(conn), 112 | w: bufio.NewWriter(conn), 113 | } 114 | 115 | // Verify the connection by reading the welcome +OK greeting. 116 | if _, err := pCon.ReadOne(); err != nil { 117 | return nil, err 118 | } 119 | 120 | return pCon, nil 121 | } 122 | 123 | // Send sends a POP3 command to the server. The given comand is suffixed with "\r\n". 124 | func (c *Conn) Send(b string) error { 125 | if _, err := c.w.WriteString(b + "\r\n"); err != nil { 126 | return err 127 | } 128 | return c.w.Flush() 129 | } 130 | 131 | // Cmd sends a command to the server. POP3 responses are either single line or multi-line. 132 | // The first line always with -ERR in case of an error or +OK in case of a successful operation. 133 | // OK+ is always followed by a response on the same line which is either the actual response data 134 | // in case of single line responses, or a help message followed by multiple lines of actual response 135 | // data in case of multiline responses. 136 | // See https://www.shellhacks.com/retrieve-email-pop3-server-command-line/ for examples. 137 | func (c *Conn) Cmd(cmd string, isMulti bool, args ...interface{}) (*bytes.Buffer, error) { 138 | var cmdLine string 139 | 140 | // Repeat a %v to format each arg. 141 | if len(args) > 0 { 142 | format := " " + strings.TrimRight(strings.Repeat("%v ", len(args)), " ") 143 | 144 | // CMD arg1 argn ...\r\n 145 | cmdLine = fmt.Sprintf(cmd+format, args...) 146 | } else { 147 | cmdLine = cmd 148 | } 149 | 150 | if err := c.Send(cmdLine); err != nil { 151 | return nil, err 152 | } 153 | 154 | // Read the first line of response to get the +OK/-ERR status. 155 | b, err := c.ReadOne() 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | // Single line response. 161 | if !isMulti { 162 | return bytes.NewBuffer(b), err 163 | } 164 | 165 | buf, err := c.ReadAll() 166 | return buf, err 167 | } 168 | 169 | // ReadOne reads a single line response from the conn. 170 | func (c *Conn) ReadOne() ([]byte, error) { 171 | b, _, err := c.r.ReadLine() 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | r, err := parseResp(b) 177 | return r, err 178 | } 179 | 180 | // ReadAll reads all lines from the connection until the POP3 multiline terminator "." is encountered 181 | // and returns a bytes.Buffer of all the read lines. 182 | func (c *Conn) ReadAll() (*bytes.Buffer, error) { 183 | buf := &bytes.Buffer{} 184 | 185 | for { 186 | b, _, err := c.r.ReadLine() 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | // "." indicates the end of a multi-line response. 192 | if bytes.Equal(b, []byte(".")) { 193 | break 194 | } 195 | 196 | if _, err := buf.Write(b); err != nil { 197 | return nil, err 198 | } 199 | if _, err := buf.Write(lineBreak); err != nil { 200 | return nil, err 201 | } 202 | } 203 | 204 | return buf, nil 205 | } 206 | 207 | // Auth authenticates the given credentials with the server. 208 | func (c *Conn) Auth(user, password string) error { 209 | if err := c.User(user); err != nil { 210 | return err 211 | } 212 | 213 | if err := c.Pass(password); err != nil { 214 | return err 215 | } 216 | 217 | // Issue a NOOP to force the server to respond to the auth. 218 | // Couresy: github.com/TheCreeper/go-pop3 219 | return c.Noop() 220 | } 221 | 222 | // User sends the username to the server. 223 | func (c *Conn) User(s string) error { 224 | _, err := c.Cmd("USER", false, s) 225 | return err 226 | } 227 | 228 | // Pass sends the password to the server. 229 | func (c *Conn) Pass(s string) error { 230 | _, err := c.Cmd("PASS", false, s) 231 | return err 232 | } 233 | 234 | // Stat returns the number of messages and their total size in bytes in the inbox. 235 | func (c *Conn) Stat() (int, int, error) { 236 | b, err := c.Cmd("STAT", false) 237 | if err != nil { 238 | return 0, 0, err 239 | } 240 | 241 | // count size 242 | f := bytes.Fields(b.Bytes()) 243 | 244 | // Total number of messages. 245 | count, err := strconv.Atoi(string(f[0])) 246 | if err != nil { 247 | return 0, 0, err 248 | } 249 | if count == 0 { 250 | return 0, 0, nil 251 | } 252 | 253 | // Total size of all messages in bytes. 254 | size, err := strconv.Atoi(string(f[1])) 255 | if err != nil { 256 | return 0, 0, err 257 | } 258 | 259 | return count, size, nil 260 | } 261 | 262 | // List returns a list of (message ID, message Size) pairs. 263 | // If the optional msgID > 0, then only that particular message is listed. 264 | // The message IDs are sequential, 1 to N. 265 | func (c *Conn) List(msgID int) ([]MessageID, error) { 266 | var ( 267 | buf *bytes.Buffer 268 | err error 269 | ) 270 | 271 | if msgID <= 0 { 272 | // Multiline response listing all messages. 273 | buf, err = c.Cmd("LIST", true) 274 | } else { 275 | // Single line response listing one message. 276 | buf, err = c.Cmd("LIST", false, msgID) 277 | } 278 | if err != nil { 279 | return nil, err 280 | } 281 | 282 | var ( 283 | out []MessageID 284 | lines = bytes.Split(buf.Bytes(), lineBreak) 285 | ) 286 | 287 | for _, l := range lines { 288 | // id size 289 | f := bytes.Fields(l) 290 | if len(f) == 0 { 291 | break 292 | } 293 | 294 | id, err := strconv.Atoi(string(f[0])) 295 | if err != nil { 296 | return nil, err 297 | } 298 | 299 | size, err := strconv.Atoi(string(f[1])) 300 | if err != nil { 301 | return nil, err 302 | } 303 | 304 | out = append(out, MessageID{ID: id, Size: size}) 305 | } 306 | 307 | return out, nil 308 | } 309 | 310 | // Uidl returns a list of (message ID, message UID) pairs. If the optional msgID 311 | // is > 0, then only that particular message is listed. It works like Top() but only works on 312 | // servers that support the UIDL command. Messages size field is not available in the UIDL response. 313 | func (c *Conn) Uidl(msgID int) ([]MessageID, error) { 314 | var ( 315 | buf *bytes.Buffer 316 | err error 317 | ) 318 | 319 | if msgID <= 0 { 320 | // Multiline response listing all messages. 321 | buf, err = c.Cmd("UIDL", true) 322 | } else { 323 | // Single line response listing one message. 324 | buf, err = c.Cmd("UIDL", false, msgID) 325 | } 326 | if err != nil { 327 | return nil, err 328 | } 329 | 330 | var ( 331 | out []MessageID 332 | lines = bytes.Split(buf.Bytes(), lineBreak) 333 | ) 334 | 335 | for _, l := range lines { 336 | // id size 337 | f := bytes.Fields(l) 338 | if len(f) == 0 { 339 | break 340 | } 341 | 342 | id, err := strconv.Atoi(string(f[0])) 343 | if err != nil { 344 | return nil, err 345 | } 346 | 347 | out = append(out, MessageID{ID: id, UID: string(f[1])}) 348 | } 349 | 350 | return out, nil 351 | } 352 | 353 | // Retr downloads a message by the given msgID, parses it and returns it as a 354 | // emersion/go-message.message.Entity object. 355 | func (c *Conn) Retr(msgID int) (*message.Entity, error) { 356 | b, err := c.Cmd("RETR", true, msgID) 357 | if err != nil { 358 | return nil, err 359 | } 360 | 361 | m, err := message.Read(b) 362 | if err != nil { 363 | if !message.IsUnknownCharset(err) { 364 | return nil, err 365 | } 366 | } 367 | 368 | return m, nil 369 | } 370 | 371 | // RetrRaw downloads a message by the given msgID and returns the raw []byte 372 | // of the entire message. 373 | func (c *Conn) RetrRaw(msgID int) (*bytes.Buffer, error) { 374 | b, err := c.Cmd("RETR", true, msgID) 375 | return b, err 376 | } 377 | 378 | // Top retrieves a message by its ID with full headers and numLines lines of the body. 379 | func (c *Conn) Top(msgID int, numLines int) (*message.Entity, error) { 380 | b, err := c.Cmd("TOP", true, msgID, numLines) 381 | if err != nil { 382 | return nil, err 383 | } 384 | 385 | m, err := message.Read(b) 386 | if err != nil { 387 | return nil, err 388 | } 389 | 390 | return m, nil 391 | } 392 | 393 | // Dele deletes one or more messages. The server only executes the 394 | // deletions after a successful Quit(). 395 | func (c *Conn) Dele(msgID ...int) error { 396 | for _, id := range msgID { 397 | _, err := c.Cmd("DELE", false, id) 398 | if err != nil { 399 | return err 400 | } 401 | } 402 | return nil 403 | } 404 | 405 | // Rset clears the messages marked for deletion in the current session. 406 | func (c *Conn) Rset() error { 407 | _, err := c.Cmd("RSET", false) 408 | return err 409 | } 410 | 411 | // Noop issues a do-nothing NOOP command to the server. This is useful for 412 | // prolonging open connections. 413 | func (c *Conn) Noop() error { 414 | _, err := c.Cmd("NOOP", false) 415 | return err 416 | } 417 | 418 | // Quit sends the QUIT command to server and gracefully closes the connection. 419 | // Message deletions (DELE command) are only excuted by the server on a graceful 420 | // quit and close. 421 | func (c *Conn) Quit() error { 422 | defer c.conn.Close() 423 | 424 | if _, err := c.Cmd("QUIT", false); err != nil { 425 | return err 426 | } 427 | 428 | return nil 429 | } 430 | 431 | // parseResp checks if the response is an error that starts with `-ERR` 432 | // and returns an error with the message that succeeds the error indicator. 433 | // For success `+OK` messages, it returns the remaining response bytes. 434 | func parseResp(b []byte) ([]byte, error) { 435 | if len(b) == 0 { 436 | return nil, nil 437 | } 438 | 439 | if bytes.Equal(b, respOK) { 440 | return nil, nil 441 | } else if bytes.HasPrefix(b, respOKInfo) { 442 | return bytes.TrimPrefix(b, respOKInfo), nil 443 | } else if bytes.Equal(b, respErr) { 444 | return nil, errors.New("unknown error (no info specified in response)") 445 | } else if bytes.HasPrefix(b, respErrInfo) { 446 | return nil, errors.New(string(bytes.TrimPrefix(b, respErrInfo))) 447 | } else { 448 | return nil, fmt.Errorf("unknown response: %s. Neither -ERR, nor +OK", string(b)) 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /pop3_test.go: -------------------------------------------------------------------------------- 1 | package pop3 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/smtp" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/emersion/go-message" 13 | "github.com/emersion/go-message/mail" 14 | ) 15 | 16 | const MSG = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do.` 17 | 18 | // n represents number of messages to add to the testuser's inbox 19 | func add_messages(n int) error { 20 | to := []string{"recipient@example.net"} 21 | msgs := make([][]byte, 0) 22 | for i := 0; i < 5; i++ { 23 | to := "To: recipient@example.net\r\n" 24 | subject := fmt.Sprintf("Subject: Subject %d\r\n", i) 25 | mime := "MIME-version: 1.0;\nContent-Type: text/plain; charset=\"UTF-8\";\n\n" 26 | body := fmt.Sprintf("Message %d.\r\n"+MSG+"\r\n", i) 27 | msg := []byte(to + subject + mime + body) 28 | msgs = append(msgs, msg) 29 | } 30 | 31 | for _, msg := range msgs { 32 | err := smtp.SendMail("localhost:2500", nil, "sender@example.org", to, msg) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func getConnection() (*Conn, error) { 41 | p := New(Opt{ 42 | Host: "localhost", 43 | Port: 1100, 44 | TLSEnabled: false, 45 | }) 46 | 47 | c, err := p.NewConn() 48 | if err != nil { 49 | return nil, err 50 | } 51 | return c, nil 52 | } 53 | 54 | func readAndCompareMessageBody(m *message.Entity, msg string) error { 55 | mr := mail.NewReader(m) 56 | if mr != nil { 57 | // This is a multipart message 58 | for { 59 | p, err := mr.NextPart() 60 | if err == io.EOF { 61 | break 62 | } else if err != nil { 63 | return err 64 | } 65 | b, err := io.ReadAll(p.Body) 66 | if err != nil { 67 | return err 68 | } 69 | if !strings.EqualFold(string(b), msg) { 70 | return fmt.Errorf("expected message body:\n%sreceived:\n%s", msg, string(b)) 71 | } 72 | } 73 | return nil 74 | } else { 75 | t, _, _ := m.Header.ContentType() 76 | log.Println("This is a non-multipart message with type", t) 77 | return nil 78 | } 79 | } 80 | 81 | func TestAll(t *testing.T) { 82 | 83 | c, err := getConnection() 84 | if err != nil { 85 | t.Fatal("error establishing connection to pop3 server ", err) 86 | } 87 | 88 | err = add_messages(5) 89 | if err != nil { 90 | t.Fatal("unable to send messages to the mail server", err) 91 | } 92 | 93 | // testing Auth 94 | if err := c.Auth("recipient", "password"); err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | // testing Stat 99 | count, size, err := c.Stat() 100 | if err != nil { 101 | t.Fatal("error using Stat", err) 102 | } 103 | log.Printf("count: %d, size: %d\n", count, size) 104 | 105 | // testing Uidl 106 | msgIds, err := c.Uidl(0) 107 | if err != nil { 108 | t.Fatal("error using Uidl(0)", err) 109 | } 110 | 111 | if len(msgIds) != count { 112 | t.Fatalf("Uidl returned: %d number of messages, but actually there are %d messages\n", len(msgIds), 5) 113 | } 114 | 115 | msgId, err := c.Uidl(msgIds[0].ID) 116 | if err != nil { 117 | t.Fatal("error using Uidl for positive message ID", err) 118 | } 119 | if len(msgId) != 1 { 120 | t.Fatalf("Uidl returns a list of (message ID, message UID) pairs. If the optional msgID is > 0, then only that particular message is listed but it returned %d pair\n", len(msgId)) 121 | } 122 | 123 | // testing List 124 | msgs, err := c.List(0) 125 | if err != nil { 126 | t.Fatal("error using List(0)", err) 127 | } 128 | if len(msgs) != 5 { 129 | t.Fatalf("List(0) returned incorrect number of messages got: %d actual: %d\n", len(msgs), 5) 130 | } 131 | msgId, err = c.List(msgs[1].ID) 132 | if err != nil { 133 | t.Fatal("error using List for positive message ID", err) 134 | } 135 | if len(msgId) != 1 { 136 | t.Fatalf("List returns a list of (message ID, message UID) pairs. If the optional msgID is > 0, then only that particular message is listed but it returned %d pair\n", len(msgId)) 137 | } 138 | 139 | // testing Retr 140 | m, err := c.Retr(msgs[0].ID) 141 | if err != nil { 142 | t.Fatal("error using Retr", err) 143 | } 144 | if m.Header.Get("subject") != "Subject 0" { 145 | t.Fatalf("Retr returned wrong subject returned: %s, expected: Subject 0 ", m.Header.Get("subject")) 146 | } 147 | err = readAndCompareMessageBody(m, "Message 0.\r\n"+MSG+"\r\n") 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | // testing RetrRaw 153 | mb, err := c.RetrRaw(msgs[0].ID) 154 | if err != nil { 155 | t.Fatal("error using RetrRaw", err) 156 | } 157 | b := mb.Bytes() 158 | if !bytes.Contains(b, []byte("Message 0.\r\n"+MSG+"\r\n")) { 159 | t.Fatalf("expected message body:\n%s, received:\n%s", "Message 0.\r\n"+MSG+"\r\n", string(b)) 160 | } 161 | 162 | // testing Top 163 | m, err = c.Top(msgs[0].ID, 1) 164 | if err != nil { 165 | t.Fatal("error using Top", err) 166 | } 167 | err = readAndCompareMessageBody(m, "Message 0.\r\n") 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | 172 | // testing Noop 173 | err = c.Noop() 174 | if err != nil { 175 | t.Fatal("error in using Noop", err) 176 | } 177 | 178 | // testing Dele 179 | err = c.Dele([]int{1, 2}...) 180 | if err != nil { 181 | t.Fatal("error using Dele", err) 182 | } 183 | msgs, _ = c.List(0) 184 | if len(msgs) != 3 { 185 | t.Fatalf("after deleting 2 messages number of messages in inbox should be 3 but got %d", len(msgs)) 186 | } 187 | // testing Rset, list 188 | err = c.Rset() 189 | if err != nil { 190 | t.Fatal("error using Rset", err) 191 | } 192 | msgs, _ = c.List(0) 193 | if len(msgs) != 5 { 194 | t.Fatalf("after Rseting number of messages in inbox should be 5 but got %d", len(msgs)) 195 | } 196 | 197 | // testing Quit 198 | err = c.Quit() 199 | if err != nil { 200 | t.Fatal("error using Quit method", err) 201 | } 202 | } 203 | --------------------------------------------------------------------------------