~sircmpwn/aerc

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
6 2

[PATCH v2 0/5] Add maildir support (#16)

Details
Message ID
<20190711134454.80318-1-ben@benburwell.com>
Sender timestamp
1562852689
DKIM signature
missing
Download raw message
For this reroll, I've updated the error handling for malformed messages,
added support for maildir sources relative to the user's home directory,
and added a previously-overlooked mention of aerc-maildir to
aerc-config.

Ben Burwell (5):
  Create UIDStore package
  Add maildir backend worker
  Handle the invalid "utf8" encoding
  Implement maildir copy
  Add maildir docs

 Makefile                    |   3 +
 doc/aerc-config.5.scd       |   3 +-
 doc/aerc-maildir.5.scd      |  40 ++++
 doc/aerc.1.scd              |   3 +-
 go.mod                      |   3 +-
 go.sum                      |   4 +
 lib/uidstore/uidstore.go    |  62 +++++++
 worker/maildir/container.go | 143 +++++++++++++++
 worker/maildir/message.go   | 322 ++++++++++++++++++++++++++++++++
 worker/maildir/worker.go    | 354 ++++++++++++++++++++++++++++++++++++
 worker/worker.go            |   3 +
 11 files changed, 937 insertions(+), 3 deletions(-)
 create mode 100644 doc/aerc-maildir.5.scd
 create mode 100644 lib/uidstore/uidstore.go
 create mode 100644 worker/maildir/container.go
 create mode 100644 worker/maildir/message.go
 create mode 100644 worker/maildir/worker.go

-- 
2.22.0

[PATCH v2 1/5] Create UIDStore package

Details
Message ID
<20190711134454.80318-2-ben@benburwell.com>
In-Reply-To
<20190711134454.80318-1-ben@benburwell.com> (view parent)
Sender timestamp
1562852690
DKIM signature
missing
Download raw message
Patch: +62 -0
This package can be used to provide a source for mapping mock UIDs back
to relevant keys for alternate backends. For example, for the Maildir
backend, we need to map between UID and message file names.
---
 lib/uidstore/uidstore.go | 62 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)
 create mode 100644 lib/uidstore/uidstore.go

diff --git a/lib/uidstore/uidstore.go b/lib/uidstore/uidstore.go
new file mode 100644
index 0000000..11c5e47
--- /dev/null
+++ b/lib/uidstore/uidstore.go
@@ -0,0 +1,62 @@
// Package uidstore provides a concurrency-safe two-way mapping between UIDs
// used by the UI and arbitrary string keys as used by different mail backends.
//
// Multiple Store instances can safely be created and the UIDs that they
// generate will be globally unique.
package uidstore

import (
	"sync"
	"sync/atomic"
)

var nextUID uint32 = 1

// Store holds a mapping between application keys and globally-unique UIDs.
type Store struct {
	keyByUID map[uint32]string
	uidByKey map[string]uint32
	m        sync.Mutex
}

// NewStore creates a new, empty Store.
func NewStore() *Store {
	return &Store{
		keyByUID: make(map[uint32]string),
		uidByKey: make(map[string]uint32),
	}
}

// GetOrInsert returns the UID for the provided key. If the key was already
// present in the store, the same UID value is returned. Otherwise, the key is
// inserted and the newly generated UID is returned.
func (s *Store) GetOrInsert(key string) uint32 {
	s.m.Lock()
	defer s.m.Unlock()
	if uid, ok := s.uidByKey[key]; ok {
		return uid
	}
	uid := atomic.AddUint32(&nextUID, 1)
	s.keyByUID[uid] = key
	s.uidByKey[key] = uid
	return uid
}

// GetKey returns the key for the provided UID, if available.
func (s *Store) GetKey(uid uint32) (string, bool) {
	s.m.Lock()
	defer s.m.Unlock()
	key, ok := s.keyByUID[uid]
	return key, ok
}

// RemoveUID removes the specified UID from the store.
func (s *Store) RemoveUID(uid uint32) {
	s.m.Lock()
	defer s.m.Unlock()
	key, ok := s.keyByUID[uid]
	if ok {
		delete(s.uidByKey, key)
	}
	delete(s.keyByUID, uid)
}
-- 
2.22.0

[PATCH v2 2/5] Add maildir backend worker

Details
Message ID
<20190711134454.80318-3-ben@benburwell.com>
In-Reply-To
<20190711134454.80318-1-ben@benburwell.com> (view parent)
Sender timestamp
1562852691
DKIM signature
missing
Download raw message
Patch: +786 -0
Add the initial implementation of a backend for Maildir accounts. Much
of the functionality required is implemented in the go-message and
go-maildir libraries, so we use them as much as possible.

The maildir worker hooks into a new maildir:// URL scheme in the
accounts.conf file which points to a container of several maildir
directories. From there, the OpenDirectory, FetchDirectoryContents, etc
messages work on subdirectories. This is implemented as a Container
struct which handles mapping between the symbolic email folder names and
UIDs to the concrete directories and file names.
---
From v1, I updated the MessageInfo method to return an error. In order
that a single malformed message not affect the ability to view all other
messages requested in the same FetchMessageHeaders message, I removed
the MessageInfos method from the Container struct. Now, each requested
message gets an independent response.

