processing inbox

This commit is contained in:
Erik Winter 2021-01-29 12:29:23 +01:00
parent 1d69a1fbce
commit e1758db30e
12 changed files with 356 additions and 154 deletions

83
cmd/process-inbox/main.go Normal file
View File

@ -0,0 +1,83 @@
package main
import (
"fmt"
"log"
"os"
"git.sr.ht/~ewintr/gte/internal/task"
"git.sr.ht/~ewintr/gte/pkg/mstore"
)
func main() {
config := &mstore.EmailConfiguration{
IMAPURL: os.Getenv("IMAP_URL"),
IMAPUsername: os.Getenv("IMAP_USERNAME"),
IMAPPassword: os.Getenv("IMAP_PASSWORD"),
}
if !config.Valid() {
log.Fatal("please set MAIL_USER, MAIL_PASSWORD, etc environment variables")
}
mailStore, err := mstore.EmailConnect(config)
if err != nil {
log.Fatal(err)
}
defer mailStore.Disconnect()
taskRepo := task.NewRepository(mailStore)
tasks, err := taskRepo.FindAll("INBOX")
if err != nil {
log.Fatal(err)
}
for _, t := range tasks {
fmt.Printf("processing: %s... ", t.Action)
if t.Dirty() {
if err := taskRepo.Update(t); err != nil {
log.Fatal(err)
}
fmt.Printf("updated.")
}
fmt.Printf("\n")
}
/*
folders, err := mailStore.FolderNames()
if err != nil {
log.Fatal(err)
}
for _, f := range folders {
fmt.Println(f)
}
if err := mailStore.Select("Today"); err != nil {
log.Fatal(err)
}
messages, err := mailStore.Messages()
if err != nil {
log.Fatal(err)
}
for _, m := range messages {
fmt.Printf("%d: %s\n", m.Uid, m.Subject)
}
if len(messages) == 0 {
log.Fatal("no messages")
return
}
if err := mailStore.Remove(messages[0].Uid); err != nil {
log.Fatal(err)
}
body := NewBody(`From: todo <process@erikwinter.nl>
Subject: the subject
And here comes the body`)
if err := mailStore.Append("INBOX", imap.Literal(body)); err != nil {
log.Fatal(err)
}
*/
}

View File

@ -1,91 +0,0 @@
package main
import (
"fmt"
"io"
"log"
"os"
"strings"
"git.sr.ht/~ewintr/gte/pkg/mstore"
)
type Body struct {
reader io.Reader
length int
}
func NewBody(msg string) *Body {
return &Body{
reader: strings.NewReader(msg),
length: len([]byte(msg)),
}
}
func (b *Body) Read(p []byte) (int, error) {
return b.reader.Read(p)
}
func (b *Body) Len() int {
return b.length
}
func main() {
config := &mstore.EmailConfiguration{
IMAPURL: os.Getenv("IMAP_URL"),
IMAPUsername: os.Getenv("IMAP_USERNAME"),
IMAPPassword: os.Getenv("IMAP_PASSWORD"),
}
if !config.Valid() {
fmt.Printf("conf: %v\n", config)
log.Fatal("please set MAIL_USER, MAIL_PASSWORD, etc environment variables")
}
//fmt.Printf("conf: %+v\n", config)
mailStore, err := mstore.EmailConnect(config)
if err != nil {
log.Fatal(err)
}
defer mailStore.Disconnect()
/*
folders, err := mailStore.FolderNames()
if err != nil {
log.Fatal(err)
}
for _, f := range folders {
fmt.Println(f)
}
*/
if err := mailStore.Select("Today"); err != nil {
log.Fatal(err)
}
messages, err := mailStore.Messages()
if err != nil {
log.Fatal(err)
}
for _, m := range messages {
fmt.Printf("%d: %s\n", m.Uid, m.Subject)
}
if len(messages) == 0 {
log.Fatal("no messages")
return
}
if err := mailStore.Remove(messages[0].Uid); err != nil {
log.Fatal(err)
}
/*
body := NewBody(`From: todo <process@erikwinter.nl>
Subject: the subject
And here comes the body`)
if err := mailStore.Append("INBOX", imap.Literal(body)); err != nil {
log.Fatal(err)
}
*/
}

