# HG changeset patch # User telesto # Date 1508511567 -7200 # Fri Oct 20 16:59:27 2017 +0200 # Node ID 1f44b88e8d13547109d91c1126915127aee410c6 # Parent 5d4b2cfb31b3b5e69b206f27939c2932d02fc151 list: print emails while reading them don't read all emails and print them, but read all filenames and parse/print emails as we go. diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -1,12 +1,41 @@ # mailbox - an MH message system -Mailbox consists of a library and a command line tool and is written in - [Go](http://golang.org/). Mailbox is a program in the spirit of [nmh](http://www.nongnu.org/nmh/) - many ideas of nmh found their way into mailbox. Compared to nmh, mailbox has less features, as it aims to be simpler to setup and to use than nmh. -As mailbox is setup/installed by editing and compiling the source code it takes some programming skills and a Go compiler to use mailbox. +Mailbox is a command line tool and is written in [Go](http://golang.org/). +Mailbox is a program in the spirit of [nmh](http://www.nongnu.org/nmh/) - many ideas of nmh found their way into mailbox. Compared to nmh, mailbox has less features, as it aims to be simpler to setup and to use than nmh. +As mailbox is setup/installed by editing and compiling the source code - it takes some programming skills and a go compiler to use mailbox. + +## Nonfeatures +### No state +State is the root of all evil. Mailbox has no "current message", no "current folder" or the like. + +### No support for .mh_sequence files +No State means: no permanent sequences. IMO it's not worth the effort, as I never use sequences (except marking messages as new, but i can arrange that by using the folder structure). +Saying that, mailbox is problably not useful for people who receive a lot of emails. + +### No MTA or MRA functionality +Mailbox is a MUA - it does one thing and does it well. -## Features/Nonfeatures -### No support for .mh_sequence files -Not worth the SLOC, as I never use sequences (except marking new messages, but i can live without). -Saying that, mailbox is problably not useful for people who receive a lot of emails. +## Features +### Acts as a Filter +Many Mailbox commands can act as a filter by reading from/writing to stdin. +Example: + + mb raw inbox/4 | mb append draft # copy msg no. 4 from folder "inbox" to "draft" + +### Mime-Support +Reading Mime-Messages is fun again! + +### Sequences +Mailbox supports a slightly modified subset of nmh sequences when adressing messages. +There are two fundamental types of sequence tokens (n, m are non zero numbers): + +* "n": msg id == n +* "n-m": msg id >= n and msg id <= m + +Sequence tokens can be or-combined with ",". +Sequences can be prefixed with a folder name. If no folder name is given the default +folder will be used. + +Example: Sequence "inbox/1,6-8,11" will match messages 1,6,7,8,11 in the "inbox" folder. ## Installation Mailbox is installed by editing and compiling the source code. @@ -19,30 +48,18 @@ $GOPATH/dev/src/bitbucket.org/telesto/mailbox/setup_template.go to setup mailbox. -## Sequences -Mailbox supports a slightly modified subset of nmh sequences when -adressing messages. -There are two fundamental sequence tokens (n, m, o are non zero numbers): - -* "n": msg id == n -* "m-o": message id is >= m and <= o +## Command +### ls +List messages in a folder. -These simple sequences tokens can be or-combined with ",", like: +Example: List all messages in inbox folder: -* "n,m": msg id == n or msg id == m -* "n,m-o": msg id == n or (message id is >= m and <= o) - -There is no limit, a sequence like 1,5,18-29,44,71,91-98,103,112-145 -is valid. + mb ls inbox ## Examples -* list all messages in inbox folder +* view message 318 in default folder - mb ls inbox - -* view message 318 in "inbox" - - mb view inbox/318 + mb view 318 * move some messages to the trash folder @@ -51,3 +68,7 @@ * remove all messages in "trash" mb rm trash/1-99999 + +## TODO +### Setting up your Email enviroment +fetchmail && "mb append" \ No newline at end of file diff --git a/bin/mbhtml b/bin/mbhtml new file mode 100755 --- /dev/null +++ b/bin/mbhtml @@ -0,0 +1,5 @@ +#!/bin/sh +# mbhtml pipes the output of an mb part command to lynx +# todo: check args +# todo: if len(args) == 1 read from stdin +mb part "$@" | lynx -stdin \ No newline at end of file diff --git a/bin/mbml b/bin/mbml --- a/bin/mbml +++ b/bin/mbml @@ -7,5 +7,5 @@ echo "usage: mbml src dst" exit 1 fi -mb mv $1/$( lib_LastMsg $1 ) $2 +mb mv "$1"/"$( lib_LastMsg $1 )" "$2" diff --git a/bin/mbrl b/bin/mbrl --- a/bin/mbrl +++ b/bin/mbrl @@ -3,9 +3,9 @@ source $GOPATH/src/bitbucket.org/telesto/mailbox/bin/lib -if ! [ $1 ]; then +if ! [ "$1" ]; then echo "usage: mbrl folder" exit 1 fi -mb rm $1/$( lib_LastMsg $1 ) +mb rm "$1"/$( lib_LastMsg "$1" ) diff --git a/bin/mbvl b/bin/mbvl --- a/bin/mbvl +++ b/bin/mbvl @@ -3,9 +3,9 @@ source $GOPATH/src/bitbucket.org/telesto/mailbox/bin/lib -if ! [ $1 ]; then +if ! [ "$1" ]; then echo "usage: mbvl folder" exit 1 fi -mb view $1/$( lib_LastMsg $1 ) +mb view "$1"/$( lib_LastMsg "$1" ) diff --git a/cmd/mb/main.go b/cmd/mb/main.go --- a/cmd/mb/main.go +++ b/cmd/mb/main.go @@ -8,7 +8,6 @@ "os" "path" "strconv" - "strings" mb "bitbucket.org/telesto/mailbox" "bitbucket.org/telesto/mailbox/mh" @@ -171,52 +170,51 @@ err = err1 } case "parts": - var src io.ReadCloser = os.Stdin - if len(os.Args) > 2 { - if src, err = openMsg(mbox, os.Args[2]); err != nil { - break - } - } - var msg *mail.Message - if msg, err = mail.ReadMessage(src); err != nil { - src.Close() - break - } - var parts []string - parts, err = mb.Parts(msg) - for i, part := range parts { - os.Stdout.WriteString(strconv.Itoa(i+1) + ": " + part + "\n") - } - if err != nil { - src.Close() - break - } - err = src.Close() - case "grep": - // syntax: pick regexp + // todo: read sequence from stdin if len(os.Args) < 3 { + // todo: read stdin err = missingParams break } var folder string - var match *mb.Regexp - if folder, match, err = parseRegexpCmd(os.Args[2:]); err != nil { + var msgs *mb.Sequence + folder, msgs, err = parseSequence(mbox, os.Args[2]) + if err != nil { break } - var msgs []mh.Message - if msgs, err = mbox.ReadFolder(folder); err != nil { + var parts []string + parts, err = mb.Parts(mbox, folder, msgs) + for _, part := range parts { + os.Stdout.WriteString(part + "\n") + } + if 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 "grep": + // syntax: pick 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") +// } case "new": var name string if name, err = mb.Create(mbox); len(name) > 0 { @@ -385,9 +383,15 @@ if len(f) == 0 { f = mbox.Default() } - return mbox.Open(f, b) + id, err := strconv.ParseInt(b, 10, 64) + if err != nil { + return nil, err + } + return mbox.Open(f, id) } +// todo: accept things like "inbox/5 inbox/8\ndraft/18-20" +// todo: make the folder part of the sequence func parseSequence(mbox *mh.Mailbox, value string) (string, *mb.Sequence, error) { folder, seq := path.Split(value) if len(folder) == 0 { diff --git a/command.go b/command.go --- a/command.go +++ b/command.go @@ -20,22 +20,29 @@ // its not an error if no match is found. func walk(mbox *mh.Mailbox, folder string, seq *Sequence, proc func(int64) error) error { - // todo: let only sorted msgs out here - msgs, err := mbox.ReadFolder(folder) - if err != nil { - return err - } - // we need a sorted list, so the "Move" cmd will will process the msgs + // we need a sorted list, so the "Move" cmd will process the msgs // in the correct time order - // todo: see above, need sorted list - for j := range msgs { - if seq.IsMatch(msgs[j].Id) { - if err = proc(msgs[j].Id); err != nil { - return err + 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 { + err = err1 } } + case err1, ok := <-errors: + if !ok { + return err + } + if err == nil { + err = err1 + } } - return nil + return err } func Move(mbox *mh.Mailbox, srcFolder string, msgs *Sequence, dst string) error { @@ -92,15 +99,26 @@ return files, parse(msg, saver) } -// todo: print something like "inbox/33:1
"? -func Parts(msg *mail.Message) ([]string, error) { - var parts []string +// todo: decode "name" - partsLister := func(p part, no int) error { - parts = append(parts, p.Header.Get("Content-Type")) - return nil +func Parts(mbox *mh.Mailbox, folder string, msgs *Sequence) ([]string, error) { + var parts []string + proc := func(id int64) error { + r, err := mbox.Open(folder, id) + if err != nil { + return err + } + msg, err := mail.ReadMessage(r) + if err != nil { + 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"))) + return nil + } + return parse(msg, partsLister) } - return parts, parse(msg, partsLister) + return parts, walk(mbox, folder, msgs, proc) } func ViewPart(dst io.Writer, msg *mail.Message, number int) error { @@ -150,10 +168,9 @@ // if the mail has only one header line without a nl at the end, "mailbox: EOF" // is thrown func View(dst io.Writer, msg *mail.Message) error { - dec := newDecoder() var b bytes.Buffer for _, key := range viewHeader { - b.WriteString(key + ": " + dec.safeDecodeHeader(msg.Header.Get(key)) + "\n") + b.WriteString(key + ": " + safeDecodeHeader(msg.Header.Get(key)) + "\n") } if _, err := io.Copy(dst, &b); err != nil { return err diff --git a/decode.go b/decode.go --- a/decode.go +++ b/decode.go @@ -14,14 +14,6 @@ *mime.WordDecoder } -func (d decoder) safeDecodeHeader(key string) string { - value, err := d.DecodeHeader(key) - if err != nil { - value = "Error: " + err.Error() - } - return value -} - var dec = &mime.WordDecoder{CharsetReader: func(charset string, input io.Reader) (io.Reader, error) { cr, err := decodeCharSet(charset) if err != nil { @@ -30,16 +22,12 @@ return transform.NewReader(input, cr), nil }} -func newDecoder() decoder { - dec := new(mime.WordDecoder) - dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { - cr, err := decodeCharSet(charset) - if err != nil { - return nil, err - } - return transform.NewReader(input, cr), nil +func safeDecodeHeader(key string) string { + h, err := dec.DecodeHeader(key) + if err != nil { + h = "Error: " + err.Error() } - return decoder{dec} + return h } func decodeCharSet(charset string) (transform.Transformer, error) { diff --git a/edit.go b/edit.go --- a/edit.go +++ b/edit.go @@ -215,15 +215,14 @@ // BUG: inbox/5556 gibt error EOF func forwardMsg(dst io.Writer, from string, msg *mail.Message) error { - dec := newDecoder() - subject := dec.safeDecodeHeader(msg.Header.Get("Subject")) + subject := safeDecodeHeader(msg.Header.Get("Subject")) var b bytes.Buffer fmt.Fprintf(&b, "From: %s\nTo: \nSubject: Fw: %s\nCc: \nBcc: \n", from, subject) if ct := msg.Header.Get("Content-Type"); len(ct) > 0 { fmt.Fprintf(&b, "Content-Type: %s\n", ct) } b.WriteString("\n\n\nBegin forwarded message:\n\n\n") - fmt.Fprintf(&b, "Date: %s\nFrom: %s\nTo: %s\nCc: %s\nSubject: %s\n\n", dec.safeDecodeHeader(msg.Header.Get("Date")), + fmt.Fprintf(&b, "Date: %s\nFrom: %s\nTo: %s\nCc: %s\nSubject: %s\n\n", safeDecodeHeader(msg.Header.Get("Date")), parseAddressList(msg.Header.Get("From")), parseAddressList(msg.Header.Get("To")), parseAddressList(msg.Header.Get("Cc")), subject) @@ -244,7 +243,6 @@ // fmtReply does always a "group reply" func fmtReply(dst io.Writer, sender string, msg *mail.Message) error { - dec := newDecoder() b, err := boundary(textproto.MIMEHeader(msg.Header)) if err != nil { return errors.New("reading mime boundary: " + err.Error()) @@ -256,9 +254,9 @@ if _, err := fmt.Fprintf(dst, "From: %s\nTo: %s\nSubject: Re: %s\nCc: %s\nBcc: \n\n"+ "\nOn %s, %s wrote\n\n\n", sender, parseAddressList(h.Get("From")), - dec.safeDecodeHeader(h.Get("Subject")), + safeDecodeHeader(h.Get("Subject")), parseAddressList(h.Get("Cc")), - dec.safeDecodeHeader(h.Get("Date")), + safeDecodeHeader(h.Get("Date")), parseAddressList(h.Get("From"))); err != nil { return err } diff --git a/list.go b/list.go --- a/list.go +++ b/list.go @@ -1,7 +1,6 @@ package mailbox import ( - "bufio" "fmt" "io" "net/mail" @@ -15,41 +14,45 @@ var oneDayAhead = time.Now().Add(24 * time.Hour) // todo: delim as param -func List(mbox *mh.Mailbox, dst io.Writer, folder string, pick *Sequence) error { - msgs, err := mbox.ReadFolder(folder) - if err != nil && msgs == nil { - // don't leave if we have msgs, show as much as we can - return err - } - out := bufio.NewWriter(dst) - defer out.Flush() - dec := newDecoder() +func List(mbox *mh.Mailbox, out io.Writer, folder string, pick *Sequence) error { fType, ok := mbox.FolderType(folder) if !ok { fType = defaultFolderType } - for _, msg := range msgs { - if !pick.IsMatch(msg.Id) { - continue - } - 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" + var err error + msgs, errors := mbox.ReadFolder(folder) + for { + select { + case msg, ok := <-msgs: + if !ok { + return err + } + if !pick.IsMatch(msg.Id) { + continue + } + 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" + } + // 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"))) + out.Write([]byte{'\n'}) + case err1, ok := <-errors: + if !ok { + return err + } + if err == nil { + err = err1 + } } - // 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")), - dec.safeDecodeHeader(header.Get("Subject"))) - out.WriteByte('\n') - } - if err1 := out.Flush(); err == nil { - err = err1 } return err } diff --git a/mh/mh.go b/mh/mh.go --- a/mh/mh.go +++ b/mh/mh.go @@ -5,6 +5,7 @@ import ( "errors" "io" + "io/ioutil" "net/mail" "os" "path" @@ -32,6 +33,8 @@ *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, @@ -94,43 +97,85 @@ return t, ok } -func (mbox Mailbox) ReadFolder(folder string) ([]Message, error) { - if err := mbox.lock(); err != nil { +func (mbox Mailbox) Folders(parent string) ([]string, error) { + files, err := ioutil.ReadDir(mbox.osPath(parent)) + if err != nil { return nil, err } - defer mbox.unlock() - - // todo: return sorted set here? - list, err := mbox.readDir(folder) - setErr := func(err1 error) { - if err == nil { - err = err1 + var folders []string + for _, file := range files { + if file.IsDir() { + folders = append(folders, file.Name()) } } - // continue on error, show what we got - var mails []Message - for i := range list { - file, err1 := mbox.open(folder, list[i].Name()) - if err1 != nil { - setErr(err1) - // save error msg in subject instead of returning error - mails = append(mails, errMail(list[i].Id, err1)) - continue + return folders, nil +} + +// returns a sorted list +func (mbox Mailbox) ReadFolder(folder string) (chan Message, chan error) { + mails := make(chan Message, 20) + errors := make(chan error) + go func() { + defer close(errors) + defer close(mails) + if err := mbox.lock(); err != nil { + errors <- err + } + defer mbox.unlock() + list, err := mbox.readDir(folder) + if err != nil { + errors <- err } - msg, err1 := mail.ReadMessage(file) - // safe err1 for error msg in mail, see below - if err2 := file.Close(); err2 != nil { - setErr(err2) + // 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) + if err != nil { + // save error msg in subject instead of returning error + mails <- errMail(list[i].Id, err) + continue + } + msg, err := mail.ReadMessage(file) + // safe err1 for error msg in mail, see below + if err1 := file.Close(); err == nil { + err = err1 + } + if err != nil { + // save error msg in subject instead of returning error + mails <- errMail(list[i].Id, err) + continue + } + mails <- Message{list[i].Id, msg} } - setErr(err1) - if err1 != nil { - // save error msg in subject instead of returning error - mails = append(mails, errMail(list[i].Id, err1)) - continue + }() + 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}) } - mails = append(mails, Message{list[i].Id, msg}) } - return mails, err + sort.Sort(byId{msgs}) + return msgs, f.Close() } func (mh *Mailbox) Append(folder string, msg io.Reader) (newMsg string, err error) { @@ -193,29 +238,6 @@ Id int64 } -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) rename(fOld string, old int64, fNew string, new int64) error { return os.Rename(mh.msgFile(fOld, old), mh.msgFile(fNew, new)) } @@ -273,9 +295,9 @@ return mh.unlock() } -func (mh *Mailbox) open(folder, msg string) (io.ReadCloser, error) { +func (mh *Mailbox) open(folder string, msg int64) (io.ReadCloser, error) { var src io.ReadCloser - src, err := os.Open(mh.osPath(folder, msg)) + src, err := os.Open(mh.msgFile(folder, msg)) return src, err } @@ -294,7 +316,7 @@ } // todo: we need this for Raw, rename this? -func (mh *Mailbox) Open(folder, msg string) (io.ReadCloser, error) { +func (mh *Mailbox) Open(folder string, msg int64) (io.ReadCloser, error) { if err := mh.lock(); err != nil { return nil, err } diff --git a/parse.go b/parse.go --- a/parse.go +++ b/parse.go @@ -11,7 +11,7 @@ "strings" ) -// the api for mail.Message and multipart.Part is totally different, +// the api for mail.Message and multipart.Part is different, // which sucks. so we need to wrap them into a struct type part struct { Header textproto.MIMEHeader @@ -81,8 +81,7 @@ return nil, err } if charset, ok := params["charset"]; ok { - out, err = newDecoder().CharsetReader(charset, out) - if err != nil { + if out, err = dec.CharsetReader(charset, out); err != nil { return nil, err } } diff --git a/sequence.go b/sequence.go --- a/sequence.go +++ b/sequence.go @@ -72,9 +72,8 @@ } func (s Regexp) Matches(m mh.Message) bool { - dec := newDecoder() for key := range m.Header { - if s.matchHeader(key) && s.r.MatchString(dec.safeDecodeHeader(m.Header.Get(key))) { + if s.matchHeader(key) && s.r.MatchString(safeDecodeHeader(m.Header.Get(key))) { return true } }