Also, the Configure procedure learns how to handle ~ home directories.

 go.mod                      |   1 +
 go.sum                      |   2 +
 worker/maildir/container.go | 105 +++++++++++
 worker/maildir/message.go   | 322 ++++++++++++++++++++++++++++++++
 worker/maildir/worker.go    | 353 ++++++++++++++++++++++++++++++++++++
 worker/worker.go            |   3 +
 6 files changed, 786 insertions(+)
 create mode 100644 worker/maildir/container.go
 create mode 100644 worker/maildir/message.go
 create mode 100644 worker/maildir/worker.go

diff --git a/go.mod b/go.mod
index 1c3d156..40c4df2 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@ require (
	github.com/ddevault/go-libvterm v0.0.0-20190526194226-b7d861da3810
	github.com/emersion/go-imap v1.0.0-beta.6
	github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e
	github.com/emersion/go-maildir v0.0.0-20190505155239-cec913e0802c
	github.com/emersion/go-message v0.10.3
	github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317
	github.com/emersion/go-smtp v0.11.1
diff --git a/go.sum b/go.sum
index 6c33d94..41b28a1 100644
--- a/go.sum
+++ b/go.sum
@@ -22,6 +22,8 @@ github.com/emersion/go-imap v1.0.0-beta.6 h1:x1Mco8GTkkw2+1/YHasok0pWrQLpgwJVG07
github.com/emersion/go-imap v1.0.0-beta.6/go.mod h1:ORBuwFXdwt9QrAOecJPpirG6j9mao9wMfHIkd0EZfdo=
github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e h1:L7bswVJZcf2YHofgom49oFRwVqmBj/qZqDy9/SJpZMY=
github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
github.com/emersion/go-maildir v0.0.0-20190505155239-cec913e0802c h1:Rx3zrFK2haYD5dHh2kF937k6psgyLB6u8GgLjdDv+hw=
github.com/emersion/go-maildir v0.0.0-20190505155239-cec913e0802c/go.mod h1:GnCg8DiGPgjPjAW4qqrCJDTHYflFCe5bvLE+lJ6TLwI=
github.com/emersion/go-message v0.10.3 h1:4pajGb3Rq+gHLfRcWysgcwtGRNgLpB8LC6X/vRZ89d0=
github.com/emersion/go-message v0.10.3/go.mod h1:3h+HsGTCFHmk4ngJ2IV/YPhdlaOcR6hcgqM3yca9v7c=
github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 h1:rDJPbyliyym8ZL/Wt71kdolp6yaD4fLIQz638E6JEt0=
diff --git a/worker/maildir/container.go b/worker/maildir/container.go
new file mode 100644
index 0000000..351afed
--- /dev/null
+++ b/worker/maildir/container.go
@@ -0,0 +1,105 @@
package maildir

import (
	"fmt"
	"io/ioutil"
	"log"
	"path/filepath"
	"sort"

	"github.com/emersion/go-maildir"

	"git.sr.ht/~sircmpwn/aerc/lib/uidstore"
)

// A Container is a directory which contains other directories which adhere to
// the Maildir spec
type Container struct {
	dir  string
	log  *log.Logger
	uids *uidstore.Store
}

// NewContainer creates a new container at the specified directory
// TODO: return an error if the provided directory is not accessible
func NewContainer(dir string, l *log.Logger) *Container {
	return &Container{dir: dir, uids: uidstore.NewStore(), log: l}
}

// ListFolders returns a list of maildir folders in the container
func (c *Container) ListFolders() ([]string, error) {
	files, err := ioutil.ReadDir(c.dir)
	if err != nil {
		return nil, fmt.Errorf("error reading folders: %v", err)
	}
	dirnames := []string{}
	for _, f := range files {
		if f.IsDir() {
			dirnames = append(dirnames, f.Name())
		}
	}
	return dirnames, nil
}

// OpenDirectory opens an existing maildir in the container by name, moves new
// messages into cur, and registers the new keys in the UIDStore.
func (c *Container) OpenDirectory(name string) (maildir.Dir, error) {
	dir := c.Dir(name)
	keys, err := dir.Unseen()
	if err != nil {
		return dir, err
	}
	for _, key := range keys {
		c.uids.GetOrInsert(key)
	}
	return dir, nil
}

// Dir returns a maildir.Dir with the specified name inside the container
func (c *Container) Dir(name string) maildir.Dir {
	return maildir.Dir(filepath.Join(c.dir, name))
}

// UIDs fetches the unique message identifiers for the maildir
func (c *Container) UIDs(d maildir.Dir) ([]uint32, error) {
	keys, err := d.Keys()
	if err != nil {
		return nil, fmt.Errorf("could not get keys for %s: %v", d, err)
	}
	sort.Strings(keys)
	var uids []uint32
	for _, key := range keys {
		uids = append(uids, c.uids.GetOrInsert(key))
	}
	return uids, nil
}

// Message returns a Message struct for the given UID and maildir
func (c *Container) Message(d maildir.Dir, uid uint32) (*Message, error) {
	if key, ok := c.uids.GetKey(uid); ok {
		return &Message{
			dir: d,
			uid: uid,
			key: key,
		}, nil
	}
	return nil, fmt.Errorf("could not find message with uid %d in maildir %s",
		uid, d)
}

// DeleteAll deletes a set of messages by UID and returns the subset of UIDs
// which were successfully deleted, stopping upon the first error.
func (c *Container) DeleteAll(d maildir.Dir, uids []uint32) ([]uint32, error) {
	var success []uint32
	for _, uid := range uids {
		msg, err := c.Message(d, uid)
		if err != nil {
			return success, err
		}
		if err := msg.Remove(); err != nil {
			return success, err
		}
		success = append(success, uid)
	}
	return success, nil
}
diff --git a/worker/maildir/message.go b/worker/maildir/message.go
new file mode 100644
index 0000000..b95ec98
--- /dev/null
+++ b/worker/maildir/message.go
@@ -0,0 +1,322 @@
package maildir

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"mime/quotedprintable"
	gomail "net/mail"
	"strings"

	"github.com/emersion/go-maildir"
	"github.com/emersion/go-message"
	_ "github.com/emersion/go-message/charset"
	"github.com/emersion/go-message/mail"

	"git.sr.ht/~sircmpwn/aerc/models"
)