4
go.mod
View File

@ -5,6 +5,6 @@ go 1.14
require (
git.sr.ht/~ewintr/go-kit v0.0.0-20201229104230-4d7958f8de04
github.com/emersion/go-imap v1.0.6
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.14.0
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/google/uuid v1.2.0
)

4
go.sum
View File

@ -53,8 +53,6 @@ github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rq
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.14.0 h1:RYW203p+EcPjL8Z/ZpT9lZ6iOc8MG1MQzEx1UKEkXlA=
github.com/emersion/go-smtp v0.14.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -92,6 +90,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=

View File

@ -2,21 +2,9 @@ package task
import "time"
type Date time.Time
func (d *Date) Weekday() Weekday {
return d.Weekday()
}
type Weekday time.Weekday
type Period int
type Task struct {
Action string
Due Date
}
type Recurrer interface {
FirstAfter(date Date) Date
}

61
internal/task/repo.go Normal file
View File

@ -0,0 +1,61 @@
package task
import (
"errors"
"fmt"
"git.sr.ht/~ewintr/gte/pkg/mstore"
)
var (
ErrMStoreError = errors.New("mstore gave error response")
)
type TaskRepo struct {
mstore mstore.MStorer
}
func NewRepository(ms mstore.MStorer) *TaskRepo {
return &TaskRepo{
mstore: ms,
}
}
func (tr *TaskRepo) FindAll(folder string) ([]*Task, error) {
msgs, err := tr.mstore.Messages(folder)
if err != nil {
return []*Task{}, err
}
tasks := []*Task{}
for _, msg := range msgs {
if msg.Valid() {
tasks = append(tasks, NewFromMessage(msg))
}
}
return tasks, nil
}
func (tr *TaskRepo) Update(t *Task) error {
if !t.Current {
return ErrOutdatedTask
}
if !t.Dirty() {
return nil
}
// add new
if err := tr.mstore.Add(t.Folder, t.Subject(), t.Body()); err != nil {
return fmt.Errorf("%w: %s", ErrMStoreError, err)
}
// remove old
if err := tr.mstore.Remove(t.Message); err != nil {
return fmt.Errorf("%w: %s", ErrMStoreError, err)
}
t.Current = false
return nil
}

115
internal/task/task.go Normal file
View File

@ -0,0 +1,115 @@
package task
import (
"errors"
"fmt"
"strings"
"time"
"git.sr.ht/~ewintr/gte/pkg/mstore"
"github.com/google/uuid"
)
var (
ErrOutdatedTask = errors.New("task is outdated")
)
type Date time.Time
func (d *Date) Weekday() Weekday {
return d.Weekday()
}
type Task struct {
Id string
Folder string
Action string
Due Date
Message *mstore.Message
Current bool
Simplified bool
}
func NewFromMessage(msg *mstore.Message) *Task {
fmt.Println(msg.Subject)
id := FieldFromBody("id", msg.Body)
if id == "" {
id = uuid.New().String()
}
action := FieldFromBody("action", msg.Body)
if action == "" {
action = FieldFromSubject("action", msg.Subject)
}
folder := msg.Folder
if folder == "INBOX" {
folder = "New"
}
return &Task{
Id: id,
Action: action,
Folder: folder,
Message: msg,
Current: true,
Simplified: false,
}
}
// Dirty checks if the task has unsaved changes
func (t *Task) Dirty() bool {
mBody := t.Message.Body
mSubject := t.Message.Subject
if t.Id != FieldFromBody("id", mBody) {
return true
}
if t.Folder != t.Message.Folder {
return true
}
if t.Action != FieldFromBody("action", mBody) {
return true
}
if t.Action != FieldFromSubject("action", mSubject) {
return true
}
return false
}
func (t *Task) Subject() string {
return t.Action
}
func (t *Task) Body() string {
body := fmt.Sprintf("id: %s\n", t.Id)
body += fmt.Sprintf("action: %s\n", t.Action)
return body
}
func FieldFromBody(field, body string) string {
lines := strings.Split(body, "\n")
for _, line := range lines {
parts := strings.SplitN(line, ":", 2)
if len(parts) < 2 {
continue
}
if strings.ToLower(parts[0]) == field {
return strings.TrimSpace(parts[1])
}
}
return ""
}
func FieldFromSubject(field, subject string) string {
if field == "action" {
return strings.ToLower(subject)
}
return ""
}

