lots of things
10 files changed, 217 insertions(+), 217 deletions(-)

M cmd/mb/main.go
M command.go
M config.go
M decode.go
M edit.go
M list.go
M mh/mh.go
M sequence.go
M sequence_test.go
A => testdata/parts/1
M cmd/mb/main.go +35 -34
@@ 70,7 70,7 @@ func main() {
 		}
 	case "ls":
 		// todo: allow sequence
-		folder := mbox.Default()
+		folder := mbox.DefaultFolder
 		if len(os.Args) > 2 {
 			folder = os.Args[2]
 		}

          
@@ 106,6 106,7 @@ func main() {
 		}
 		err = src.Close()
 	case "part":
+		// todo: accept sequence
 		if len(os.Args) < 3 {
 			err = missingParams
 			break

          
@@ 170,6 171,7 @@ func main() {
 			err = err1
 		}
 	case "parts":
+		// bug: does not work anymore
 		// todo: read sequence from stdin
 		if len(os.Args) < 3 {
 			// todo: read stdin

          
@@ 178,8 180,7 @@ func main() {
 		}
 		var folder string
 		var msgs *mb.Sequence
-		folder, msgs, err = parseSequence(mbox, os.Args[2])
-		if err != nil {
+		if folder, msgs, err = parseSequence(mbox, os.Args[2]); err != nil {
 			break
 		}
 		var parts []string

          
@@ 192,29 193,29 @@ func main() {
 		}
 	case "grep":
 		// syntax: pick <folder> regexp
-//		if len(os.Args) < 3 {
-//			err = missingParams
-//			break
-//		}
-//		var folder string
-//		var match *mb.Regexp
-//		if folder, match, err = parseRegexpCmd(os.Args[2:]); err != nil {
-//			break
-//		}
-//		var msgs []mh.Message
-//		if msgs, errors = mbox.ReadFolder(folder); err != nil {
-//			break
-//		}
-//		var picked []string
-//		for i := range msgs {
-//			if match.Matches(msgs[i]) {
-//				picked = append(picked, strconv.FormatInt(msgs[i].Id, 10))
-//			}
-//		}
-//		if len(picked) > 0 {
-//			// main must not know how a filename is created TODO
-//			os.Stdout.WriteString(path.Join(folder, strings.Join(picked, ",")) + "\n")
-//		}
+		//		if len(os.Args) < 3 {
+		//			err = missingParams
+		//			break
+		//		}
+		//		var folder string
+		//		var match *mb.Regexp
+		//		if folder, match, err = parseRegexpCmd(os.Args[2:]); err != nil {
+		//			break
+		//		}
+		//		var msgs []mh.Message
+		//		if msgs, errors = mbox.ReadFolder(folder); err != nil {
+		//			break
+		//		}
+		//		var picked []string
+		//		for i := range msgs {
+		//			if match.Matches(msgs[i]) {
+		//				picked = append(picked, strconv.FormatInt(msgs[i].Id, 10))
+		//			}
+		//		}
+		//		if len(picked) > 0 {
+		//			// main must not know how a filename is created TODO
+		//			os.Stdout.WriteString(path.Join(folder, strings.Join(picked, ",")) + "\n")
+		//		}
 	case "new":
 		var name string
 		if name, err = mb.Create(mbox); len(name) > 0 {

          
@@ 238,7 239,7 @@ func main() {
 		}
 	case "detach":
 		// todo.
-		os.Stdout.WriteString("todo!\n")
+		os.Stdout.WriteString("todo\n")
 	case "reply": // is a reply-all
 		var src io.ReadCloser = os.Stdin
 		if len(os.Args) > 2 {

          
@@ 321,7 322,7 @@ func main() {
 		if err != nil {
 			break
 		}
-		err = mb.Move(mbox, folder, msgs, mbox.Sent())
+		err = mb.Move(mbox, folder, msgs, mbox.Sent)
 	case "mv":
 		// todo: defaultfolder
 		if len(os.Args) < 4 {

          
@@ 352,7 353,7 @@ func main() {
 		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
+		// be piped through grep -v "^From " to remove the
 		// leading "From " line
 		if len(os.Args) < 3 {
 			err = missingParams

          
@@ 379,13 380,13 @@ func main() {
 }
 
 func openMsg(mbox *mh.Mailbox, msg string) (io.ReadCloser, error) {
-	f, b := path.Split(msg)
+	f, m := path.Split(msg)
 	if len(f) == 0 {
-		f = mbox.Default()
+		f = mbox.DefaultFolder
 	}
-	id, err := strconv.ParseInt(b, 10, 64)
+	id, err := strconv.ParseInt(m, 10, 64)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("%s: invalid msg id", m)
 	}
 	return mbox.Open(f, id)
 }

          
@@ 395,7 396,7 @@ func openMsg(mbox *mh.Mailbox, msg strin
 func parseSequence(mbox *mh.Mailbox, value string) (string, *mb.Sequence, error) {
 	folder, seq := path.Split(value)
 	if len(folder) == 0 {
-		folder = mbox.Default()
+		folder = mbox.DefaultFolder
 	}
 	msgs, err := mb.ParseSequence(seq)
 	if err != nil {

          
M command.go +45 -26
@@ 22,29 22,42 @@ import (
 func walk(mbox *mh.Mailbox, folder string, seq *Sequence, proc func(int64) error) error {
 	// we need a sorted list, so the "Move" cmd will process the msgs
 	// in the correct time order
+
+	// todo: read dir only, not all messages!
+	// best would be:
+	// msgs, errors := mbox.ReadFolder(folder, seq)
+	// then let proc act on msg, like proc(*mail.Message) error
+	// todo: use this in List command!
 	msgs, errors := mbox.ReadFolder(folder)
 	var err error
-	select {
-	case msg, ok := <-msgs:
-		if !ok {
-			return err
-		}
-		if seq.IsMatch(msg.Id) {
-			if err1 := proc(msg.Id); err == nil {
+	for {
+		select {
+		case msg, ok := <-msgs:
+			if !ok {
+				return err
+			}
+			if seq.IsMatch(msg.Id) {
+				if err1 := proc(msg.Id); err == nil {
+					// parts: return this, so it is not written at the end
+					// of the list to stderr, like:
+					// inbox/17:1 text/plain; charset=utf-8
+					// inbox/18:1 text/plain; charset=utf-8
+					// inbox/19:1 text/plain; charset=UTF-8
+					// inbox/20:1 text/plain; charset="ISO-8859-1"
+					// mailbox: inbox/16: mime: no media type
+					err = fmt.Errorf("%s/%d: %v", folder, msg.Id, err1)
+				}
+			}
+		case err1 := <-errors:
+			if err == nil {
 				err = err1
 			}
 		}
-	case err1, ok := <-errors:
-		if !ok {
-			return err
-		}
-		if err == nil {
-			err = err1
-		}
 	}
 	return err
 }
 
+// todo: provide a set of msgs/parts by prematching all msgs of a folder
 func Move(mbox *mh.Mailbox, srcFolder string, msgs *Sequence, dst string) error {
 	proc := func(id int64) error {
 		return mbox.Move(srcFolder, dst, id)

          
@@ 52,6 65,7 @@ func Move(mbox *mh.Mailbox, srcFolder st
 	return walk(mbox, srcFolder, msgs, proc)
 }
 
+// todo: provide a set of msgs/parts by prematching all msgs of a folder
 func Remove(mbox *mh.Mailbox, folder string, msgs *Sequence) error {
 	proc := func(id int64) error {
 		return mbox.Remove(folder, id)

          
@@ 59,40 73,42 @@ func Remove(mbox *mh.Mailbox, folder str
 	return walk(mbox, folder, msgs, proc)
 }
 
-func partError(no int, msg string) error {
-	return errors.New("mime part " + strconv.Itoa(no) + ": " + msg)
-}
-
 // todo: implement matcher
+// todo: provide a set of msgs/parts by prematching all msgs of a folder, maybe as channel
+// todo: return saved filenames in a channel
+// todo: do we need to decode the filename?
 func Save(msg *mail.Message, number int) ([]string, error) {
 	var files []string
 	saver := func(p part, no int) error {
 		if no != number {
 			return nil
 		}
+		partError := func(err error) error {
+			return errors.New("mime part " + strconv.Itoa(no) + ": " + err.Error())
+		}
 		mp, ok := p.Body.(*multipart.Part)
 		if !ok {
-			return partError(no, "not an attached file")
+			return partError(errors.New("not an attached file"))
 		}
 		filename := mp.FileName()
 		if len(filename) == 0 {
-			return partError(no, "no filename")
+			return partError(errors.New("no filename"))
 		}
 		f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
 		if err != nil {
-			return partError(no, err.Error())
+			return partError(err)
 		}
 		defer f.Close()
 		in, err := mimeDecoder(p)
 		if err != nil {
-			return partError(no, err.Error())
+			return partError(err)
 		}
 		if _, err = io.Copy(f, in); err != nil {
-			return partError(no, err.Error())
+			return partError(err)
 		}
 		files = append(files, f.Name())
 		if err = f.Close(); err != nil {
-			return partError(no, err.Error())
+			return partError(err)
 		}
 		return nil
 	}

          
@@ 100,7 116,7 @@ func Save(msg *mail.Message, number int)
 }
 
 // todo: decode "name"
-
+// todo: provide a set of msgs/parts by prematching all msgs of a folder
 func Parts(mbox *mh.Mailbox, folder string, msgs *Sequence) ([]string, error) {
 	var parts []string
 	proc := func(id int64) error {

          
@@ 113,7 129,7 @@ func Parts(mbox *mh.Mailbox, folder stri
 			return err
 		}
 		partsLister := func(p part, no int) error {
-			parts = append(parts, fmt.Sprintf("%s/%d:%d  %s", folder, id, no, p.Header.Get("Content-Type")))
+			parts = append(parts, fmt.Sprintf("%s/%d:%d %s", folder, id, no, p.Header.Get("Content-Type")))
 			return nil
 		}
 		return parse(msg, partsLister)

          
@@ 121,6 137,7 @@ func Parts(mbox *mh.Mailbox, folder stri
 	return parts, walk(mbox, folder, msgs, proc)
 }
 
+// todo: provide a set of msgs/parts by prematching all msgs of a folder
 func ViewPart(dst io.Writer, msg *mail.Message, number int) error {
 	partPrinter := func(p part, no int) error {
 		if no != number {

          
@@ 141,6 158,7 @@ func anno(w io.Writer, p part, no int) e
 	return err
 }
 
+// we have a writer twice, dst and anno parameter
 func newPlainTextDecoder(dst io.Writer, t transform.Transformer, anno func(io.Writer, part, int) error) func(part, int) error {
 	return func(p part, no int) error {
 		// todo: if the msg has no content type header, this will return "mime: no media type"

          
@@ 167,6 185,7 @@ func newPlainTextDecoder(dst io.Writer, 
 
 // if the mail has only one header line without a nl at the end, "mailbox: EOF"
 // is thrown
+// todo: provide a set of msgs/parts by prematching all msgs of a folder
 func View(dst io.Writer, msg *mail.Message) error {
 	var b bytes.Buffer
 	for _, key := range viewHeader {

          
M config.go +4 -4
@@ 20,18 20,20 @@ var (
 	// editor is a program to edit mails.
 	//
 	// This editor has to be UTF-8 capable.
-	editor = "vim"
+	editor = "/usr/bin/nvi"
 
 	// viewHeader holds a list of mail header fields, as described in RFC 5322.
 	// These fields and their values will be shown when viewing a mail.
 	viewHeader = []string{"From", "To", "Cc", "Subject", "Date"}
 
+	// todo: use this below	listDelim = " | "
+
 	// formats holds the listing format for the various folder types
 	// (as used in the list command).
 	//
 	// For help on these formats see the documentation of the fmt package.
 	// Argument Indexes:
-	// 1:message id, 2: msg flags, 3: date, 4: from, 5: to, 6: subject
+	// 1: message id, 2: msg flags, 3: date, 4: from, 5: to, 6: subject
 	formats = map[int]string{
 		// show all values
 		inOutbox: "%5[1]d | %[2]s | %[3]s | %-15.15[4]s | %-15.15[5]s | %-50.50[6]s",

          
@@ 43,8 45,6 @@ var (
 	// DefaultFolderType is the folder format for unregistered folders.
 	defaultFolderType = inbox
 
-	listDelim = " | "
-
 	// todo:
 	sendmailConfig = "/home/schlichti/.etc/ssmtp/mailbox.gmx.conf"
 )

          
M decode.go +47 -20
@@ 1,43 1,70 @@ 
 package mailbox
 
 import (
-	"errors"
 	"io"
 	"mime"
+	"net/mail"
 	"strings"
 
 	"golang.org/x/text/encoding/charmap"
 	"golang.org/x/text/transform"
 )
 
-type decoder struct {
-	*mime.WordDecoder
-}
-
 var dec = &mime.WordDecoder{CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {
-	cr, err := decodeCharSet(charset)
-	if err != nil {
-		return nil, err
+	var tr transform.Transformer = transform.Nop
+	switch strings.ToLower(charset) {
+	case "iso-8859-1", "windows-1252":
+		tr = charmap.Windows1252.NewDecoder()
+	case "iso-8859-15":
+		tr = charmap.ISO8859_15.NewDecoder()
+	case "us-ascii": // actually, we need to replace us ascii > 0x7f with something?
 	}
-	return transform.NewReader(input, cr), nil
+	return transform.NewReader(input, tr), nil
 }}
 
-func safeDecodeHeader(key string) string {
-	h, err := dec.DecodeHeader(key)
+func safeDecodeHeader(s string) string {
+	h, err := dec.DecodeHeader(s)
 	if err != nil {
 		h = "Error: " + err.Error()
 	}
 	return h
 }
 
-func decodeCharSet(charset string) (transform.Transformer, error) {
-	switch strings.ToLower(charset) {
-	case "iso-8859-1", "windows-1252":
-		return charmap.Windows1252.NewDecoder(), nil
-	case "iso-8859-15":
-		return charmap.ISO8859_15.NewDecoder(), nil
-	case "utf-8", "us-ascii": // actually, we need to replace us ascii > 0x7f with something
-		return transform.Nop, nil
+func ReadMessage(r io.Reader) (*Message, error) {
+	msg, err := mail.ReadMessage(r)
+	if err != nil {
+		return nil, err
 	}
-	return nil, errors.New("unknown charset '" + charset + "'")
+	return &Message{NewHeader(msg.Header), msg.Body}, nil
+}
+
+type Message struct {
+	Header Header
+	Body   io.Reader
+}
+
+// todo: mime.header decoden?
+type Header struct {
+	mail.Header
+	dec *mime.WordDecoder
+}
+
+func NewHeader(h mail.Header) Header {
+	return Header{h, dec}
 }
+
+func (h Header) Decode(key string) string {
+	v, err := h.dec.DecodeHeader(h.Get(key))
+	if err != nil {
+		v = "Error: " + err.Error()
+	}
+	return v
+}
+
+func (h Header) DecodeAddresses(key string) []*mail.Address {
+	list, err := h.AddressList(key)
+	if err != nil {
+		list = []*mail.Address{&mail.Address{Name: "Error: " + err.Error()}}
+	}
+	return list
+}

          
M edit.go +3 -3
@@ 82,7 82,7 @@ func edit(mbox *mh.Mailbox, msg *mail.Me
 	if err != nil {
 		return "", err
 	}
-	if err = format(tmp, mbox.From(), msg); err != nil {
+	if err = format(tmp, mbox.Mailaddress, msg); err != nil {
 		tmp.Close()
 		os.Remove(tmp.Name())
 		return "", err

          
@@ 122,14 122,14 @@ func edit(mbox *mh.Mailbox, msg *mail.Me
 	wait.Add(1)
 	go func() {
 		defer wait.Done()
-		msgid, err = mbox.Append(mbox.Draft(), r)
+		msgid, err = mbox.Append(mbox.Draft, r)
 		r.Close()
 	}()
 	err1 := encodeMessage(w, newMsg)
 	// todo: error handling
 	w.Close()
 	wait.Wait()
-	msgfile := path.Join(mbox.Draft(), msgid)
+	msgfile := path.Join(mbox.Draft, msgid)
 	if err == nil {
 		err = err1
 	}

          
M list.go +16 -21
@@ 10,17 10,16 @@ import (
 	"bitbucket.org/telesto/mailbox/mh"
 )
 
-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 *mh.Mailbox, out io.Writer, folder string, pick *Sequence) error {
-	fType, ok := mbox.FolderType(folder)
+	fType, ok := mbox.Folders[folder]
 	if !ok {
 		fType = defaultFolderType
 	}
 	var err error
 	msgs, errors := mbox.ReadFolder(folder)
+	sixMonthPast := time.Now().Add(-6 * 30 * 24 * time.Hour)
+	oneDayAhead := time.Now().Add(24 * time.Hour)
 	for {
 		select {
 		case msg, ok := <-msgs:

          
@@ 30,25 29,23 @@ func List(mbox *mh.Mailbox, out io.Write
 			if !pick.IsMatch(msg.Id) {
 				continue
 			}
-			header := msg.Header
+			header := NewHeader(msg.Header)
+			// header := msg.Header
 			// ignore error, we can't do anything about it
 			date, _ := header.Date()
 			format := "Jan _2 15:04"
 			if date.Before(sixMonthPast) || date.After(oneDayAhead) {
-				format = "Jan _2 2006"
+				format = "Jan _2  2006"
 			}
 			// this was a deleted flag, we dont need that for now
 			flags := " "
 			fmt.Fprintf(out, formats[fType], msg.Id, flags,
 				date.Format(format),
-				decodeAddressList(header.Get("From")),
-				decodeAddressList(header.Get("To")),
-				safeDecodeHeader(header.Get("Subject")))
+				listAddresses(header.DecodeAddresses("From")),
+				listAddresses(header.DecodeAddresses("To")),
+				header.Decode("Subject"))
 			out.Write([]byte{'\n'})
-		case err1, ok := <-errors:
-			if !ok {
-				return err
-			}
+		case err1 := <-errors:
 			if err == nil {
 				err = err1
 			}

          
@@ 58,19 55,17 @@ func List(mbox *mh.Mailbox, out io.Write
 }
 
 // this function returns the names only!
-func decodeAddressList(list string) string {
+// todo: merge with edit.go:parseAddressList
+// put this and safeDecodeHeader() into an header struct
+func listAddresses(list []*mail.Address) string {
 	if len(list) == 0 {
 		return ""
 	}
-	addrList, err := mail.ParseAddressList(list)
-	if err != nil {
-		return "Error: " + err.Error()
-	}
 	var addresses []string
-	for i := range addrList {
-		address := addrList[i].Name
+	for i := range list {
+		address := list[i].Name
 		if len(address) == 0 {
-			address = addrList[i].Address
+			address = list[i].Address
 		}
 		addresses = append(addresses, address)
 	}

          
M mh/mh.go +40 -105
@@ 5,7 5,6 @@ package mh
 import (
 	"errors"
 	"io"
-	"io/ioutil"
 	"net/mail"
 	"os"
 	"path"

          
@@ 28,26 27,21 @@ const (
 	outbox
 )
 
-type Message struct {
-	Id int64
-	*mail.Message
-}
-
 // todo: maildir only, the rest is for account
 // we want to be able to use Mailbox by only providing the mail dir
 type Mailbox struct {
 	// MailDir points to the MH Maildir.
-	mailDir,
+	Maildir,
 	// Mailaddress holds the address of the sender.
-	mailAddress,
+	Mailaddress,
 	// if no folder for a msg is given this folder is used as default
-	defaultf string
+	DefaultFolder string
 	// Draft is the folder in which msg are put before being send.
-	draft string
+	Draft string
 	// Sent is a folder which holds sent messages.
-	sent string
+	Sent string
 
-	folders map[string]int
+	Folders map[string]int
 }
 
 func NewMailbox() *Mailbox {

          
@@ 58,14 52,13 @@ func NewMailbox() *Mailbox {
 }
 
 func (mh *Mailbox) osPath(elem ...string) string {
-	return path.Join(mh.mailDir, strings.Join(elem, "/"))
+	return path.Join(mh.Maildir, strings.Join(elem, "/"))
 }
 
 func (mh *Mailbox) msgFile(folder string, msgid int64) string {
-	return path.Join(mh.mailDir, folder, strconv.FormatInt(msgid, 10))
+	return mh.osPath(folder, strconv.FormatInt(msgid, 10))
 }
 
-// todo: we sort mails in ReadFolder, could use this here
 func (mh *Mailbox) nextId(folder string) (int64, error) {
 	if err := mh.lock(); err != nil {
 		return 0, err

          
@@ 76,44 69,43 @@ func (mh *Mailbox) nextId(folder string)
 	if err != nil {
 		return 0, err
 	}
-	var max int64
-	for i := range list {
-		// we even consider directories, so we do not create a
-		// file with a name of an existing directory
-		id, err := strconv.ParseInt(list[i].Name(), 10, 64)
-		if err != nil {
-			// ignore every file that apparently is not a mail
-			continue
-		}
-		if id > max {
-			max = id
-		}
+	if len(list) == 0 {
+		return 1, nil
 	}
-	return max, nil
+	return list[len(list)-1].Id + 1, nil
 }
 
-func (mbox Mailbox) FolderType(folder string) (int, bool) {
-	t, ok := mbox.folders[folder]
-	return t, ok
+type Message struct {
+	Id int64
+	*mail.Message
 }
 
-func (mbox Mailbox) Folders(parent string) ([]string, error) {
-	files, err := ioutil.ReadDir(mbox.osPath(parent))
+// returns a sorted list, can be a number file or a number dir
+func (mh *Mailbox) readDir(folder string) ([]numberInode, error) {
+
+	f, err := os.Open(mh.osPath(folder))
 	if err != nil {
 		return nil, err
 	}
-	var folders []string
-	for _, file := range files {
-		if file.IsDir() {
-			folders = append(folders, file.Name())
+	defer f.Close()
+	files, err := f.Readdir(-1)
+	if err != nil {
+		return nil, err
+	}
+	var msgs []numberInode
+	for i := 0; i < len(files); i++ {
+		// ignore every file with a filename not a number
+		if id, err := strconv.ParseInt(files[i].Name(), 10, 0); err == nil {
+			msgs = append(msgs, numberInode{files[i], id})
 		}
 	}
-	return folders, nil
+	sort.Slice(msgs, func(i, j int) bool { return msgs[i].Id < msgs[j].Id })
+	return msgs, f.Close()
 }
 
 // returns a sorted list
 func (mbox Mailbox) ReadFolder(folder string) (chan Message, chan error) {
-	mails := make(chan Message, 20)
+	mails := make(chan Message)
 	errors := make(chan error)
 	go func() {
 		defer close(errors)

          
@@ 128,11 120,7 @@ func (mbox Mailbox) ReadFolder(folder st
 		}
 		// continue on error, show what we got
 		for i := range list {
-			msgid, err := strconv.ParseInt(list[i].Name(), 10, 64)
-			if err != nil {
-				continue
-			}
-			file, err := mbox.open(folder, msgid)
+			file, err := mbox.open(folder, list[i].Id)
 			if err != nil {
 				// save error msg in subject instead of returning error
 				mails <- errMail(list[i].Id, err)

          
@@ 154,30 142,6 @@ func (mbox Mailbox) ReadFolder(folder st
 	return mails, errors
 }
 
-// returns a sorted list
-func (mh *Mailbox) readDir(folder string) ([]msgFile, error) {
-
-	f, err := os.Open(mh.osPath(folder))
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-	files, err := f.Readdir(-1)
-	if err != nil {
-		return nil, err
-	}
-	l := len(files)
-	var msgs []msgFile
-	for i := 0; i < l; i++ {
-		// ignore every file that is not a mail
-		if id, err := strconv.ParseInt(files[i].Name(), 10, 0); err == nil && !files[i].IsDir() {
-			msgs = append(msgs, msgFile{files[i], id})
-		}
-	}
-	sort.Sort(byId{msgs})
-	return msgs, f.Close()
-}
-
 func (mh *Mailbox) Append(folder string, msg io.Reader) (newMsg string, err error) {
 	if err := mh.lock(); err != nil {
 		return "", err

          
@@ 191,14 155,14 @@ func (mh *Mailbox) Append(folder string,
 	var file *os.File
 	file, err = os.OpenFile(mh.msgFile(folder, id), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
 	if err != nil {
-		return
+		return newMsg, err
 	}
 	if _, err = io.Copy(file, msg); err != nil {
 		file.Close()
-		return
+		return newMsg, err
 	}
 	if err = file.Close(); err != nil {
-		return
+		return newMsg, err
 	}
 	return mh.msgFile(folder, id), mh.unlock()
 }

          
@@ 206,8 170,8 @@ func (mh *Mailbox) Append(folder string,
 func errMail(id int64, err error) Message {
 	h := make(map[string][]string, 1)
 	h["Subject"] = []string{"Error: " + err.Error()}
-	b := strings.NewReader("Error: " + err.Error())
-	return Message{id, &mail.Message{h, b}}
+	body := strings.NewReader("Error: " + err.Error())
+	return Message{id, &mail.Message{h, body}}
 }
 
 func (mh *Mailbox) MkFolder(folder string) error {

          
@@ 221,19 185,7 @@ func (mh *Mailbox) MkFolder(folder strin
 	return mh.unlock()
 }
 
-// sorter implements sort.Interface.
-type sorter []msgFile
-
-func (f sorter) Len() int      { return len(f) }
-func (f sorter) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
-
-type byId struct {
-	sorter
-}
-
-func (f byId) Less(i, j int) bool { return f.sorter[i].Id < f.sorter[j].Id }
-
-type msgFile struct {
+type numberInode struct {
 	os.FileInfo
 	Id int64
 }

          
@@ 243,6 195,7 @@ func (mh *Mailbox) rename(fOld string, o
 }
 
 // todo: sort by date, use case: very old msg is moved into a folder
+// todo: this will fail if there is a number dir
 func (mh *Mailbox) Pack(folder string) error {
 	if err := mh.lock(); err != nil {
 		return err

          
@@ 334,8 287,7 @@ const lockfile = ".lock"
 
 func (mh *Mailbox) lock() error {
 	return nil
-	lock, err := os.OpenFile(mh.osPath(lockfile),
-		os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
+	lock, err := os.OpenFile(mh.osPath(lockfile), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
 	if err != nil {
 		if os.IsExist(err) {
 			return errors.New("mailbox is locked")

          
@@ 361,20 313,3 @@ func (mh *Mailbox) unlock() error {
 	}
 	return nil
 }
-
-func (mh *Mailbox) Default() string {
-	return mh.defaultf
-}
-
-func (mh *Mailbox) Draft() string {
-	return mh.draft
-}
-
-func (mh *Mailbox) Sent() string {
-	return mh.sent
-}
-
-func (mh *Mailbox) From() string {
-	return mh.mailAddress
-
-}

          
M sequence.go +1 -0
@@ 65,6 65,7 @@ func NewAlwaysMatcher() *Sequence {
 	return &Sequence{section: [][2]int64{[2]int64{0, 1<<63 - 1}}}
 }
 
+// todo: not used
 type Regexp struct {
 	matchHeader func(string) bool
 	matchBody   bool

          
M sequence_test.go +7 -4
@@ 6,12 6,13 @@ var testsSequence = []struct {
 	seq      string
 	parseErr bool
 	number   int64
-	want     bool
+	isMatch  bool
 }{
 	{"123", false, 123, true},
 	{"56,144", false, 56, true},
 	{"22-33", false, 22, true},
 	{"22-33", false, 23, true},
+	{"22-33", false, 34, false},
 	{"abc", true, 0, false},
 	{"-", true, 0, false},
 	{",", true, 0, false},

          
@@ 26,6 27,7 @@ var testsSequence = []struct {
 	{"44-56", false, 47, true},
 	{"12,14-16", false, 15, true},
 	{"1-6,45-55,80-90", false, 7, false},
+	{"1-6,45-55,80-90", false, 50, true},
 	{"1-6,45-55,80-90", false, 55, true},
 }
 

          
@@ 37,10 39,11 @@ func TestSequence(t *testing.T) {
 				i, err, tc.parseErr)
 		}
 		if err == nil {
-			got := s.Matches(tc.number)
-			if got != tc.want {
+			got := s.IsMatch(tc.number)
+			want := tc.isMatch
+			if got != want {
 				t.Errorf("%d: wrong matching, found '%t', expected %t",
-					i, got, tc.want)
+					i, got, want)
 			}
 		}
 	}

          
A => testdata/parts/1 +19 -0
@@ 0,0 1,19 @@ 
+Content-Transfer-Encoding: 7bit
+Content-Type: multipart/mixed;
+	boundary=Apple-Mail-B355CC53-B5E6-4AC7-805C-66B3AA273978
+Mime-Version: 1.0 (1.0)
+
+--Apple-Mail-B355CC53-B5E6-4AC7-805C-66B3AA273978
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: quoted-printable
+
+text
+
+--Apple-Mail-B355CC53-B5E6-4AC7-805C-66B3AA273978
+Content-Type: image/jpeg; name=IMG_4993.JPG;
+Content-Disposition: inline; filename=IMG_4993.JPG
+Content-Transfer-Encoding: base64
+
+image
+
+--Apple-Mail-B355CC53-B5E6-4AC7-805C-66B3AA273978--