// A Message is an individual email inside of a maildir.Dir.
type Message struct {
	dir maildir.Dir
	uid uint32
	key string
}

// NewReader reads a message into memory and returns an io.Reader for it.
func (m Message) NewReader() (io.Reader, error) {
	f, err := m.dir.Open(m.key)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	b, err := ioutil.ReadAll(f)
	if err != nil {
		return nil, err
	}
	return bytes.NewReader(b), nil
}

// Flags fetches the set of flags currently applied to the message.
func (m Message) Flags() ([]maildir.Flag, error) {
	return m.dir.Flags(m.key)
}

// SetFlags replaces the message's flags with a new set.
func (m Message) SetFlags(flags []maildir.Flag) error {
	return m.dir.SetFlags(m.key, flags)
}

// MarkRead either adds or removes the maildir.FlagSeen flag from the message.
func (m Message) MarkRead(seen bool) error {
	flags, err := m.Flags()
	if err != nil {
		return fmt.Errorf("could not read previous flags: %v", err)
	}
	if seen {
		flags = append(flags, maildir.FlagSeen)
		return m.SetFlags(flags)
	}
	var newFlags []maildir.Flag
	for _, flag := range flags {
		if flag != maildir.FlagSeen {
			newFlags = append(newFlags, flag)
		}
	}
	return m.SetFlags(newFlags)
}

// Remove deletes the email immediately.
func (m Message) Remove() error {
	return m.dir.Remove(m.key)
}

// MessageInfo populates a models.MessageInfo struct for the message.
func (m Message) MessageInfo() (*models.MessageInfo, error) {
	f, err := m.dir.Open(m.key)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	msg, err := message.Read(f)
	if err != nil {
		return nil, fmt.Errorf("could not read message: %v", err)
	}
	bs, err := parseEntityStructure(msg)
	if err != nil {
		return nil, fmt.Errorf("could not get structure: %v", err)
	}
	env, err := parseEnvelope(&msg.Header)
	if err != nil {
		return nil, fmt.Errorf("could not get envelope: %v", err)
	}
	flags, err := m.Flags()
	if err != nil {
		return nil, fmt.Errorf("could not read flags: %v", err)
	}
	return &models.MessageInfo{
		BodyStructure: bs,
		Envelope:      env,
		Flags:         translateFlags(flags),
		InternalDate:  env.Date,
		RFC822Headers: &mail.Header{msg.Header},
		Size:          0,
		Uid:           m.uid,
	}, nil
}

// NewBodyPartReader creates a new io.Reader for the requested body part(s) of
// the message.
func (m Message) NewBodyPartReader(requestedParts []int) (io.Reader, error) {
	f, err := m.dir.Open(m.key)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	msg, err := message.Read(f)
	if err != nil {
		return nil, fmt.Errorf("could not read message: %v", err)
	}
	return fetchEntityPartReader(msg, requestedParts)
}

func fetchEntityPartReader(e *message.Entity, index []int) (io.Reader, error) {
	if len(index) < 1 {
		return nil, fmt.Errorf("no part to read")
	}
	if mpr := e.MultipartReader(); mpr != nil {
		idx := 0
		for {
			idx++
			part, err := mpr.NextPart()
			if err != nil {
				return nil, err
			}
			if idx == index[0] {
				rest := index[1:]
				if len(rest) < 1 {
					return fetchEntityReader(part)
				}
				return fetchEntityPartReader(part, index[1:])
			}
		}
	}
	if index[0] != 1 {
		return nil, fmt.Errorf("cannont return non-first part of non-multipart")
	}
	return fetchEntityReader(e)
}

