Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
genert committed Mar 16, 2021
1 parent eb4720e commit a3a573a
Show file tree
Hide file tree
Showing 10 changed files with 548 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@

# Dependency directories (remove the comment below to include it)
# vendor/

# IDE files
*.swp
.idea
.vscode
pop3.iml
59 changes: 57 additions & 2 deletions README.md
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.
25 changes: 25 additions & 0 deletions assertion.go
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
}
49 changes: 49 additions & 0 deletions assertion_test.go
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))
})
}
}
219 changes: 219 additions & 0 deletions client.go
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
}
Loading

0 comments on commit a3a573a

Please sign in to comment.