pax_global_header00006660000000000000000000000064145627222560014525gustar00rootroot0000000000000052 comment=620e5d62741f70e919dade27dbcbdf85e8385b88 go-pop3-1.0.0/000077500000000000000000000000001456272225600130075ustar00rootroot00000000000000go-pop3-1.0.0/.github/000077500000000000000000000000001456272225600143475ustar00rootroot00000000000000go-pop3-1.0.0/.github/workflows/000077500000000000000000000000001456272225600164045ustar00rootroot00000000000000go-pop3-1.0.0/.github/workflows/default.yaml000066400000000000000000000004661456272225600207220ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest services: mail-server: image: inbucket/inbucket ports: - 9000:9000 - 2500:2500 - 1100:1100 steps: - uses: actions/checkout@v1 - name: testing run: go test .go-pop3-1.0.0/LICENSE000066400000000000000000000020701456272225600140130ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2021, Kailash Nadh 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-pop3-1.0.0/README.md000066400000000000000000000034331456272225600142710ustar00rootroot00000000000000# go-pop3 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. ## Install `go get -u github.com/knadh/go-pop3` ## Example ```go import ( "fmt" "github.com/knadh/go-pop3" ) func main() { // Initialize the client. p := pop3.New(pop3.Opt{ Host: "pop.gmail.com", Port: 995, TLSEnabled: true, }) // Create a new connection. POP3 connections are stateful and should end // with a Quit() once the opreations are done. c, err := p.NewConn() if err != nil { log.Fatal(err) } defer c.Quit() // Authenticate. if err := c.Auth("myuser", "mypassword"); err != nil { log.Fatal(err) } // Print the total number of messages and their size. count, size, _ := c.Stat() fmt.Println("total messages=", count, "size=", size) // Pull the list of all message IDs and their sizes. msgs, _ := c.List(0) for _, m := range msgs { fmt.Println("id=", m.ID, "size=", m.Size) } // Pull all messages on the server. Message IDs go from 1 to N. for id := 1; id <= count; id++ { m, _ := c.Retr(id) fmt.Println(id, "=", m.Header.Get("subject")) // To read the multi-part e-mail bodies, see: // https://github.com/emersion/go-message/blob/master/example_test.go#L12 } // Delete all the messages. Server only executes deletions after a successful Quit() for id := 1; id <= count; id++ { c.Dele(id) } } ``` [![PkgGoDev](https://pkg.go.dev/badge/github.com/knadh/go-pop3)](https://pkg.go.dev/github.com/knadh/go-pop3) ### To-do: tests 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. Licensed under the MIT License. go-pop3-1.0.0/go.mod000066400000000000000000000001311456272225600141100ustar00rootroot00000000000000module github.com/knadh/go-pop3 go 1.16 require github.com/emersion/go-message v0.15.0 go-pop3-1.0.0/go.sum000066400000000000000000000011451456272225600141430ustar00rootroot00000000000000github.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-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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= go-pop3-1.0.0/pop3.go000066400000000000000000000237331456272225600142270ustar00rootroot00000000000000// Package pop3 is a simple POP3 e-mail client library. package pop3 import ( "bufio" "bytes" "crypto/tls" "errors" "fmt" "net" "strconv" "strings" "time" "github.com/emersion/go-message" ) // Client implements a Client e-mail client. type Client struct { opt Opt dialer Dialer } // Conn is a stateful connection with the POP3 server/ type Conn struct { conn net.Conn r *bufio.Reader w *bufio.Writer } // Opt represents the client configuration. type Opt struct { Host string `json:"host"` Port int `json:"port"` // Default is 3 seconds. DialTimeout time.Duration `json:"dial_timeout"` Dialer Dialer `json:"-"` TLSEnabled bool `json:"tls_enabled"` TLSSkipVerify bool `json:"tls_skip_verify"` } type Dialer interface { Dial(network, address string) (net.Conn, error) } // MessageID contains the ID and size of an individual message. type MessageID struct { // ID is the numerical index (non-unique) of the message. ID int Size int // UID is only present if the response is to the UIDL command. UID string } var ( lineBreak = []byte("\r\n") respOK = []byte("+OK") // `+OK` without additional info respOKInfo = []byte("+OK ") // `+OK ` respErr = []byte("-ERR") // `-ERR` without additional info respErrInfo = []byte("-ERR ") // `-ERR ` ) // New returns a new client object using an existing connection. func New(opt Opt) *Client { if opt.DialTimeout < time.Millisecond { opt.DialTimeout = time.Second * 3 } c := &Client{ opt: opt, dialer: opt.Dialer, } if c.dialer == nil { c.dialer = &net.Dialer{Timeout: opt.DialTimeout} } return c } // NewConn creates and returns live POP3 server connection. func (c *Client) NewConn() (*Conn, error) { var ( addr = fmt.Sprintf("%s:%d", c.opt.Host, c.opt.Port) ) conn, err := c.dialer.Dial("tcp", addr) if err != nil { return nil, err } // No TLS. if c.opt.TLSEnabled { // Skip TLS host verification. tlsCfg := tls.Config{} if c.opt.TLSSkipVerify { tlsCfg.InsecureSkipVerify = c.opt.TLSSkipVerify } else { tlsCfg.ServerName = c.opt.Host } conn = tls.Client(conn, &tlsCfg) } pCon := &Conn{ conn: conn, r: bufio.NewReader(conn), w: bufio.NewWriter(conn), } // Verify the connection by reading the welcome +OK greeting. if _, err := pCon.ReadOne(); err != nil { return nil, err } return pCon, nil } // Send sends a POP3 command to the server. The given comand is suffixed with "\r\n". func (c *Conn) Send(b string) error { if _, err := c.w.WriteString(b + "\r\n"); err != nil { return err } return c.w.Flush() } // Cmd sends a command to the server. POP3 responses are either single line or multi-line. // The first line always with -ERR in case of an error or +OK in case of a successful operation. // OK+ is always followed by a response on the same line which is either the actual response data // in case of single line responses, or a help message followed by multiple lines of actual response // data in case of multiline responses. // See https://www.shellhacks.com/retrieve-email-pop3-server-command-line/ for examples. func (c *Conn) Cmd(cmd string, isMulti bool, args ...interface{}) (*bytes.Buffer, error) { var cmdLine string // Repeat a %v to format each arg. if len(args) > 0 { format := " " + strings.TrimRight(strings.Repeat("%v ", len(args)), " ") // CMD arg1 argn ...\r\n cmdLine = fmt.Sprintf(cmd+format, args...) } else { cmdLine = cmd } if err := c.Send(cmdLine); err != nil { return nil, err } // Read the first line of response to get the +OK/-ERR status. b, err := c.ReadOne() if err != nil { return nil, err } // Single line response. if !isMulti { return bytes.NewBuffer(b), err } buf, err := c.ReadAll() return buf, err } // ReadOne reads a single line response from the conn. func (c *Conn) ReadOne() ([]byte, error) { b, _, err := c.r.ReadLine() if err != nil { return nil, err } r, err := parseResp(b) return r, err } // ReadAll reads all lines from the connection until the POP3 multiline terminator "." is encountered // and returns a bytes.Buffer of all the read lines. func (c *Conn) ReadAll() (*bytes.Buffer, error) { buf := &bytes.Buffer{} for { b, _, err := c.r.ReadLine() if err != nil { return nil, err } // "." indicates the end of a multi-line response. if bytes.Equal(b, []byte(".")) { break } if _, err := buf.Write(b); err != nil { return nil, err } if _, err := buf.Write(lineBreak); err != nil { return nil, err } } return buf, nil } // Auth authenticates the given credentials with the server. func (c *Conn) Auth(user, password string) error { if err := c.User(user); err != nil { return err } if err := c.Pass(password); err != nil { return err } // Issue a NOOP to force the server to respond to the auth. // Couresy: github.com/TheCreeper/go-pop3 return c.Noop() } // User sends the username to the server. func (c *Conn) User(s string) error { _, err := c.Cmd("USER", false, s) return err } // Pass sends the password to the server. func (c *Conn) Pass(s string) error { _, err := c.Cmd("PASS", false, s) return err } // Stat returns the number of messages and their total size in bytes in the inbox. func (c *Conn) Stat() (int, int, error) { b, err := c.Cmd("STAT", false) if err != nil { return 0, 0, err } // count size f := bytes.Fields(b.Bytes()) // Total number of messages. count, err := strconv.Atoi(string(f[0])) if err != nil { return 0, 0, err } if count == 0 { return 0, 0, nil } // Total size of all messages in bytes. size, err := strconv.Atoi(string(f[1])) if err != nil { return 0, 0, err } return count, size, nil } // List returns a list of (message ID, message Size) pairs. // If the optional msgID > 0, then only that particular message is listed. // The message IDs are sequential, 1 to N. func (c *Conn) List(msgID int) ([]MessageID, error) { var ( buf *bytes.Buffer err error ) if msgID <= 0 { // Multiline response listing all messages. buf, err = c.Cmd("LIST", true) } else { // Single line response listing one message. buf, err = c.Cmd("LIST", false, msgID) } if err != nil { return nil, err } var ( out []MessageID lines = bytes.Split(buf.Bytes(), lineBreak) ) for _, l := range lines { // id size f := bytes.Fields(l) if len(f) == 0 { break } id, err := strconv.Atoi(string(f[0])) if err != nil { return nil, err } size, err := strconv.Atoi(string(f[1])) if err != nil { return nil, err } out = append(out, MessageID{ID: id, Size: size}) } return out, nil } // Uidl returns a list of (message ID, message UID) pairs. If the optional msgID // is > 0, then only that particular message is listed. It works like Top() but only works on // servers that support the UIDL command. Messages size field is not available in the UIDL response. func (c *Conn) Uidl(msgID int) ([]MessageID, error) { var ( buf *bytes.Buffer err error ) if msgID <= 0 { // Multiline response listing all messages. buf, err = c.Cmd("UIDL", true) } else { // Single line response listing one message. buf, err = c.Cmd("UIDL", false, msgID) } if err != nil { return nil, err } var ( out []MessageID lines = bytes.Split(buf.Bytes(), lineBreak) ) for _, l := range lines { // id size f := bytes.Fields(l) if len(f) == 0 { break } id, err := strconv.Atoi(string(f[0])) if err != nil { return nil, err } out = append(out, MessageID{ID: id, UID: string(f[1])}) } return out, nil } // Retr downloads a message by the given msgID, parses it and returns it as a // emersion/go-message.message.Entity object. func (c *Conn) Retr(msgID int) (*message.Entity, error) { b, err := c.Cmd("RETR", true, msgID) if err != nil { return nil, err } m, err := message.Read(b) if err != nil { if !message.IsUnknownCharset(err) { return nil, err } } return m, nil } // RetrRaw downloads a message by the given msgID and returns the raw []byte // of the entire message. func (c *Conn) RetrRaw(msgID int) (*bytes.Buffer, error) { b, err := c.Cmd("RETR", true, msgID) return b, err } // Top retrieves a message by its ID with full headers and numLines lines of the body. func (c *Conn) Top(msgID int, numLines int) (*message.Entity, error) { b, err := c.Cmd("TOP", true, msgID, numLines) if err != nil { return nil, err } m, err := message.Read(b) if err != nil { return nil, err } return m, nil } // Dele deletes one or more messages. The server only executes the // deletions after a successful Quit(). func (c *Conn) Dele(msgID ...int) error { for _, id := range msgID { _, err := c.Cmd("DELE", false, id) if err != nil { return err } } return nil } // Rset clears the messages marked for deletion in the current session. func (c *Conn) Rset() error { _, err := c.Cmd("RSET", false) return err } // Noop issues a do-nothing NOOP command to the server. This is useful for // prolonging open connections. func (c *Conn) Noop() error { _, err := c.Cmd("NOOP", false) return err } // Quit sends the QUIT command to server and gracefully closes the connection. // Message deletions (DELE command) are only excuted by the server on a graceful // quit and close. func (c *Conn) Quit() error { if _, err := c.Cmd("QUIT", false); err != nil { return err } return c.conn.Close() } // parseResp checks if the response is an error that starts with `-ERR` // and returns an error with the message that succeeds the error indicator. // For success `+OK` messages, it returns the remaining response bytes. func parseResp(b []byte) ([]byte, error) { if len(b) == 0 { return nil, nil } if bytes.Equal(b, respOK) { return nil, nil } else if bytes.HasPrefix(b, respOKInfo) { return bytes.TrimPrefix(b, respOKInfo), nil } else if bytes.Equal(b, respErr) { return nil, errors.New("unknown error (no info specified in response)") } else if bytes.HasPrefix(b, respErrInfo) { return nil, errors.New(string(bytes.TrimPrefix(b, respErrInfo))) } else { return nil, fmt.Errorf("unknown response: %s. Neither -ERR, nor +OK", string(b)) } } go-pop3-1.0.0/pop3_test.go000066400000000000000000000113061456272225600152570ustar00rootroot00000000000000package pop3 import ( "bytes" "fmt" "io" "log" "net/smtp" "strings" "testing" "github.com/emersion/go-message" "github.com/emersion/go-message/mail" ) const MSG = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do.` // n represents number of messages to add to the testuser's inbox func add_messages(n int) error { to := []string{"recipient@example.net"} msgs := make([][]byte, 0) for i := 0; i < 5; i++ { to := "To: recipient@example.net\r\n" subject := fmt.Sprintf("Subject: Subject %d\r\n", i) mime := "MIME-version: 1.0;\nContent-Type: text/plain; charset=\"UTF-8\";\n\n" body := fmt.Sprintf("Message %d.\r\n"+MSG+"\r\n", i) msg := []byte(to + subject + mime + body) msgs = append(msgs, msg) } for _, msg := range msgs { err := smtp.SendMail("localhost:2500", nil, "sender@example.org", to, msg) if err != nil { log.Fatal(err) } } return nil } func getConnection() (*Conn, error) { p := New(Opt{ Host: "localhost", Port: 1100, TLSEnabled: false, }) c, err := p.NewConn() if err != nil { return nil, err } return c, nil } func readAndCompareMessageBody(m *message.Entity, msg string) error { mr := mail.NewReader(m) if mr != nil { // This is a multipart message for { p, err := mr.NextPart() if err == io.EOF { break } else if err != nil { return err } b, err := io.ReadAll(p.Body) if err != nil { return err } if !strings.EqualFold(string(b), msg) { return fmt.Errorf("expected message body:\n%sreceived:\n%s", msg, string(b)) } } return nil } else { t, _, _ := m.Header.ContentType() log.Println("This is a non-multipart message with type", t) return nil } } func TestAll(t *testing.T) { c, err := getConnection() if err != nil { t.Fatal("error establishing connection to pop3 server ", err) } err = add_messages(5) if err != nil { t.Fatal("unable to send messages to the mail server", err) } // testing Auth if err := c.Auth("recipient", "password"); err != nil { t.Fatal(err) } // testing Stat count, size, err := c.Stat() if err != nil { t.Fatal("error using Stat", err) } log.Printf("count: %d, size: %d\n", count, size) // testing Uidl msgIds, err := c.Uidl(0) if err != nil { t.Fatal("error using Uidl(0)", err) } if len(msgIds) != count { t.Fatalf("Uidl returned: %d number of messages, but actually there are %d messages\n", len(msgIds), 5) } msgId, err := c.Uidl(msgIds[0].ID) if err != nil { t.Fatal("error using Uidl for positive message ID", err) } if len(msgId) != 1 { 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)) } // testing List msgs, err := c.List(0) if err != nil { t.Fatal("error using List(0)", err) } if len(msgs) != 5 { t.Fatalf("List(0) returned incorrect number of messages got: %d actual: %d\n", len(msgs), 5) } msgId, err = c.List(msgs[1].ID) if err != nil { t.Fatal("error using List for positive message ID", err) } if len(msgId) != 1 { 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)) } // testing Retr m, err := c.Retr(msgs[0].ID) if err != nil { t.Fatal("error using Retr", err) } if m.Header.Get("subject") != "Subject 0" { t.Fatalf("Retr returned wrong subject returned: %s, expected: Subject 0 ", m.Header.Get("subject")) } err = readAndCompareMessageBody(m, "Message 0.\r\n"+MSG+"\r\n") if err != nil { t.Fatal(err) } // testing RetrRaw mb, err := c.RetrRaw(msgs[0].ID) if err != nil { t.Fatal("error using RetrRaw", err) } b := mb.Bytes() if !bytes.Contains(b, []byte("Message 0.\r\n"+MSG+"\r\n")) { t.Fatalf("expected message body:\n%s, received:\n%s", "Message 0.\r\n"+MSG+"\r\n", string(b)) } // testing Top m, err = c.Top(msgs[0].ID, 1) if err != nil { t.Fatal("error using Top", err) } err = readAndCompareMessageBody(m, "Message 0.\r\n") if err != nil { t.Fatal(err) } // testing Noop err = c.Noop() if err != nil { t.Fatal("error in using Noop", err) } // testing Dele err = c.Dele([]int{1, 2}...) if err != nil { t.Fatal("error using Dele", err) } msgs, _ = c.List(0) if len(msgs) != 3 { t.Fatalf("after deleting 2 messages number of messages in inbox should be 3 but got %d", len(msgs)) } // testing Rset, list err = c.Rset() if err != nil { t.Fatal("error using Rset", err) } msgs, _ = c.List(0) if len(msgs) != 5 { t.Fatalf("after Rseting number of messages in inbox should be 5 but got %d", len(msgs)) } // testing Quit err = c.Quit() if err != nil { t.Fatal("error using Quit method", err) } }