// fetchEntityReader makes an io.Reader for the given entity. Since the
// go-message package decodes the body for us, and the UI expects to deal with
// a reader whose bytes are encoded with the part's encoding, we are in the
// interesting position of needing to re-encode the reader before sending it
// off to the UI layer.
//
// TODO: probably change the UI to expect an already-decoded reader and decode
// in the IMAP worker.
func fetchEntityReader(e *message.Entity) (io.Reader, error) {
	enc := e.Header.Get("content-transfer-encoding")
	var buf bytes.Buffer

	// base64
	if strings.EqualFold(enc, "base64") {
		wc := base64.NewEncoder(base64.StdEncoding, &buf)
		defer wc.Close()
		if _, err := io.Copy(wc, e.Body); err != nil {
			return nil, fmt.Errorf("could not base64 encode: %v", err)
		}
		return &buf, nil
	}

	// quoted-printable
	if strings.EqualFold(enc, "quoted-printable") {
		wc := quotedprintable.NewWriter(&buf)
		defer wc.Close()
		if _, err := io.Copy(wc, e.Body); err != nil {
			return nil, fmt.Errorf("could not quoted-printable encode: %v", err)
		}
		return &buf, nil
	}

	// other general encoding
	if _, err := io.Copy(&buf, e.Body); err != nil {
		return nil, err
	}

	return &buf, nil
}

// split a MIME type into its major and minor parts
func splitMIME(m string) (string, string) {
	parts := strings.Split(m, "/")
	if len(parts) != 2 {
		return parts[0], ""
	}
	return parts[0], parts[1]
}

func parseEntityStructure(e *message.Entity) (*models.BodyStructure, error) {
	var body models.BodyStructure
	contentType, ctParams, err := e.Header.ContentType()
	if err != nil {
		return nil, fmt.Errorf("could not parse content type: %v", err)
	}
	mimeType, mimeSubType := splitMIME(contentType)
	body.MIMEType = mimeType
	body.MIMESubType = mimeSubType
	body.Params = ctParams
	body.Description = e.Header.Get("content-description")
	body.Encoding = e.Header.Get("content-transfer-encoding")
	if cd := e.Header.Get("content-disposition"); cd != "" {
		contentDisposition, cdParams, err := e.Header.ContentDisposition()
		if err != nil {
			return nil, fmt.Errorf("could not parse content disposition: %v", err)
		}
		body.Disposition = contentDisposition
		body.DispositionParams = cdParams
	}
	body.Parts = []*models.BodyStructure{}
	if mpr := e.MultipartReader(); mpr != nil {
		for {
			part, err := mpr.NextPart()
			if err == io.EOF {
				return &body, nil
			} else if err != nil {
				return nil, err
			}
			ps, err := parseEntityStructure(part)
			if err != nil {
				return nil, fmt.Errorf("could not parse child entity structure: %v", err)
			}
			body.Parts = append(body.Parts, ps)
		}
	}
	return &body, nil
}

func parseEnvelope(h *message.Header) (*models.Envelope, error) {
	date, err := gomail.ParseDate(h.Get("date"))
	if err != nil {
		return nil, fmt.Errorf("could not parse date header: %v", err)
	}
	from, err := parseAddressList(h, "from")
	if err != nil {
		return nil, fmt.Errorf("could not read from address: %v", err)
	}
	to, err := parseAddressList(h, "to")
	if err != nil {
		return nil, fmt.Errorf("could not read to address: %v", err)
	}
	cc, err := parseAddressList(h, "cc")
	if err != nil {
		return nil, fmt.Errorf("could not read cc address: %v", err)
	}
	bcc, err := parseAddressList(h, "bcc")
	if err != nil {
		return nil, fmt.Errorf("could not read bcc address: %v", err)
	}
	return &models.Envelope{
		Date:      date,
		Subject:   h.Get("subject"),
		MessageId: h.Get("message-id"),
		From:      from,
		To:        to,
		Cc:        cc,
		Bcc:       bcc,
	}, nil
}

func parseAddressList(h *message.Header, key string) ([]*models.Address, error) {
	var converted []*models.Address
	hdr := h.Get(key)
	if strings.TrimSpace(hdr) == "" {
		return converted, nil
	}
	addrs, err := gomail.ParseAddressList(hdr)
	if err != nil {
		if strings.Index(hdr, "@") < 0 {
			return []*models.Address{&models.Address{
				Name: hdr,
			}}, nil
		}
		return nil, err
	}
	for _, addr := range addrs {
		parts := strings.Split(addr.Address, "@")
		var mbox, host string
		if len(parts) > 1 {
			mbox = strings.Join(parts[0:len(parts)-1], "@")
			host = parts[len(parts)-1]
		} else {
			mbox = addr.Address
		}
		converted = append(converted, &models.Address{
			Name:    addr.Name,
			Mailbox: mbox,
			Host:    host,
		})
	}
	return converted, nil
}

var flagMap = map[maildir.Flag]models.Flag{
	maildir.FlagReplied: models.AnsweredFlag,
	maildir.FlagSeen:    models.SeenFlag,
	maildir.FlagTrashed: models.DeletedFlag,
	maildir.FlagFlagged: models.FlaggedFlag,
	// maildir.FlagDraft Flag = 'D'
	// maildir.FlagPassed Flag = 'P'
}

func translateFlags(maildirFlags []maildir.Flag) []models.Flag {
	var flags []models.Flag
	for _, maildirFlag := range maildirFlags {
		if flag, ok := flagMap[maildirFlag]; ok {
			flags = append(flags, flag)
		}
	}
	return flags
}
diff --git a/worker/maildir/worker.go b/worker/maildir/worker.go
new file mode 100644
index 0000000..f0c92ed
--- /dev/null
+++ b/worker/maildir/worker.go
@@ -0,0 +1,353 @@
package maildir