View File

@ -2,12 +2,35 @@ package mstore
import (
"fmt"
"io"
"strings"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
)
type Body struct {
reader io.Reader
length int
}
func NewBody(msg string) *Body {
return &Body{
reader: strings.NewReader(msg),
length: len([]byte(msg)),
}
}
func (b *Body) Read(p []byte) (int, error) {
return b.reader.Read(p)
}
func (b *Body) Len() int {
return b.length
}
type EmailConfiguration struct {
IMAPURL string
IMAPUsername string
@ -48,7 +71,7 @@ func (es *Email) Disconnect() {
es.imap.Logout()
}
func (es *Email) FolderNames() ([]string, error) {
func (es *Email) Folders() ([]string, error) {
boxes, done := make(chan *imap.MailboxInfo), make(chan error)
go func() {
done <- es.imap.List("", "*", boxes)
@ -66,21 +89,20 @@ func (es *Email) FolderNames() ([]string, error) {
return folders, nil
}
func (es *Email) Select(folder string) error {
func (es *Email) selectFolder(folder string) error {
status, err := es.imap.Select(folder, false)
if err != nil {
return err
}
fmt.Printf("status: %+v\n", status)
es.mboxStatus = status
return nil
}
func (es *Email) Messages() ([]*Message, error) {
if es.mboxStatus == nil {
return []*Message{}, fmt.Errorf("no mailbox selected")
func (es *Email) Messages(folder string) ([]*Message, error) {
if err := es.selectFolder(folder); err != nil {
return []*Message{}, err
}
if es.mboxStatus.Messages == 0 {
@ -97,9 +119,9 @@ func (es *Email) Messages() ([]*Message, error) {
messages := []*Message{}
for m := range imsg {
//fmt.Printf("%+v\n", m)
messages = append(messages, &Message{
Uid: m.Uid,
Folder: folder,
Subject: m.Envelope.Subject,
})
}
@ -111,21 +133,29 @@ func (es *Email) Messages() ([]*Message, error) {
return messages, nil
}
func (es *Email) Append(mbox string, msg imap.Literal) error {
return es.imap.Append(mbox, nil, time.Time{}, msg)
func (es *Email) Add(folder, subject, body string) error {
msgStr := fmt.Sprintf(`From: todo <process@erikwinter.nl>
Subject: %s
%s`, subject, body)
msg := NewBody(msgStr)
return es.imap.Append(folder, nil, time.Time{}, imap.Literal(msg))
}
func (es *Email) Remove(uid uint32) error {
if uid == 0 {
return fmt.Errorf("invalid uid: %d", uid)
func (es *Email) Remove(msg *Message) error {
if !msg.Valid() {
return ErrInvalidMessage
}
if es.mboxStatus == nil {
return fmt.Errorf("no mailbox selected")
if err := es.selectFolder(msg.Folder); err != nil {
return err
}
// set deleted flag
seqset := new(imap.SeqSet)
seqset.AddRange(uid, uid)
seqset.AddRange(msg.Uid, msg.Uid)
storeItem := imap.FormatFlagsOp(imap.SetFlags, true)
err := es.imap.UidStore(seqset, storeItem, imap.FormatStringList([]string{imap.DeletedFlag}), nil)
if err != nil {

View File

@ -37,7 +37,7 @@ func NewMemory(folders []string) (*Memory, error) {
func (mem *Memory) Folders() ([]string, error) {
folders := []string{}
for f, _ := range mem.messages {
for f := range mem.messages {
folders = append(folders, f)
}
@ -56,15 +56,15 @@ func (mem *Memory) Select(folder string) error {
return nil
}
func (mem *Memory) Add(subject, body string) error {
func (mem *Memory) Add(folder, subject, body string) error {
if subject == "" {
return ErrInvalidMessage
}
if mem.Selected == "" {
return ErrNoFolderSelected
if _, ok := mem.messages[folder]; !ok {
return ErrFolderDoesNotExist
}
mem.messages[mem.Selected] = append(mem.messages[mem.Selected], &Message{
mem.messages[folder] = append(mem.messages[folder], &Message{
Uid: mem.nextUid,
Subject: subject,
Body: body,

View File

@ -101,12 +101,6 @@ func TestMemorySelect(t *testing.T) {
func TestMemoryAdd(t *testing.T) {
folder := "folder"
t.Run("no folder selected", func(t *testing.T) {
mem, err := mstore.NewMemory([]string{folder})
test.OK(t, err)
test.Equals(t, mstore.ErrNoFolderSelected, mem.Add("subject", ""))
})
for _, tc := range []struct {
name string
subject string
@ -124,8 +118,7 @@ func TestMemoryAdd(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
mem, err := mstore.NewMemory([]string{folder})
test.OK(t, err)
mem.Select(folder)
test.Equals(t, tc.exp, mem.Add(tc.subject, ""))
test.Equals(t, tc.exp, mem.Add(folder, tc.subject, ""))
})
}
}
@ -168,7 +161,6 @@ func TestMemoryMessages(t *testing.T) {
test.OK(t, err)
expMessages := []*mstore.Message{}
test.OK(t, mem.Select(folderA))
for i := 1; i <= tc.amount; i++ {
m := &mstore.Message{
Uid: uint32(i),
@ -178,7 +170,7 @@ func TestMemoryMessages(t *testing.T) {
if tc.folder == folderA {
expMessages = append(expMessages, m)
}
test.OK(t, mem.Add(m.Subject, m.Body))
test.OK(t, mem.Add(folderA, m.Subject, m.Body))
}
test.OK(t, mem.Select(tc.folder))
@ -200,9 +192,8 @@ func TestMemoryRemove(t *testing.T) {
mem, err := mstore.NewMemory([]string{folderA, folderB})
test.OK(t, err)
test.OK(t, mem.Select(folderA))
for i := 1; i <= 3; i++ {
test.OK(t, mem.Add(fmt.Sprintf("subject-%d", i), ""))
test.OK(t, mem.Add(folderA, fmt.Sprintf("subject-%d", i), ""))
}
for _, tc := range []struct {
name string

View File

@ -12,18 +12,18 @@ var (
type Message struct {
Uid uint32
Folder string
Subject string
Body string
}
func (m *Message) Valid() bool {
return m.Uid != 0 && m.Subject != ""
return m.Uid != 0 && m.Subject != "" && m.Folder != ""
}
type MStorer interface {
Folders() ([]string, error)
Select(folder string) error
Messages() ([]*Message, error)
Add(message *Message) error
Remove(uid uint32) error
Messages(folder string) ([]*Message, error)
Add(folder, subject, body string) error
Remove(msg *Message) error
}

View File

@ -18,22 +18,47 @@ func TestMessageValid(t *testing.T) {
message: &mstore.Message{},
},
{
name: "no uid",
message: &mstore.Message{Subject: "subject", Body: "body"},
name: "no uid",
message: &mstore.Message{
Subject: "subject",
Folder: "folder",
Body: "body",
},
},
{
name: "no subject",
message: &mstore.Message{Uid: 1, Body: "body"},
name: "no folder",
message: &mstore.Message{
Uid: 1,
Subject: "subject",
Body: "body",
},
},
{
name: "no body",
message: &mstore.Message{Uid: 1, Subject: "subject"},
exp: true,
name: "no subject",
message: &mstore.Message{
Uid: 1,
Folder: "folder",
Body: "body",
},
},
{
name: "all present",
message: &mstore.Message{Uid: 1, Subject: "subject", Body: "body"},
exp: true,
name: "no body",
message: &mstore.Message{
Uid: 1,
Folder: "folder",
Subject: "subject",
},
exp: true,
},
{
name: "all present",
message: &mstore.Message{
Uid: 1,
Folder: "folder",
Subject: "subject",
Body: "body",
},
exp: true,
},
} {
t.Run(tc.name, func(t *testing.T) {