libUnreadMail/imap/imap.go
Alexander Weinhold 081a041108 be less retarded about limiting read input
* i.e. don't produce integer overflows...
2018-01-04 20:48:36 +01:00

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
}