import (
	"fmt"
	"io"
	"net/url"
	"os"
	"path/filepath"

	"github.com/emersion/go-maildir"

	"git.sr.ht/~sircmpwn/aerc/models"
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

var errUnsupported = fmt.Errorf("unsupported command")

// A Worker handles interfacing between aerc's UI and a group of maildirs.
type Worker struct {
	c        *Container
	selected *maildir.Dir
	worker   *types.Worker
}

// NewWorker creates a new maildir worker with the provided worker.
func NewWorker(worker *types.Worker) *Worker {
	return &Worker{worker: worker}
}

// Run starts the worker's message handling loop.
func (w *Worker) Run() {
	for {
		action := <-w.worker.Actions
		msg := w.worker.ProcessAction(action)
		if err := w.handleMessage(msg); err == errUnsupported {
			w.worker.PostMessage(&types.Unsupported{
				Message: types.RespondTo(msg),
			}, nil)
		} else if err != nil {
			w.worker.PostMessage(&types.Error{
				Message: types.RespondTo(msg),
				Error:   err,
			}, nil)
		}
	}
}

func (w *Worker) done(msg types.WorkerMessage) {
	w.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil)
}

func (w *Worker) err(msg types.WorkerMessage, err error) {
	w.worker.PostMessage(&types.Error{
		Message: types.RespondTo(msg),
		Error:   err,
	}, nil)
}

func (w *Worker) handleMessage(msg types.WorkerMessage) error {
	switch msg := msg.(type) {
	case *types.Unsupported:
		// No-op
	case *types.Configure:
		return w.handleConfigure(msg)
	case *types.Connect:
		return w.handleConnect(msg)
	case *types.ListDirectories:
		return w.handleListDirectories(msg)
	case *types.OpenDirectory:
		return w.handleOpenDirectory(msg)
	case *types.FetchDirectoryContents:
		return w.handleFetchDirectoryContents(msg)
	case *types.CreateDirectory:
		return w.handleCreateDirectory(msg)
	case *types.FetchMessageHeaders:
		return w.handleFetchMessageHeaders(msg)
	case *types.FetchMessageBodyPart:
		return w.handleFetchMessageBodyPart(msg)
	case *types.FetchFullMessages:
		return w.handleFetchFullMessages(msg)
	case *types.DeleteMessages:
		return w.handleDeleteMessages(msg)
	case *types.ReadMessages:
		return w.handleReadMessages(msg)
	case *types.CopyMessages:
		return w.handleCopyMessages(msg)
	case *types.AppendMessage:
		return w.handleAppendMessage(msg)
	case *types.SearchDirectory:
		return w.handleSearchDirectory(msg)
	}
	return errUnsupported
}

func (w *Worker) handleConfigure(msg *types.Configure) error {
	defer w.done(msg)
	u, err := url.Parse(msg.Config.Source)
	if err != nil {
		w.worker.Logger.Printf("error configuring maildir worker: %v", err)
		return err
	}
	dir := u.Path
	if u.Host == "~" {
		home, err := os.UserHomeDir()
		if err != nil {
			return fmt.Errorf("could not resolve home directory: %v", err)
		}
		dir = filepath.Join(home, u.Path)
	}
	w.c = NewContainer(dir, w.worker.Logger)
	w.worker.Logger.Printf("configured base maildir: %s", dir)
	return nil
}

func (w *Worker) handleConnect(msg *types.Connect) error {
	w.done(msg)
	return nil
}

func (w *Worker) handleListDirectories(msg *types.ListDirectories) error {
	defer w.done(msg)
	dirs, err := w.c.ListFolders()
	if err != nil {
		w.worker.Logger.Printf("error listing directories: %v", err)
		return err
	}
	for _, name := range dirs {
		w.worker.PostMessage(&types.Directory{
			Message: types.RespondTo(msg),
			Dir: &models.Directory{
				Name:       name,
				Attributes: []string{},
			},
		}, nil)
	}
	return nil
}

func (w *Worker) handleOpenDirectory(msg *types.OpenDirectory) error {
	defer w.done(msg)
	w.worker.Logger.Printf("opening %s", msg.Directory)
	dir, err := w.c.OpenDirectory(msg.Directory)
	if err != nil {
		return err
	}
	w.selected = &dir
	// TODO: why does this need to be sent twice??
	info := &types.DirectoryInfo{
		Info: &models.DirectoryInfo{
			Name:     msg.Directory,
			Flags:    []string{},
			ReadOnly: false,
			// total messages
			Exists: 0,
			// new messages since mailbox was last opened
			Recent: 0,
			// total unread
			Unseen: 0,
		},
	}
	w.worker.PostMessage(info, nil)
	w.worker.PostMessage(info, nil)
	return nil
}

func (w *Worker) handleFetchDirectoryContents(
	msg *types.FetchDirectoryContents) error {
	defer w.done(msg)
	uids, err := w.c.UIDs(*w.selected)
	if err != nil {
		w.worker.Logger.Printf("error scanning uids: %v", err)
		return err
	}
	w.worker.PostMessage(&types.DirectoryContents{
		Message: types.RespondTo(msg),
		Uids:    uids,
	}, nil)
	return nil
}

func (w *Worker) handleCreateDirectory(msg *types.CreateDirectory) error {
	dir := w.c.Dir(msg.Directory)
	defer w.done(msg)
	if err := dir.Create(); err != nil {
		w.worker.Logger.Printf("could not create directory %s: %v",
			msg.Directory, err)
		return err
	}
	return nil
}

