Skip to content

Commit

Permalink
make maildir package more complete and fix some encoding bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
laochailan committed Jun 8, 2019
1 parent e62d0e0 commit b191538
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 27 deletions.
23 changes: 21 additions & 2 deletions mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func readParts(reader io.Reader, boundary string, parts []Part) ([]Part, error)
if err != nil {
return nil, err
}
if strings.HasPrefix(mediaType, "text/") {
if strings.HasPrefix(mediaType, "text/plain") {
slurp = convertToUtf8(slurp)
}

Expand Down Expand Up @@ -390,6 +390,25 @@ func (m *Mail) Encode() (string, error) {
return buffer.String(), err
}

func addToMaildir(maildirPath string, content []byte) (filename string, err error) {
md, err := maildir.Open(maildirPath, true)
if err != nil {
return
}

msg, err := md.NewMessage(content)
if err != nil {
return
}
err = msg.SetFlags("S")
if err != nil {
return
}

filename, err = msg.Filename()
return
}

func sendMail(m *Mail) error {
addrl, err := m.Header.AddressList("From")
if len(addrl) != 1 {
Expand Down Expand Up @@ -426,7 +445,7 @@ func sendMail(m *Mail) error {
return err
}

filename, err := maildir.Store(expandEnvHome(account.Sent_Dir), []byte(mailcont), "S")
filename, err := addToMaildir(expandEnvHome(account.Sent_Dir), []byte(mailcont))
if err != nil {
return err
}
Expand Down
8 changes: 5 additions & 3 deletions mailbuffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"os/exec"
"strings"

"github.com/paulrosania/go-charset/charset"
_ "github.com/paulrosania/go-charset/data"
"github.com/mattn/go-runewidth"
termbox "github.com/nsf/termbox-go"
)
Expand Down Expand Up @@ -101,7 +103,7 @@ func formatPlain(buf []termbox.Cell, y, w int, text string) ([]termbox.Cell, int
if runeWidth == 0 || (runeWidth == 2 && runewidth.IsAmbiguousWidth(ch)) {
runeWidth = 1
}
x+=runeWidth
x += runeWidth
}

for ; x < w; x++ {
Expand Down Expand Up @@ -180,8 +182,8 @@ func (b *MailBuffer) refreshBuf() {

func (b *MailBuffer) drawHeader() {
getHeader := func(key string) string {
dec := new(mime.WordDecoder)
str, err := dec.DecodeHeader(b.mail.Header.Get(key)) // ignore charset for now
dec := &mime.WordDecoder{charset.NewReader}
str, err := dec.DecodeHeader(b.mail.Header.Get(key))
if err != nil {
str = err.Error()
}
Expand Down
187 changes: 165 additions & 22 deletions maildir/maildir.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@
// Use of this source code is governed by the MIT-styled
// license that can be found in the LICENSE file.

// Package maildir provides simple routines to add messages to a Maildir.
// Package maildir provides methods for interacting with the Maildir format as specified at http://cr.yp.to/proto/maildir.html
package maildir

import (
"crypto/rand"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"time"
)

var ErrorUnknownKey = errors.New("Key does not exist in Maildir.")
var ErrorDuplicateKey = errors.New("Key exists more than once.")

func key() (string, error) {
hostname, err := os.Hostname()
if err != nil {
Expand All @@ -30,47 +36,184 @@ func key() (string, error) {
return fmt.Sprintf("%v.M%vP%vR%x.%v", t.Unix(), t.Nanosecond()/1000, os.Getpid(), string(buf[:]), hostname), nil
}

// Store stores a message an a given maildir.
// flags must be given as a sorted string of valid Maildir flags, e.g. "FRS".
//
// If flags is empty, the mail is stored in maidir/new. If not, it’s stored to maildir/cur.
func Store(maildir string, mail []byte, flags string) (filename string, err error) {
_, err = os.Stat(maildir)
if err != nil {
return "", err
func splitKeyFlags(filename string) (key, flags string, err error) {
splits := strings.Split(filepath.Base(filename), ":2,")
if len(splits) != 2 {
return "", "", errors.New("Mail filename format violation.")
}

return splits[0], splits[1], nil
}

func joinKeyFlags(key, flags string) string {
return key + ":2," + flags
}

// Dir represents a single Maildir folder.
type Dir struct {
path string
}

// Open opens a maildir. Given create, if the "cur", "tmp", or "new" directories do not exist, they are created.
func Open(path string, create bool) (md *Dir, err error) {
md = &Dir{path}
if !create {
return md, nil
}

err = os.Mkdir(path, os.ModeDir|0700)
if err != nil && !os.IsExist(err) {
return nil, fmt.Errorf("creating Maildir failed: %s", err)
}

err = os.Mkdir(filepath.Join(path, "new"), os.ModeDir|0700)
if err != nil && !os.IsExist(err) {
return nil, fmt.Errorf("creating Maildir failed: %s", err)
}

err = os.Mkdir(filepath.Join(path, "cur"), os.ModeDir|0700)
if err != nil && !os.IsExist(err) {
return nil, fmt.Errorf("creating Maildir failed: %s", err)
}

basename, err := key()
err = os.Mkdir(filepath.Join(path, "tmp"), os.ModeDir|0700)
if err != nil && !os.IsExist(err) {
return nil, fmt.Errorf("creating Maildir failed: %s", err)
}

return md, nil
}

// NewMessage writes a raw mail into the Maildir. After writing is complete, the corresponding file is moved to the "new" directory.
func (md *Dir) NewMessage(mail []byte) (msg *Message, err error) {
msg = new(Message)

msg.md = md
msg.Key, err = key()
if err != nil {
return "", err
return nil, err
}
tmpname := filepath.Join(maildir, "tmp", basename)

tmpname := filepath.Join(md.path, "tmp", msg.Key)
file, err := os.Create(tmpname)
if err != nil {
return "", err
return nil, err
}

size, err := file.Write(mail)
file.Close()
if err != nil {
os.Remove(tmpname)
return "", err
return nil, err
}

name := fmt.Sprintf("%s,S=%d", basename, size)
name := fmt.Sprintf("%s,S=%d", msg.Key, size)

target := "new"
if len(flags) != 0 {
target = "cur"
name += ":2," + flags
}

filename = filepath.Join(maildir, target, name)
err = os.Rename(tmpname, filename)
msg.filename = filepath.Join(md.path, target, name)
err = os.Rename(tmpname, msg.filename)
if err != nil {
os.Remove(tmpname)
return nil, err
}

return msg, nil
}

func (md *Dir) keyToFilename(key string) (filename string, err error) {
matches, err := filepath.Glob(filepath.Join(md.path, "[new,cur]", key+"*"))
if err != nil {
return "", err
}
if len(matches) == 0 {
return "", ErrorUnknownKey
}
if len(matches) > 1 {
return "", ErrorDuplicateKey
}

filename = matches[0]
return
}

// Get finds a message in the Maildir by its unique Key.
func (md *Dir) Get(key string) (msg *Message, err error) {
fn, err := md.keyToFilename(key)
msg = &Message{key, fn, md}
return
}

// List returns a list of all messages in the Maildir
func (md *Dir) List() (msgs []Message, err error) {
filenames, err := filepath.Glob(filepath.Join(md.path, "[new,cur]", "*"))
if err != nil {
return nil, err
}

msgs = make([]Message, len(filenames))

for i, f := range filenames {
msgs[i].Key, _, err = splitKeyFlags(f)
if err != nil {
return nil, err
}

msgs[i].filename = f
msgs[i].md = md
}

return msgs, nil
}

// Message represents a file in a Maildir.
type Message struct {
// Key is a unique string identifying the message in the Maildir
Key string

// last known filename. May be invalid.
filename string

// Maildir the mail belongs to
md *Dir
}

// Flags returns the current flags stored in the message filename.
func (msg *Message) Flags() (flags string, err error) {
fn, err := msg.Filename()
if err != nil {
return "", err
}

return filename, nil
_, flags, err = splitKeyFlags(fn)
return
}

// Filename returns the current path of the file containing Message.
// Treat with caution as it may become invalid after flags are changed by another MUA for example.
func (msg *Message) Filename() (string, error) {
_, err := os.Stat(msg.filename)
if err == nil {
return msg.filename, nil
}

return msg.md.keyToFilename(msg.Key)
}

// SetFlags sets the flags on a message and moves it into the "cur" directory (if not already).
// newFlags is a string containing at most one letter of the flags defined by the specification: PRSTDF.
func (msg *Message) SetFlags(newFlags string) error {
sortedFlags := []byte(newFlags)
sort.Slice(sortedFlags, func(i, j int) bool { return sortedFlags[i] < sortedFlags[j] })

filename, err := msg.Filename()
if err != nil {
return err
}

newFilename := filepath.Join(msg.md.path, "cur", joinKeyFlags(msg.Key, string(sortedFlags)))
err = os.Rename(filename, newFilename)
msg.filename = newFilename

return err
}

0 comments on commit b191538

Please sign in to comment.