081a041108
* i.e. don't produce integer overflows...
236 lines
4.6 KiB
Go
236 lines
4.6 KiB
Go
package imap
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/mail"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
tag = "<<unreadMail>> "
|
|
other = "* "
|
|
maxRead = 11000000
|
|
timeout = 3000 // ms
|
|
taggedOK = tag + "OK"
|
|
otherOK = other + "OK"
|
|
search = other + "SEARCH"
|
|
)
|
|
|
|
type Error string
|
|
|
|
func (err Error) Error() string {
|
|
return string(err)
|
|
}
|
|
|
|
type connection struct {
|
|
*tls.Conn
|
|
user string
|
|
passwd string
|
|
unreadMails []*mail.Message
|
|
err []error
|
|
}
|
|
|
|
type stateFunc func(*connection) stateFunc
|
|
|
|
func expect(response string, tok string) (string, bool) {
|
|
lines := strings.Split(response, "\n")
|
|
for _, l := range lines {
|
|
line := strings.TrimSpace(l)
|
|
if strings.HasPrefix(line, tok) {
|
|
return strings.TrimSpace(strings.TrimPrefix(line, tok)), true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func parseMail(response string) (*mail.Message, error) {
|
|
reader := bufio.NewReader(strings.NewReader(response))
|
|
for {
|
|
line, _ := reader.ReadBytes('\n')
|
|
if !strings.HasPrefix(string(line), other) {
|
|
break
|
|
}
|
|
}
|
|
return mail.ReadMessage(reader)
|
|
}
|
|
|
|
/*-------------- states ----------*/
|
|
|
|
func connected(c *connection) stateFunc {
|
|
_ = c.read("server hello")
|
|
if c.err != nil {
|
|
return nil
|
|
} else {
|
|
return login
|
|
}
|
|
}
|
|
|
|
func login(c *connection) stateFunc {
|
|
c.write(cmd(fmt.Sprintf("login %s %s", c.user, c.passwd)))
|
|
if c.err != nil {
|
|
return logout
|
|
}
|
|
_ = c.read("login response")
|
|
if c.err != nil {
|
|
return logout
|
|
} else {
|
|
return selectBox
|
|
}
|
|
}
|
|
|
|
func selectBox(c *connection) stateFunc {
|
|
c.write(cmd("select Inbox"))
|
|
if c.err != nil {
|
|
return logout
|
|
}
|
|
_ = c.read("select response")
|
|
if c.err != nil {
|
|
return logout
|
|
} else {
|
|
return searchUnseen
|
|
}
|
|
}
|
|
|
|
func searchUnseen(c *connection) stateFunc {
|
|
c.write(cmd("uid search unseen"))
|
|
if c.err != nil {
|
|
return logout
|
|
}
|
|
s := c.read("search response")
|
|
if c.err != nil {
|
|
return logout
|
|
} else {
|
|
if r, ok := expect(s, search); ok {
|
|
uids := UIDsFromResponse(r)
|
|
return func(c *connection) stateFunc { return fetch(c, uids) }
|
|
} else {
|
|
return logout
|
|
}
|
|
}
|
|
}
|
|
|
|
func fetch(c *connection, ids []int) stateFunc {
|
|
nErrs := 0
|
|
for _, id := range ids {
|
|
c.write(cmd(fmt.Sprintf("uid fetch %d body[]", id)))
|
|
if len(c.err) == nErrs {
|
|
s := c.read("uid fetch of " + strconv.Itoa(id))
|
|
if len(c.err) == nErrs {
|
|
s = strings.TrimSuffix(strings.TrimSpace(s), ")")
|
|
mail, err := parseMail(s)
|
|
if err == nil {
|
|
c.unreadMails = append(c.unreadMails, mail)
|
|
} else {
|
|
c.err = append(c.err, err)
|
|
}
|
|
}
|
|
}
|
|
nErrs = len(c.err)
|
|
}
|
|
return logout
|
|
}
|
|
|
|
func logout(c *connection) stateFunc {
|
|
c.write(cmd("logout"))
|
|
c.read("logout response")
|
|
c.Close()
|
|
return nil
|
|
}
|
|
|
|
/*---------- End of states ----------*/
|
|
|
|
func UIDsFromResponse(response string) []int {
|
|
ids := make([]int, 0)
|
|
if response == "" {
|
|
return ids
|
|
}
|
|
list := strings.Split(response, " ")
|
|
for _, id := range list {
|
|
val, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
log.Println("Could not convert uid '" + id + "'")
|
|
} else {
|
|
ids = append(ids, val)
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func cmd(s string) string {
|
|
return tag + s + "\r\n"
|
|
}
|
|
|
|
func (c *connection) write(s string) {
|
|
c.Conn.SetWriteDeadline(time.Now().Add(timeout * time.Millisecond))
|
|
bytes := []byte(s)
|
|
n, err := c.Conn.Write(bytes)
|
|
var desc string
|
|
if strings.Contains(s, "login") {
|
|
// would not want to print login credentials in case of error
|
|
desc = "login"
|
|
} else {
|
|
desc = s
|
|
}
|
|
if n != len(bytes) || err != nil {
|
|
c.err = append(c.err, Error("Could not write "+desc))
|
|
}
|
|
}
|
|
|
|
func (c *connection) read(desc string) string {
|
|
buf := make([]byte, 0)
|
|
reader := bufio.NewReader(io.LimitReader(c.Conn, maxRead))
|
|
var err error
|
|
var conclusion string
|
|
if desc == "server hello" {
|
|
conclusion = other
|
|
} else {
|
|
conclusion = tag
|
|
}
|
|
for {
|
|
var line []byte
|
|
c.SetReadDeadline(time.Now().Add(timeout * time.Millisecond))
|
|
line, tmpErr := reader.ReadBytes('\n')
|
|
if tmpErr == nil {
|
|
if l := string(line); strings.HasPrefix(l, conclusion) {
|
|
if strings.HasPrefix(l, conclusion+"OK") {
|
|
break
|
|
} else {
|
|
buf = nil
|
|
err = Error("Did not receive OK in " + desc + ", got: " + l)
|
|
break
|
|
}
|
|
}
|
|
buf = append(buf, line...)
|
|
} else {
|
|
errMsg := "Error while reading " + desc
|
|
if tmpErr != nil {
|
|
errMsg += "\n" + tmpErr.Error()
|
|
}
|
|
err = Error(errMsg)
|
|
break
|
|
}
|
|
}
|
|
s := string(buf)
|
|
if err != nil {
|
|
c.err = append(c.err, err)
|
|
return ""
|
|
} else {
|
|
return s
|
|
}
|
|
}
|
|
|
|
func Fetch(c *tls.Conn, user string, passwd string) ([]*mail.Message, []error) {
|
|
state := connected
|
|
conn := &connection{Conn: c, user: user, passwd: passwd}
|
|
for state != nil {
|
|
state = state(conn)
|
|
}
|
|
return conn.unreadMails, conn.err
|
|
}
|