func (w *Worker) handleFetchMessageHeaders(
	msg *types.FetchMessageHeaders) error {
	defer w.done(msg)
	for _, uid := range msg.Uids {
		m, err := w.c.Message(*w.selected, uid)
		if err != nil {
			w.worker.Logger.Printf("could not get message: %v", err)
			w.err(msg, err)
			continue
		}
		info, err := m.MessageInfo()
		if err != nil {
			w.worker.Logger.Printf("could not get message info: %v", err)
			w.err(msg, err)
			continue
		}
		w.worker.PostMessage(&types.MessageInfo{
			Message: types.RespondTo(msg),
			Info:    info,
		}, nil)
	}
	return nil
}

func (w *Worker) handleFetchMessageBodyPart(
	msg *types.FetchMessageBodyPart) error {
	defer w.done(msg)

	// get reader
	m, err := w.c.Message(*w.selected, msg.Uid)
	if err != nil {
		w.worker.Logger.Printf("could not get message %d: %v", msg.Uid, err)
		return err
	}
	r, err := m.NewBodyPartReader(msg.Part)
	if err != nil {
		w.worker.Logger.Printf(
			"could not get body part reader for message=%d, parts=%#v: %v",
			msg.Uid, msg.Part, err)
		return err
	}
	w.worker.PostMessage(&types.MessageBodyPart{
		Message: types.RespondTo(msg),
		Part: &models.MessageBodyPart{
			Reader: r,
			Uid:    msg.Uid,
		},
	}, nil)

	// mark message as read
	if err := m.MarkRead(true); err != nil {
		w.worker.Logger.Printf("could not mark message as read: %v", err)
		return err
	}

	// send updated flags to ui
	info, err := m.MessageInfo()
	if err != nil {
		w.worker.Logger.Printf("could not fetch message info: %v", err)
		return err
	}
	w.worker.PostMessage(&types.MessageInfo{
		Message: types.RespondTo(msg),
		Info:    info,
	}, nil)

	return nil
}

func (w *Worker) handleFetchFullMessages(msg *types.FetchFullMessages) error {
	defer w.done(msg)
	for _, uid := range msg.Uids {
		m, err := w.c.Message(*w.selected, uid)
		if err != nil {
			w.worker.Logger.Printf("could not get message %d: %v", uid, err)
			return err
		}
		r, err := m.NewReader()
		if err != nil {
			w.worker.Logger.Printf("could not get message reader: %v", err)
			return err
		}
		w.worker.PostMessage(&types.FullMessage{
			Message: types.RespondTo(msg),
			Content: &models.FullMessage{
				Uid:    uid,
				Reader: r,
			},
		}, nil)
	}
	return nil
}

func (w *Worker) handleDeleteMessages(msg *types.DeleteMessages) error {
	defer w.done(msg)
	deleted, err := w.c.DeleteAll(*w.selected, msg.Uids)
	if len(deleted) > 0 {
		w.worker.PostMessage(&types.MessagesDeleted{
			Message: types.RespondTo(msg),
			Uids:    deleted,
		}, nil)
	}
	if err != nil {
		w.worker.Logger.Printf("error removing some messages: %v", err)
		return err
	}
	return nil
}

func (w *Worker) handleReadMessages(msg *types.ReadMessages) error {
	defer w.done(msg)
	for _, uid := range msg.Uids {
		m, err := w.c.Message(*w.selected, uid)
		if err != nil {
			w.worker.Logger.Printf("could not get message: %v", err)
			w.err(msg, err)
			continue
		}
		if err := m.MarkRead(msg.Read); err != nil {
			w.worker.Logger.Printf("could not mark message as read: %v", err)
			w.err(msg, err)
			continue
		}
		info, err := m.MessageInfo()
		if err != nil {
			w.worker.Logger.Printf("could not get message info: %v", err)
			w.err(msg, err)
			continue
		}
		w.worker.PostMessage(&types.MessageInfo{
			Message: types.RespondTo(msg),
			Info:    info,
		}, nil)
	}
	return nil
}

func (w *Worker) handleCopyMessages(msg *types.CopyMessages) error {
	// TODO: this.
	return nil
}

func (w *Worker) handleAppendMessage(msg *types.AppendMessage) error {
	defer w.done(msg)
	dest := w.c.Dir(msg.Destination)
	delivery, err := dest.NewDelivery()
	if err != nil {
		w.worker.Logger.Printf("could not deliver message to %s: %v",
			msg.Destination, err)
		return err
	}
	defer delivery.Close()
	if _, err := io.Copy(delivery, msg.Reader); err != nil {
		w.worker.Logger.Printf("could not write message to destination: %v", err)
		return err
	}
	return nil
}

func (w *Worker) handleSearchDirectory(msg *types.SearchDirectory) error {
	return errUnsupported
}
diff --git a/worker/worker.go b/worker/worker.go
index 7db7be5..dd14a23 100644
--- a/worker/worker.go
+++ b/worker/worker.go
@@ -2,6 +2,7 @@ package worker

