improved pick: now its possible to specify 1..n header keys for searching
4 files changed, 140 insertions(+), 16 deletions(-)

M cmd/mb/main.go
M command.go
M list.go
M sequence.go
M cmd/mb/main.go +35 -4
@@ 14,6 14,32 @@ import (
 	"bitbucket.org/telesto/mailbox/mh"
 )
 
+type Command struct {
+	Name string
+	// Run runs the command.
+	// The args are the arguments after the command name.
+	Run func(mbox mb.Mailbox, args []string) error
+
+	// UsageLine is the one-line usage message.
+	UsageLine string
+
+	// Short is the short description shown in the 'mb help' output.
+	Short string
+
+	// Long is the long message shown in the 'mb help <command>' output.
+	Long string
+}
+
+const command = "mb"
+
+var commands = []*Command{
+	cmdList,
+}
+
+func init() {
+	cmdList.Run = runList
+}
+
 var missingParams = errors.New("missing parameters")
 
 func die(msg string) {

          
@@ 273,9 299,10 @@ func main() {
 		if err = src.Close(); err != nil {
 			break
 		}
-		folder, msgs, err1 := parseSequence(mbox, os.Args[2])
-		if err1 != nil {
-			err = err1
+		var folder string
+		var msgs *mb.Sequence
+		folder, msgs, err = parseSequence(mbox, os.Args[2])
+		if err != nil {
 			break
 		}
 		err = mb.Move(mbox, folder, msgs, mbox.Sent())

          
@@ 308,13 335,17 @@ func main() {
 		// todo: do we need this?
 		err = errors.New(cmd + ": not implemented")
 	case "store":
+		// todo: document that a maildrop mail needs to
+		// ne piped through grep -v "^From " to remove the
+		// leading "From " line
 		if len(os.Args) < 3 {
 			err = missingParams
 			break
 		}
 		folder := os.Args[2]
 		var msgId string
-		if msgId, err = mb.Store(mbox, os.Stdin, folder); len(msgId) != 0 {
+		msgId, err = mbox.Append(folder, os.Stdin)
+		if len(msgId) > 0 {
 			os.Stdout.WriteString(path.Join(folder, msgId) + "\n")
 		}
 	case "pack": // rename to sort, as we gonna sort by date

          
M command.go +0 -8
@@ 50,14 50,6 @@ func Remove(mbox Mailbox, folder string,
 	return walk(mbox, folder, msgs, proc)
 }
 
-func Store(mb Mailbox, msg io.Reader, folder string) (string, error) {
-	msgid, err := mb.Append(folder, msg)
-	if err != nil {
-		return msgid, err
-	}
-	return msgid, err
-}
-
 func partError(no int, msg string) error {
 	return errors.New("mime part " + strconv.Itoa(no) + ": " + msg)
 }

          
M list.go +4 -0
@@ 12,6 12,7 @@ import (
 var sixMonthPast = time.Now().Add(-6 * 30 * 24 * time.Hour)
 var oneDayAhead = time.Now().Add(24 * time.Hour)
 
+// todo: delim as param
 func List(mbox Mailbox, dst io.Writer, folder string, pick *Sequence) error {
 	msgs, err := mbox.ReadFolder(folder)
 	if err != nil && msgs == nil {

          
@@ 45,6 46,9 @@ func List(mbox Mailbox, dst io.Writer, f
 			dec.safeDecodeHeader(header.Get("Subject")))
 		out.WriteByte('\n')
 	}
+	if err1 := out.Flush(); err == nil {
+		err = err1
+	}
 	return err
 }
 

          
M sequence.go +101 -4
@@ 2,9 2,11 @@ package mailbox
 
 import (
 	"errors"
+	"io"
 	rgxp "regexp"
 	"strconv"
 	"strings"
+	"unicode/utf8"
 )
 
 type Sequence struct {

          
@@ 61,22 63,117 @@ func NewAlwaysMatcher() *Sequence {
 }
 
 type Regexp struct {
-	r *rgxp.Regexp
+	matchHeader func(string) bool
+	matchBody   bool
+	r           *rgxp.Regexp
 }
 
 func (s Regexp) Matches(m Message) bool {
+	dec := newDecoder()
 	for key := range m.Header() {
-		if s.r.MatchString(key + ": " + m.Header().Get(key)) {
+		if s.matchHeader(key) && s.r.MatchString(dec.safeDecodeHeader(m.Header().Get(key))) {
 			return true
 		}
 	}
+	if !s.matchBody {
+		return false
+	}
 	return false
 }
 
 func ParseRegexp(s string) (*Regexp, error) {
-	r, err := rgxp.Compile(s)
+	ss := strings.Split(s, ":")
+	if len(ss) > 2 {
+		return nil, errors.New("invalid parameter")
+	}
+	var keys string
+	regexp := ss[0]
+	if len(ss) == 2 {
+		keys = ss[0]
+		regexp = ss[1]
+	}
+	r, err := rgxp.Compile(regexp)
 	if err != nil {
 		return nil, err
 	}
-	return &Regexp{r}, nil
+	if len(keys) == 0 {
+		// match everything
+		return &Regexp{func(string) bool {
+			return true
+		}, true, r}, nil
+	}
+	m := make(map[string]struct{})
+	var matchBody bool
+	for _, key := range strings.Split(keys, ",") {
+		m[key] = struct{}{}
+		if key == "body" {
+			matchBody = true
+		}
+	}
+	// todo: don't use closure here
+	return &Regexp{func(key string) bool {
+		_, ok := m[key]
+		return ok
+	}, matchBody, r}, nil
+}
+
+// readRune is a structure to enable reading UTF-8 encoded code points
+// from an io.Reader.
+type readRune struct {
+	reader  io.Reader
+	buf     [utf8.UTFMax]byte // used only inside ReadRune
+	pending int               // number of bytes in pendBuf; only >0 for bad UTF-8
+	pendBuf [utf8.UTFMax]byte // bytes left over
 }
+
+// readByte returns the next byte from the input, which may be
+// left over from a previous read if the UTF-8 was ill-formed.
+func (r *readRune) readByte() (b byte, err error) {
+	if r.pending > 0 {
+		b = r.pendBuf[0]
+		copy(r.pendBuf[0:], r.pendBuf[1:])
+		r.pending--
+		return
+	}
+	n, err := io.ReadFull(r.reader, r.pendBuf[0:1])
+	if n != 1 {
+		return 0, err
+	}
+	return r.pendBuf[0], err
+}
+
+// unread saves the bytes for the next read.
+func (r *readRune) unread(buf []byte) {
+	copy(r.pendBuf[r.pending:], buf)
+	r.pending += len(buf)
+}
+
+// ReadRune returns the next UTF-8 encoded code point from the
+// io.Reader inside r.
+func (r *readRune) ReadRune() (rr rune, size int, err error) {
+	r.buf[0], err = r.readByte()
+	if err != nil {
+		return 0, 0, err
+	}
+	if r.buf[0] < utf8.RuneSelf { // fast check for common ASCII case
+		rr = rune(r.buf[0])
+		size = 1 // Known to be 1.
+		return
+	}
+	var n int
+	for n = 1; !utf8.FullRune(r.buf[0:n]); n++ {
+		r.buf[n], err = r.readByte()
+		if err != nil {
+			if err == io.EOF {
+				err = nil
+				break
+			}
+			return
+		}
+	}
+	rr, size = utf8.DecodeRune(r.buf[0:n])
+	if size < n { // an error
+		r.unread(r.buf[size:n])
+	}
+	return
+}