mirror of
https://github.com/axllent/mailpit.git
synced 2025-06-12 23:57:40 +02:00
Testing: Add POP3 integration tests
This commit is contained in:
parent
1bd6794b2d
commit
f7f200c6fe
448
internal/pop3client/client.go
Normal file
448
internal/pop3client/client.go
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
// Package pop3client is borrowed directly from https://github.com/knadh/go-pop3 to reduce dependencies.
|
||||||
|
// This is used solely for testing the POP3 server
|
||||||
|
package pop3client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/mail"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialer interface
|
||||||
|
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 <info>`
|
||||||
|
respErr = []byte("-ERR") // `-ERR` without additional info
|
||||||
|
respErrInfo = []byte("-ERR ") // `-ERR <info>`
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// Courtesy: 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 *mail.Message.
|
||||||
|
func (c *Conn) Retr(msgID int) (*mail.Message, error) {
|
||||||
|
b, err := c.Cmd("RETR", true, msgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := mail.ReadMessage(b)
|
||||||
|
if err != nil {
|
||||||
|
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) (*mail.Message, error) {
|
||||||
|
b, err := c.Cmd("TOP", true, msgID, numLines)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := mail.ReadMessage(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 executed by the server on a graceful
|
||||||
|
// quit and close.
|
||||||
|
func (c *Conn) Quit() error {
|
||||||
|
defer c.conn.Close()
|
||||||
|
|
||||||
|
if _, err := c.Cmd("QUIT", false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown response: %s. Neither -ERR, nor +OK", string(b))
|
||||||
|
}
|
365
server/pop3/pop3_test.go
Normal file
365
server/pop3/pop3_test.go
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
package pop3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/axllent/mailpit/config"
|
||||||
|
"github.com/axllent/mailpit/internal/auth"
|
||||||
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
|
"github.com/axllent/mailpit/internal/pop3client"
|
||||||
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
|
"github.com/jhillyerd/enmime"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testingPort int
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPOP3(t *testing.T) {
|
||||||
|
t.Log("Testing POP3 server")
|
||||||
|
setup()
|
||||||
|
defer storage.Close()
|
||||||
|
|
||||||
|
// connect with bad password
|
||||||
|
t.Log("Testing invalid login")
|
||||||
|
c, err := connectBadAuth()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("invalid login gained access")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Testing valid login")
|
||||||
|
c, err = connectAuth()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count, size, err := c.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(t, count, 0, "incorrect message count")
|
||||||
|
assertEqual(t, size, 0, "incorrect size")
|
||||||
|
|
||||||
|
// quit else we get old data
|
||||||
|
if err := c.Quit(); err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Inserting 50 messages")
|
||||||
|
|
||||||
|
insertEmailData(t) // insert 50 messages
|
||||||
|
|
||||||
|
c, err = connectAuth()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count, size, err = c.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(t, count, 50, "incorrect message count")
|
||||||
|
|
||||||
|
t.Log("Fetching 20 messages")
|
||||||
|
|
||||||
|
for i := 1; i <= 20; i++ {
|
||||||
|
_, err := c.Retr(i)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Deleting 25 messages")
|
||||||
|
|
||||||
|
for i := 1; i <= 25; i++ {
|
||||||
|
if err := c.Dele(i); err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// messages get deleted after a QUIT
|
||||||
|
if err := c.Quit(); err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = connectAuth()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Fetching message count")
|
||||||
|
|
||||||
|
count, _, err = c.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(t, count, 25, "incorrect message count")
|
||||||
|
|
||||||
|
// messages get deleted after a QUIT
|
||||||
|
if err := c.Quit(); err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = connectAuth()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Deleting 25 messages")
|
||||||
|
|
||||||
|
for i := 1; i <= 25; i++ {
|
||||||
|
if err := c.Dele(i); err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Undeleting messages")
|
||||||
|
|
||||||
|
if err := c.Rset(); err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Quit(); err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = connectAuth()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count, _, err = c.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(t, count, 25, "incorrect message count")
|
||||||
|
|
||||||
|
if err := c.Quit(); err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthentication(t *testing.T) {
|
||||||
|
// commands only allowed after authentication
|
||||||
|
authCommands := make(map[string]bool)
|
||||||
|
authCommands["STAT"] = false
|
||||||
|
authCommands["LIST"] = true
|
||||||
|
authCommands["NOOP"] = false
|
||||||
|
authCommands["RSET"] = false
|
||||||
|
authCommands["RETR 1"] = true
|
||||||
|
|
||||||
|
t.Log("Testing authenticated commands while not logged in")
|
||||||
|
setup()
|
||||||
|
defer storage.Close()
|
||||||
|
|
||||||
|
insertEmailData(t) // insert 50 messages
|
||||||
|
|
||||||
|
// non-authenticated connection
|
||||||
|
c, err := connect()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for cmd, multi := range authCommands {
|
||||||
|
if _, err := c.Cmd(cmd, multi); err == nil {
|
||||||
|
t.Errorf("%s should require authentication", cmd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := c.Cmd(strings.ToLower(cmd), multi); err == nil {
|
||||||
|
t.Errorf("%s should require authentication", cmd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Quit(); err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Testing authenticated commands while logged in")
|
||||||
|
|
||||||
|
// authenticated connection
|
||||||
|
c, err = connectAuth()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for cmd, multi := range authCommands {
|
||||||
|
if _, err := c.Cmd(cmd, multi); err != nil {
|
||||||
|
t.Errorf("%s should work when authenticated", cmd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := c.Cmd(strings.ToLower(cmd), multi); err != nil {
|
||||||
|
t.Errorf("%s should work when authenticated", cmd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Quit(); err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
auth.SetPOP3Auth("username:password")
|
||||||
|
logger.NoLogging = true
|
||||||
|
config.MaxMessages = 0
|
||||||
|
config.Database = os.Getenv("MP_DATABASE")
|
||||||
|
var foundPort bool
|
||||||
|
for !foundPort {
|
||||||
|
testingPort = randRange(1111, 2000)
|
||||||
|
if portFree(testingPort) {
|
||||||
|
foundPort = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.POP3Listen = fmt.Sprintf("localhost:%d", testingPort)
|
||||||
|
|
||||||
|
if err := storage.InitDB(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := storage.DeleteAllMessages(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go Run()
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect and authenticate
|
||||||
|
func connectAuth() (*pop3client.Conn, error) {
|
||||||
|
c, err := connect()
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.Auth("username", "password")
|
||||||
|
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect and authenticate
|
||||||
|
func connectBadAuth() (*pop3client.Conn, error) {
|
||||||
|
c, err := connect()
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.Auth("username", "notPassword")
|
||||||
|
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect but do not authenticate
|
||||||
|
func connect() (*pop3client.Conn, error) {
|
||||||
|
p := pop3client.New(pop3client.Opt{
|
||||||
|
Host: "localhost",
|
||||||
|
Port: testingPort,
|
||||||
|
TLSEnabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
c, err := p.NewConn()
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func portFree(port int) bool {
|
||||||
|
ln, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ln.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func randRange(min, max int) int {
|
||||||
|
return rand.IntN(max-min) + min
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertEmailData(t *testing.T) {
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
msg := enmime.Builder().
|
||||||
|
From(fmt.Sprintf("From %d", i), fmt.Sprintf("from-%d@example.com", i)).
|
||||||
|
Subject(fmt.Sprintf("Subject line %d end", i)).
|
||||||
|
Text([]byte(fmt.Sprintf("This is the email body %d <jdsauk;dwqmdqw;>.", i))).
|
||||||
|
To(fmt.Sprintf("To %d", i), fmt.Sprintf("to-%d@example.com", i))
|
||||||
|
|
||||||
|
env, err := msg.Build()
|
||||||
|
if err != nil {
|
||||||
|
t.Log("error ", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
if err := env.Encode(buf); err != nil {
|
||||||
|
t.Log("error ", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
bufBytes := buf.Bytes()
|
||||||
|
|
||||||
|
id, err := storage.Store(&bufBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Log("error ", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := storage.SetMessageTags(id, []string{fmt.Sprintf("Test tag %03d", i)}); err != nil {
|
||||||
|
t.Log("error ", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
||||||
|
if a == b {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message = fmt.Sprintf("%s: \"%v\" != \"%v\"", message, a, b)
|
||||||
|
t.Fatal(message)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user