package imap import ( "bufio" "crypto/tls" "fmt" "log" "net/mail" "strconv" "strings" "time" ) const ( tag = ". " 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 { for _, id := range ids { c.write(cmd(fmt.Sprintf("uid fetch %d body[]", id))) if c.err == nil { s := c.read("uid fetch of " + strconv.Itoa(id)) s = strings.TrimRight(s, ")\n") if c.err == nil { mail, err := parseMail(s) if err == nil { c.unreadMails = append(c.unreadMails, mail) } else { c.err = append(c.err, 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.Second)) 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(c.Conn) 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 && len(buf)+len(line) <= maxRead { 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 }