aerc/worker/notmuch/message.go
Reto Brunner a2c5233f71 Notmuch: use adhoc write connection.
Notmuch only allows a single write connection, all other clients trying to
modify the db block. Hence we should only open one when we actually need it.

Apparently we also need to refresh the RO DB connection upon modification,
else we get stale message tag results
2019-08-26 09:56:44 +09:00

191 lines
3.8 KiB
Go

//+build notmuch
package notmuch
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"git.sr.ht/~sircmpwn/aerc/models"
"git.sr.ht/~sircmpwn/aerc/worker/lib"
"github.com/emersion/go-message"
_ "github.com/emersion/go-message/charset"
notmuch "github.com/zenhack/go.notmuch"
)
type Message struct {
uid uint32
key string
msg *notmuch.Message
rwDB func() (*notmuch.DB, error) // used to open a db for writing
refresh func(*Message) error // called after msg modification
}
// NewReader reads a message into memory and returns an io.Reader for it.
func (m *Message) NewReader() (io.Reader, error) {
f, err := os.Open(m.msg.Filename())
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
}
// MessageInfo populates a models.MessageInfo struct for the message.
func (m *Message) MessageInfo() (*models.MessageInfo, error) {
return lib.MessageInfo(m)
}
// 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 := os.Open(m.msg.Filename())
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 lib.FetchEntityPartReader(msg, requestedParts)
}
// MarkRead either adds or removes the maildir.FlagSeen flag from the message.
func (m *Message) MarkRead(seen bool) error {
haveUnread := false
for _, t := range m.tags() {
if t == "unread" {
haveUnread = true
break
}
}
if (haveUnread && !seen) || (!haveUnread && seen) {
// we already have the desired state
return nil
}
if haveUnread {
err := m.RemoveTag("unread")
if err != nil {
return err
}
return nil
}
err := m.AddTag("unread")
if err != nil {
return err
}
return nil
}
// tags returns the notmuch tags of a message
func (m *Message) tags() []string {
ts := m.msg.Tags()
var tags []string
var tag *notmuch.Tag
for ts.Next(&tag) {
tags = append(tags, tag.Value)
}
return tags
}
func (m *Message) modify(cb func(*notmuch.Message) error) error {
db, err := m.rwDB()
if err != nil {
return err
}
defer db.Close()
msg, err := db.FindMessage(m.key)
if err != nil {
return err
}
err = cb(msg)
if err != nil {
return err
}
// we need to explicitly close here, else we don't commit
dcerr := db.Close()
if dcerr != nil && err == nil {
err = dcerr
}
// next we need to refresh the notmuch msg, else we serve stale tags
rerr := m.refresh(m)
if rerr != nil && err == nil {
err = rerr
}
return err
}
func (m *Message) AddTag(tag string) error {
err := m.modify(func(msg *notmuch.Message) error {
return msg.AddTag(tag)
})
return err
}
func (m *Message) AddTags(tags []string) error {
err := m.modify(func(msg *notmuch.Message) error {
ierr := msg.Atomic(func(msg *notmuch.Message) {
for _, t := range tags {
msg.AddTag(t)
}
})
return ierr
})
return err
}
func (m *Message) RemoveTag(tag string) error {
err := m.modify(func(msg *notmuch.Message) error {
return msg.RemoveTag(tag)
})
return err
}
func (m *Message) RemoveTags(tags []string) error {
err := m.modify(func(msg *notmuch.Message) error {
ierr := msg.Atomic(func(msg *notmuch.Message) {
for _, t := range tags {
msg.RemoveTag(t)
}
})
return ierr
})
return err
}
func (m *Message) ModelFlags() ([]models.Flag, error) {
var flags []models.Flag
seen := true
for _, tag := range m.tags() {
switch tag {
case "replied":
flags = append(flags, models.AnsweredFlag)
case "flagged":
flags = append(flags, models.FlaggedFlag)
case "unread":
seen = false
default:
continue
}
}
if seen {
flags = append(flags, models.SeenFlag)
}
return flags, nil
}
func (m *Message) UID() uint32 {
return m.uid
}