-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
548 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,57 @@ | ||
# pop3 | ||
POP3 Client written in Golang - https://www.ietf.org/rfc/rfc1939.txt | ||
# POP3 Client | ||
|
||
POP3 Client written in Golang in accordance to [RFC1939](https://www.ietf.org/rfc/rfc1939.txt). | ||
|
||
## Usage | ||
|
||
### Initialize client | ||
```go | ||
// Create a connection to the server | ||
c, err := pop3.DialTLS("REPLACE_THIS_SERVER_ADDRESS:993") | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
defer c.Quit() | ||
|
||
// Authenticate with the server | ||
if err = c.Authorization("REPLACE_THIS_USERNAME", "REPLACE_THIS_PASSWORD"); err != nil { | ||
log.Fatal(err) | ||
} | ||
``` | ||
|
||
### Commands | ||
|
||
```go | ||
// Check if there are any messages to retrieved. | ||
count, _, err := pc.Stat() | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
message, err := pc.Retr(1) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
log.Println(message.Text) | ||
|
||
if err := pc.Dele(1); err != nil { | ||
log.Fatal(err) | ||
} | ||
``` | ||
## Testing | ||
|
||
To run tests, run following command: | ||
```bash | ||
go test -race ./... | ||
``` | ||
|
||
## Contributions & Issues | ||
Contributions are welcome. Please clearly explain the purpose of the PR and follow the current style. | ||
|
||
Issues can be resolved quickest if they are descriptive and include both a reduced test case, and a set of steps to reproduce. | ||
|
||
## Licence | ||
The `genert/pop3` library is copyrighted © by [Genert Org](http://genert.org) and licensed for use under the MIT License (MIT). | ||
|
||
Please see [MIT License](LICENSE) for more information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package pop3 | ||
|
||
import "strings" | ||
|
||
// POP3 replies as extracted from rfc1939 section 9. | ||
const ( | ||
OK = "+OK" | ||
ERR = "-ERR" | ||
) | ||
|
||
// IsOK checks to see if the reply from the server contains +OK. | ||
func IsOK(s string) bool { | ||
if strings.Fields(s)[0] != OK { | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
// IsErr checks to see if the reply from the server contains +Err. | ||
func IsErr(s string) bool { | ||
if strings.Fields(s)[0] != ERR { | ||
return false | ||
} | ||
return true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package pop3_test | ||
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/genert/pop3" | ||
) | ||
|
||
func TestIsOK(t *testing.T) { | ||
testCases := []struct { | ||
message string | ||
expectedResult bool | ||
}{ | ||
{"+OK", true}, | ||
{"+OK 2 messages", true}, | ||
{"-ERR", false}, | ||
{"-ERR no such message", false}, | ||
} | ||
|
||
for _, tt := range testCases { | ||
t.Run(fmt.Sprintf(`IsOK should return %s for "%s" message`, strconv.FormatBool(tt.expectedResult), tt.message), func(t *testing.T) { | ||
t.Parallel() | ||
assert.Equal(t, tt.expectedResult, pop3.IsOK(tt.message)) | ||
}) | ||
} | ||
} | ||
|
||
func TestIsErr(t *testing.T) { | ||
testCases := []struct { | ||
message string | ||
expectedResult bool | ||
}{ | ||
{"+OK", false}, | ||
{"+OK 2 messages", false}, | ||
{"-ERR", true}, | ||
{"-ERR no such message", true}, | ||
} | ||
|
||
for _, tt := range testCases { | ||
t.Run(fmt.Sprintf(`IsErr should return %s for "%s" message`, strconv.FormatBool(tt.expectedResult), tt.message), func(t *testing.T) { | ||
t.Parallel() | ||
assert.Equal(t, tt.expectedResult, pop3.IsErr(tt.message)) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
package pop3 | ||
|
||
import ( | ||
"crypto/tls" | ||
"fmt" | ||
"net" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/jhillyerd/enmime" | ||
) | ||
|
||
// MessageList represents the metadata returned by the server for a | ||
// message stored in the maildrop. | ||
type MessageList struct { | ||
// Non unique id reported by the server | ||
ID int | ||
|
||
// Size of the message | ||
Size int | ||
} | ||
|
||
const ( | ||
CommandReset = "RSET" | ||
|
||
// CommandStat is a command to retrieve statistics about mailbox. | ||
CommandStat = "STAT" | ||
|
||
// CommandDelete is a command to delete message from POP3 server. | ||
CommandDelete = "DELE" | ||
|
||
// CommandList is a command to get list of messages from POP3 server. | ||
CommandList = "LIST" | ||
|
||
// CommandNoop is a ping-like command that tells POP3 to do nothing. | ||
// (i.e. send something line pong-response). | ||
CommandNoop = "NOOP" | ||
|
||
// CommandPassword is a command to send user password to POP3 server. | ||
CommandPassword = "PASS" | ||
|
||
// CommandQuit is a command to tell POP3 server that you are quitting. | ||
CommandQuit = "QUIT" | ||
|
||
// CommandRetrieve is a command to retrieve POP3 message from server. | ||
CommandRetrieve = "RETR" | ||
|
||
// CommandUser is a command to send user login to POP3 server. | ||
CommandUser = "USER" | ||
) | ||
|
||
// Client for POP3. | ||
type Client struct { | ||
conn *Connection | ||
} | ||
|
||
// Dial opens new connection and creates a new POP3 client. | ||
func Dial(addr string) (c *Client, err error) { | ||
var conn net.Conn | ||
if conn, err = net.Dial("tcp", addr); err != nil { | ||
return nil, fmt.Errorf("failed to dial: %w", err) | ||
} | ||
|
||
return NewClient(conn) | ||
} | ||
|
||
// DialTLS opens new TLS connection and creates a new POP3 client. | ||
func DialTLS(addr string) (c *Client, err error) { | ||
var conn *tls.Conn | ||
if conn, err = tls.Dial("tcp", addr, nil); err != nil { | ||
return nil, fmt.Errorf("failed to dial tls: %w", err) | ||
} | ||
return NewClient(conn) | ||
} | ||
|
||
// NewClient creates a new POP3 client. | ||
func NewClient(conn net.Conn) (*Client, error) { | ||
c := &Client{ | ||
conn: NewConnection(conn), | ||
} | ||
|
||
// Make sure we receive the server greeting | ||
line, err := c.conn.ReadLine() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to read line: %w", err) | ||
} | ||
|
||
if !IsOK(line) { | ||
return nil, fmt.Errorf("server did not response with +OK: %s", line) | ||
} | ||
|
||
return c, nil | ||
} | ||
|
||
// Authorization logs into POP3 server with login and password. | ||
func (c *Client) Authorization(user, pass string) error { | ||
if _, err := c.conn.Cmd("%s %s", CommandUser, user); err != nil { | ||
return fmt.Errorf("failed at USER command: %w", err) | ||
} | ||
|
||
if _, err := c.conn.Cmd("%s %s", CommandPassword, pass); err != nil { | ||
return fmt.Errorf("failed at PASS command: %w", err) | ||
} | ||
|
||
return c.Noop() | ||
} | ||
|
||
// Quit sends the QUIT message to the POP3 server and closes the connection. | ||
func (c *Client) Quit() error { | ||
if _, err := c.conn.Cmd(CommandQuit); err != nil { | ||
return fmt.Errorf("failed at QUIT command: %w", err) | ||
} | ||
|
||
if err := c.conn.Close(); err != nil { | ||
return fmt.Errorf("failed to close connection: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Noop will do nothing however can prolong the end of a connection. | ||
func (c *Client) Noop() error { | ||
if _, err := c.conn.Cmd(CommandNoop); err != nil { | ||
return fmt.Errorf("failed at NOOP command: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Stat retrieves a drop listing for the current maildrop, consisting of the | ||
// number of messages and the total size (in octets) of the maildrop. | ||
// In the event of an error, all returned numeric values will be 0. | ||
func (c *Client) Stat() (count, size int, err error) { | ||
line, err := c.conn.Cmd(CommandStat) | ||
if err != nil { | ||
return | ||
} | ||
|
||
if len(strings.Fields(line)) != 3 { | ||
return 0, 0, fmt.Errorf("invalid response returned from server: %s", line) | ||
} | ||
|
||
// Number of messages in maildrop | ||
count, err = strconv.Atoi(strings.Fields(line)[1]) | ||
if err != nil { | ||
return | ||
} | ||
if count == 0 { | ||
return | ||
} | ||
|
||
// Total size of messages in bytes | ||
size, err = strconv.Atoi(strings.Fields(line)[2]) | ||
if err != nil { | ||
return | ||
} | ||
if size == 0 { | ||
return | ||
} | ||
return | ||
} | ||
|
||
// ListAll returns a MessageList object which contains all messages in the maildrop. | ||
func (c *Client) ListAll() (list []MessageList, err error) { | ||
if _, err = c.conn.Cmd(CommandList); err != nil { | ||
return | ||
} | ||
|
||
lines, err := c.conn.ReadLines() | ||
if err != nil { | ||
return | ||
} | ||
|
||
for _, v := range lines { | ||
id, err := strconv.Atoi(strings.Fields(v)[0]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
size, err := strconv.Atoi(strings.Fields(v)[1]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
list = append(list, MessageList{id, size}) | ||
} | ||
return | ||
} | ||
|
||
// Rset will unmark any messages that have being marked for deletion in | ||
// the current session. | ||
func (c *Client) Rset() error { | ||
if _, err := c.conn.Cmd(CommandReset); err != nil { | ||
return fmt.Errorf("failed at RSET command: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
// Retr downloads the given message and returns it as a mail.Message object. | ||
func (c *Client) Retr(msg int) (*enmime.Envelope, error) { | ||
if _, err := c.conn.Cmd("%s 1", CommandRetrieve); err != nil { | ||
return nil, fmt.Errorf("failed at RETR command: %w", err) | ||
} | ||
|
||
message, err := enmime.ReadEnvelope(c.conn.Reader.DotReader()) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to read message: %w", err) | ||
} | ||
|
||
return message, nil | ||
} | ||
|
||
// Dele will delete the given message from the maildrop. | ||
// Changes will only take affect after the Quit command is issued. | ||
func (c *Client) Dele(msg int) error { | ||
if _, err := c.conn.Cmd("%s %d", CommandDelete, msg); err != nil { | ||
return fmt.Errorf("failed at DELE command: %w", err) | ||
} | ||
return nil | ||
} |
Oops, something went wrong.