import (
	"git.sr.ht/~sircmpwn/aerc/worker/imap"
	"git.sr.ht/~sircmpwn/aerc/worker/maildir"
	"git.sr.ht/~sircmpwn/aerc/worker/types"

	"fmt"
@@ -27,6 +28,8 @@ func NewWorker(source string, logger *log.Logger) (*types.Worker, error) {
		fallthrough
	case "imaps":
		worker.Backend = imap.NewIMAPWorker(worker)
	case "maildir":
		worker.Backend = maildir.NewWorker(worker)
	default:
		return nil, fmt.Errorf("Unknown backend %s", u.Scheme)
	}
-- 
2.22.0

[PATCH v2 3/5] Handle the invalid "utf8" encoding

Details
Message ID
<20190711134454.80318-4-ben@benburwell.com>
In-Reply-To
<20190711134454.80318-1-ben@benburwell.com> (view parent)
Sender timestamp
1562852692
DKIM signature
missing
Download raw message
Patch: +3 -1
See commit 0bfc369eb68a1d34ea0ee983f218e97a14099959 in the go-message
package.
---
 go.mod | 2 +-
 go.sum | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index 40c4df2..a8a25ea 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
	github.com/emersion/go-imap v1.0.0-beta.6
	github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e
	github.com/emersion/go-maildir v0.0.0-20190505155239-cec913e0802c
	github.com/emersion/go-message v0.10.3
	github.com/emersion/go-message v0.10.4
	github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317
	github.com/emersion/go-smtp v0.11.1
	github.com/gdamore/tcell v1.1.2
diff --git a/go.sum b/go.sum
index 41b28a1..9ef4818 100644
--- a/go.sum
+++ b/go.sum
@@ -26,6 +26,8 @@ github.com/emersion/go-maildir v0.0.0-20190505155239-cec913e0802c h1:Rx3zrFK2haY
github.com/emersion/go-maildir v0.0.0-20190505155239-cec913e0802c/go.mod h1:GnCg8DiGPgjPjAW4qqrCJDTHYflFCe5bvLE+lJ6TLwI=
github.com/emersion/go-message v0.10.3 h1:4pajGb3Rq+gHLfRcWysgcwtGRNgLpB8LC6X/vRZ89d0=
github.com/emersion/go-message v0.10.3/go.mod h1:3h+HsGTCFHmk4ngJ2IV/YPhdlaOcR6hcgqM3yca9v7c=
github.com/emersion/go-message v0.10.4 h1:G2hQCx7lE1IFTRAKkDbolJy0d3EVQDxbwpDYcxjJocI=
github.com/emersion/go-message v0.10.4/go.mod h1:3h+HsGTCFHmk4ngJ2IV/YPhdlaOcR6hcgqM3yca9v7c=
github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 h1:rDJPbyliyym8ZL/Wt71kdolp6yaD4fLIQz638E6JEt0=
github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317 h1:tYZxAY8nu3JJQKios9f27Sbvbkfm4XHXT476gVtszu0=
-- 
2.22.0

[PATCH v2 4/5] Implement maildir copy

Details
Message ID
<20190711134454.80318-5-ben@benburwell.com>
In-Reply-To
<20190711134454.80318-1-ben@benburwell.com> (view parent)
Sender timestamp
1562852693
DKIM signature
missing
Download raw message
Patch: +41 -2
Create a delivery in the destination directory with the content of the
source message.
---
 worker/maildir/container.go | 38 +++++++++++++++++++++++++++++++++++++
 worker/maildir/worker.go    |  5 +++--
 2 files changed, 41 insertions(+), 2 deletions(-)

diff --git a/worker/maildir/container.go b/worker/maildir/container.go
index 351afed..aa14575 100644
--- a/worker/maildir/container.go
+++ b/worker/maildir/container.go
@@ -2,6 +2,7 @@ package maildir

import (
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"path/filepath"
@@ -103,3 +104,40 @@ func (c *Container) DeleteAll(d maildir.Dir, uids []uint32) ([]uint32, error) {
	}
	return success, nil
}

func (c *Container) CopyAll(
	dest maildir.Dir, src maildir.Dir, uids []uint32) error {
	for _, uid := range uids {
		if err := c.copyMessage(dest, src, uid); err != nil {
			return fmt.Errorf("could not copy message %d: %v", uid, err)
		}
	}
	return nil
}

func (c *Container) copyMessage(
	dest maildir.Dir, src maildir.Dir, uid uint32) error {
	key, ok := c.uids.GetKey(uid)
	if !ok {
		return fmt.Errorf("could not find key for message id %d", uid)
	}

	f, err := src.Open(key)
	if err != nil {
		return fmt.Errorf("could not open source message: %v", err)
	}

	del, err := dest.NewDelivery()
	if err != nil {
		return fmt.Errorf("could not initialize delivery: %v")
	}
	defer del.Close()

	if _, err = io.Copy(del, f); err != nil {
		return fmt.Errorf("could not copy message to delivery: %v")
	}

	// TODO: preserve flags

	return nil
}
diff --git a/worker/maildir/worker.go b/worker/maildir/worker.go
index f0c92ed..a5416cc 100644
--- a/worker/maildir/worker.go
+++ b/worker/maildir/worker.go
@@ -327,8 +327,9 @@ func (w *Worker) handleReadMessages(msg *types.ReadMessages) error {
}

func (w *Worker) handleCopyMessages(msg *types.CopyMessages) error {
	// TODO: this.
	return nil
	defer w.done(msg)
	dest := w.c.Dir(msg.Destination)
	return w.c.CopyAll(dest, *w.selected, msg.Uids)
}

func (w *Worker) handleAppendMessage(msg *types.AppendMessage) error {
-- 
2.22.0

[PATCH v2 5/5] Add maildir docs

Details
Message ID
<20190711134454.80318-6-ben@benburwell.com>
In-Reply-To
<20190711134454.80318-1-ben@benburwell.com> (view parent)
Sender timestamp
1562852694
DKIM signature
missing
Download raw message
Patch: +47 -2
---
From v1, added a note about aerc-maildir to aerc-config which I had
previously overlooked.

 Makefile               |  3 +++
 doc/aerc-config.5.scd  |  3 ++-
 doc/aerc-maildir.5.scd | 40 ++++++++++++++++++++++++++++++++++++++++
 doc/aerc.1.scd         |  3 ++-
 4 files changed, 47 insertions(+), 2 deletions(-)
 create mode 100644 doc/aerc-maildir.5.scd

diff --git a/Makefile b/Makefile
index 4ff1aae..6609640 100644
--- a/Makefile
+++ b/Makefile
@@ -29,6 +29,7 @@ DOCS := \
	aerc.1 \
	aerc-config.5 \
	aerc-imap.5 \
	aerc-maildir.5 \
	aerc-smtp.5 \
	aerc-tutorial.7

@@ -58,6 +59,7 @@ install: all
	install -m644 aerc.1 $(MANDIR)/man1/aerc.1
	install -m644 aerc-config.5 $(MANDIR)/man5/aerc-config.5
	install -m644 aerc-imap.5 $(MANDIR)/man5/aerc-imap.5
	install -m644 aerc-maildir.5 $(MANDIR)/man5/aerc-maildir.5
	install -m644 aerc-smtp.5 $(MANDIR)/man5/aerc-smtp.5
	install -m644 aerc-tutorial.7 $(MANDIR)/man7/aerc-tutorial.7
	install -m644 config/accounts.conf $(SHAREDIR)/accounts.conf
@@ -77,6 +79,7 @@ uninstall:
	$(RM) $(MANDIR)/man1/aerc.1
	$(RM) $(MANDIR)/man5/aerc-config.5
	$(RM) $(MANDIR)/man5/aerc-imap.5
	$(RM) $(MANDIR)/man5/aerc-maildir.5
	$(RM) $(MANDIR)/man5/aerc-smtp.5
	$(RM) $(MANDIR)/man7/aerc-tutorial.7
	$(RM) -r $(SHAREDIR)
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index db69aff..2f4f993 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -201,6 +201,7 @@ Note that many of these configuration options are written for you, such as
	See each protocol's man page for more details:

	- *aerc-imap*(5)
	- *aerc-maildir*(5)

	Default: none

@@ -366,7 +367,7 @@ following special keys are supported:

# SEE ALSO

*aerc*(1) *aerc-imap*(5) *aerc-smtp*(5)
*aerc*(1) *aerc-imap*(5) *aerc-smtp*(5) *aerc-maildir*(5)

# AUTHORS

diff --git a/doc/aerc-maildir.5.scd b/doc/aerc-maildir.5.scd
new file mode 100644
index 0000000..5765bf8
--- /dev/null
+++ b/doc/aerc-maildir.5.scd
@@ -0,0 +1,40 @@
aerc-maildir(5)

# NAME

aerc-maildir - maildir configuration for *aerc*(1)

# SYNOPSIS

aerc implements the maildir format.

# CONFIGURATION

Maildir accounts currently are not supported with the :new-account command and
must be added manually to the *aerc-config*(5) file.

The following maildir-specific options are available:

*source*
	maildir://path

	The *source* indicates the path to the directory containing your maildirs
	rather than one maildir specifically.

	The path portion of the URL following _maildir://_ must be either an absolute
	path prefixed by */* or a path relative to your home directory prefixed with
	*~*. For example:

		source = maildir:///home/me/mail

		source = maildir://~/mail

# SEE ALSO

*aerc*(1) *aerc-config*(5) *aerc-smtp*(5)

# AUTHORS

Maintained by Drew DeVault <sir@cmpwn.com>, who is assisted by other open
source contributors. For more information about aerc development, see
https://git.sr.ht/~sircmpwn/aerc.
diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index ba5c5c7..050396f 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -169,7 +169,8 @@ write log messages to that file:

# SEE ALSO

*aerc-config*(5) *aerc-imap*(5) *aerc-smtp*(5) *aerc-tutorial*(7)
*aerc-config*(5) *aerc-imap*(5) *aerc-smtp*(5) *aerc-maildir*(5)
*aerc-tutorial*(7)

# AUTHORS

-- 
2.22.0
Details
Message ID
<BVHDUFCNHSJR.1U1RBLGEVKP88@homura>
In-Reply-To
<20190711134454.80318-1-ben@benburwell.com> (view parent)
Sender timestamp
1562945231
DKIM signature
missing
Download raw message
Thanks for all of your hard work!

To git.sr.ht:~sircmpwn/aerc
   4c7f81d..7a26b48  master -> master
Reply to thread Export thread (mbox)