M README.md +47 -26
@@ 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 @@ Proceed with the instructions you find i
$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 @@ is valid.
* 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
A => bin/mbhtml +5 -0
@@ 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
M bin/mbml +1 -1
@@ 7,5 7,5 @@ if [ $# != 2 ]; then
echo "usage: mbml src dst"
exit 1
fi
-mb mv $1/$( lib_LastMsg $1 ) $2
+mb mv "$1"/"$( lib_LastMsg $1 )" "$2"
M bin/mbrl +2 -2
@@ 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" )
M bin/mbvl +2 -2
@@ 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" )
M cmd/mb/main.go +43 -39
@@ 8,7 8,6 @@ import (
"os"
"path"
"strconv"
- "strings"
mb "bitbucket.org/telesto/mailbox"
"bitbucket.org/telesto/mailbox/mh"
@@ 171,52 170,51 @@ func main() {
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 <folder> 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 <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")
+// }
case "new":
var name string
if name, err = mb.Create(mbox); len(name) > 0 {
@@ 385,9 383,15 @@ func openMsg(mbox *mh.Mailbox, msg strin
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 {
M command.go +38 -21
@@ 20,22 20,29 @@ import (
// 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 @@ func Save(msg *mail.Message, number int)
return files, parse(msg, saver)
}
-// todo: print something like "inbox/33:1 <header>"?
-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 @@ func newPlainTextDecoder(dst io.Writer,
// 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
M decode.go +5 -17
@@ 14,14 14,6 @@ type decoder struct {
*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 @@ var dec = &mime.WordDecoder{CharsetReade
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) {
M edit.go +4 -6
@@ 215,15 215,14 @@ func fmtEditMsg(dst io.Writer, from stri
// 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 @@ func forwardMsg(dst io.Writer, from stri
// 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 @@ func fmtReply(dst io.Writer, sender stri
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
}
M list.go +34 -31
@@ 1,7 1,6 @@
package mailbox
import (
- "bufio"
"fmt"
"io"
"net/mail"
@@ 15,41 14,45 @@ var sixMonthPast = time.Now().Add(-6 * 3
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
}
M mh/mh.go +77 -55
@@ 5,6 5,7 @@ package mh
import (
"errors"
"io"
+ "io/ioutil"
"net/mail"
"os"
"path"
@@ 32,6 33,8 @@ type Message struct {
*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 @@ func (mbox Mailbox) FolderType(folder st
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 @@ type msgFile struct {
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 @@ func (mh *Mailbox) Remove(folder string,
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 @@ func (lc *lockedCloser) Close() error {
}
// 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
}
M parse.go +2 -3
@@ 11,7 11,7 @@ import (
"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 @@ func mimeDecoder(p part) (io.Reader, err
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
}
}
M sequence.go +1 -2
@@ 72,9 72,8 @@ type Regexp struct {
}
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
}
}