├── .gitignore ├── LICENSE ├── pop3.go └── pop3_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Scott Lawrence 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /pop3.go: -------------------------------------------------------------------------------- 1 | // Package pop3 provides an implementation of the Post Office Protocol, Version 2 | // 3 as defined in RFC 1939. Commands specified as optional are not 3 | // implemented; however, this implementation may be trivially extended to 4 | // support them. 5 | 6 | package pop3 7 | 8 | import ( 9 | "bufio" 10 | "crypto/tls" 11 | "errors" 12 | "fmt" 13 | "net" 14 | "strconv" 15 | "strings" 16 | ) 17 | 18 | // The POP3 client. 19 | type Client struct { 20 | conn net.Conn 21 | bin *bufio.Reader 22 | } 23 | 24 | // Dial creates an unsecured connection to the POP3 server at the given address 25 | // and returns the corresponding Client. 26 | func Dial(addr string) (*Client, error) { 27 | conn, err := net.Dial("tcp", addr) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return NewClient(conn) 32 | } 33 | 34 | // DialTLS creates a TLS-secured connection to the POP3 server at the given 35 | // address and returns the corresponding Client. 36 | func DialTLS(addr string) (*Client, error) { 37 | conn, err := tls.Dial("tcp", addr, nil) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return NewClient(conn) 42 | } 43 | 44 | // NewClient returns a new Client object using an existing connection. 45 | func NewClient(conn net.Conn) (*Client, error) { 46 | client := &Client{ 47 | bin: bufio.NewReader(conn), 48 | conn: conn, 49 | } 50 | // send dud command, to read a line 51 | _, err := client.Cmd("") 52 | if err != nil { 53 | return nil, err 54 | } 55 | return client, nil 56 | } 57 | 58 | // Convenience function to synchronously run an arbitrary command and wait for 59 | // output. The terminating CRLF must be included in the format string. 60 | // 61 | // Output sent after the first line must be retrieved via readLines. 62 | func (c *Client) Cmd(format string, args ...interface{}) (string, error) { 63 | fmt.Fprintf(c.conn, format, args...) 64 | line, _, err := c.bin.ReadLine() 65 | if err != nil { return "", err } 66 | l := string(line) 67 | if l[0:3] != "+OK" { 68 | err = errors.New(l[5:]) 69 | } 70 | if len(l) >= 4 { 71 | return l[4:], err 72 | } 73 | return "", err 74 | } 75 | 76 | func (c *Client) ReadLines() (lines []string, err error) { 77 | lines = make([]string, 0) 78 | l, _, err := c.bin.ReadLine() 79 | line := string(l) 80 | for err == nil && line != "." { 81 | if len(line) > 0 && line[0] == '.' { 82 | line = line[1:] 83 | } 84 | lines = append(lines, line) 85 | l, _, err = c.bin.ReadLine() 86 | line = string(l) 87 | } 88 | return 89 | } 90 | 91 | // User sends the given username to the server. Generally, there is no reason 92 | // not to use the Auth convenience method. 93 | func (c *Client) User(username string) (err error) { 94 | _, err = c.Cmd("USER %s\r\n", username) 95 | return 96 | } 97 | 98 | // Pass sends the given password to the server. The password is sent 99 | // unencrypted unless the connection is already secured by TLS (via DialTLS or 100 | // some other mechanism). Generally, there is no reason not to use the Auth 101 | // convenience method. 102 | func (c *Client) Pass(password string) (err error) { 103 | _, err = c.Cmd("PASS %s\r\n", password) 104 | return 105 | } 106 | 107 | // Auth sends the given username and password to the server, calling the User 108 | // and Pass methods as appropriate. 109 | func (c *Client) Auth(username, password string) (err error) { 110 | err = c.User(username) 111 | if err != nil { 112 | return 113 | } 114 | err = c.Pass(password) 115 | return 116 | } 117 | 118 | // Stat retrieves a drop listing for the current maildrop, consisting of the 119 | // number of messages and the total size (in octets) of the maildrop. 120 | // Information provided besides the number of messages and the size of the 121 | // maildrop is ignored. In the event of an error, all returned numeric values 122 | // will be 0. 123 | func (c *Client) Stat() (count, size int, err error) { 124 | l, err := c.Cmd("STAT\r\n") 125 | if err != nil { 126 | return 0, 0, err 127 | } 128 | parts := strings.Fields(l) 129 | count, err = strconv.Atoi(parts[0]) 130 | if err != nil { 131 | return 0, 0, errors.New("Invalid server response") 132 | } 133 | size, err = strconv.Atoi(parts[1]) 134 | if err != nil { 135 | return 0, 0, errors.New("Invalid server response") 136 | } 137 | return 138 | } 139 | 140 | // List returns the size of the given message, if it exists. If the message 141 | // does not exist, or another error is encountered, the returned size will be 142 | // 0. 143 | func (c *Client) List(msg int) (size int, err error) { 144 | l, err := c.Cmd("LIST %d\r\n", msg) 145 | if err != nil { 146 | return 0, err 147 | } 148 | size, err = strconv.Atoi(strings.Fields(l)[1]) 149 | if err != nil { 150 | return 0, errors.New("Invalid server response") 151 | } 152 | return size, nil 153 | } 154 | 155 | // ListAll returns a list of all messages and their sizes. 156 | func (c *Client) ListAll() (msgs []int, sizes []int, err error) { 157 | _, err = c.Cmd("LIST\r\n") 158 | if err != nil { 159 | return 160 | } 161 | lines, err := c.ReadLines() 162 | if err != nil { 163 | return 164 | } 165 | msgs = make([]int, len(lines), len(lines)) 166 | sizes = make([]int, len(lines), len(lines)) 167 | for i, l := range lines { 168 | var m, s int 169 | fs := strings.Fields(l) 170 | m, err = strconv.Atoi(fs[0]) 171 | if err != nil { 172 | return 173 | } 174 | s, err = strconv.Atoi(fs[1]) 175 | if err != nil { 176 | return 177 | } 178 | msgs[i] = m 179 | sizes[i] = s 180 | } 181 | return 182 | } 183 | 184 | // Retr downloads and returns the given message. The lines are separated by LF, 185 | // whatever the server sent. 186 | func (c *Client) Retr(msg int) (text string, err error) { 187 | _, err = c.Cmd("RETR %d\r\n", msg) 188 | if err != nil { 189 | return "", err 190 | } 191 | lines, err := c.ReadLines() 192 | text = strings.Join(lines, "\n") 193 | return 194 | } 195 | 196 | // Dele marks the given message as deleted. 197 | func (c *Client) Dele(msg int) (err error) { 198 | _, err = c.Cmd("DELE %d\r\n", msg) 199 | return 200 | } 201 | 202 | // Noop does nothing, but will prolong the end of the connection if the server 203 | // has a timeout set. 204 | func (c *Client) Noop() (err error) { 205 | _, err = c.Cmd("NOOP\r\n") 206 | return 207 | } 208 | 209 | // Rset unmarks any messages marked for deletion previously in this session. 210 | func (c *Client) Rset() (err error) { 211 | _, err = c.Cmd("RSET\r\n") 212 | return 213 | } 214 | 215 | // Quit sends the QUIT message to the POP3 server and closes the connection. 216 | func (c *Client) Quit() error { 217 | _, err := c.Cmd("QUIT\r\n") 218 | if err != nil { 219 | return err 220 | } 221 | c.conn.Close() 222 | return nil 223 | } 224 | -------------------------------------------------------------------------------- /pop3_test.go: -------------------------------------------------------------------------------- 1 | package pop3 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "net" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | type fakeAddr struct {} 14 | func (fakeAddr) Network() string { return "" } 15 | func (fakeAddr) String() string { return "" } 16 | 17 | type faker struct { 18 | io.ReadWriter 19 | } 20 | 21 | func (f faker) Close() error { 22 | return nil 23 | } 24 | 25 | func (f faker) LocalAddr() net.Addr { 26 | return fakeAddr{} 27 | } 28 | 29 | func (f faker) RemoteAddr() net.Addr { 30 | return fakeAddr{} 31 | } 32 | 33 | func (f faker) SetDeadline(t time.Time) error { 34 | return nil 35 | } 36 | 37 | func (f faker) SetReadDeadline(t time.Time) error { 38 | return nil 39 | } 40 | 41 | func (f faker) SetWriteDeadline(t time.Time) error { 42 | return nil 43 | } 44 | 45 | func TestBasic (t *testing.T) { 46 | basicServer := strings.Join(strings.Split(basicServer, "\n"), "\r\n") 47 | basicClient := strings.Join(strings.Split(basicClient, "\n"), "\r\n") 48 | 49 | var cmdbuf bytes.Buffer 50 | bcmdbuf := bufio.NewWriter(&cmdbuf) 51 | var fake faker 52 | fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(basicServer)), bcmdbuf) 53 | 54 | c, err := NewClient(fake) 55 | if err != nil { 56 | t.Fatalf("NewClient failed: %s", err) 57 | } 58 | 59 | if err = c.User("uname"); err != nil { 60 | t.Fatal("User failed: %s", err) 61 | } 62 | 63 | if err = c.Pass("password1"); err == nil { 64 | t.Fatal("Pass succeeded inappropriately") 65 | } 66 | 67 | if err = c.Auth("uname", "password2"); err != nil { 68 | t.Fatal("Auth failed: %s", err) 69 | } 70 | 71 | if err = c.Noop(); err != nil { 72 | t.Fatal("Noop failed: %s", err) 73 | } 74 | 75 | bcmdbuf.Flush() 76 | if basicClient != cmdbuf.String() { 77 | t.Fatalf("Got:\n%s\nExpected:\n%s", cmdbuf.String(), basicClient) 78 | } 79 | } 80 | 81 | var basicServer = `+OK good morning 82 | +OK send PASS 83 | -ERR [AUTH] mismatched username and password 84 | +OK send PASS 85 | +OK welcome 86 | +OK 87 | ` 88 | 89 | var basicClient = `USER uname 90 | PASS password1 91 | USER uname 92 | PASS password2 93 | NOOP 94 | ` 95 | --------------------------------------------------------------------------------