imap daemon

This commit is contained in:
Erik Winter 2021-05-13 08:15:14 +02:00
parent 5636958dc9
commit c031bef006
17 changed files with 753 additions and 120 deletions

79
cmd/daemon/service.go Normal file
View File

@ -0,0 +1,79 @@
package main
import (
"os"
"os/signal"
"time"
"git.ewintr.nl/go-kit/log"
"git.ewintr.nl/gte/internal/process"
"git.ewintr.nl/gte/internal/task"
"git.ewintr.nl/gte/pkg/msend"
"git.ewintr.nl/gte/pkg/mstore"
)
func main() {
logger := log.New(os.Stdout)
logger.Info("started")
msgStore := mstore.NewIMAP(&mstore.IMAPConfig{
IMAPURL: os.Getenv("IMAP_URL"),
IMAPUsername: os.Getenv("IMAP_USERNAME"),
IMAPPassword: os.Getenv("IMAP_PASSWORD"),
})
msgSender := msend.NewSSLSMTP(&msend.SSLSMTPConfig{
URL: os.Getenv("SMTP_URL"),
Username: os.Getenv("SMTP_USERNAME"),
Password: os.Getenv("SMTP_PASSWORD"),
From: os.Getenv("SMTP_FROM"),
To: os.Getenv("SMTP_TO"),
})
repo := task.NewRepository(msgStore)
disp := task.NewDispatcher(msgSender)
inboxProc := process.NewInbox(repo)
recurProc := process.NewRecur(repo, disp, 6)
go Run(inboxProc, recurProc, logger)
done := make(chan os.Signal)
signal.Notify(done, os.Interrupt)
<-done
logger.Info("stopped")
}
func Run(inboxProc *process.Inbox, recurProc *process.Recur, logger log.Logger) {
logger = logger.WithField("func", "run")
inboxTicker := time.NewTicker(10 * time.Second)
recurTicker := time.NewTicker(time.Hour)
oldToday := task.Today
for {
select {
case <-inboxTicker.C:
result, err := inboxProc.Process()
if err != nil {
logger.WithErr(err).Error("failed processing inbox")
continue
}
logger.WithField("count", result.Count).Info("finished processing inbox")
case <-recurTicker.C:
year, month, day := time.Now().Date()
newToday := task.NewDate(year, int(month), day)
if oldToday.Equal(newToday) {
continue
}
oldToday = newToday
result, err := recurProc.Process()
if err != nil {
logger.WithErr(err).Error("failed generating recurring tasks")
continue
}
logger.WithField("count", result.Count).Info("finished generating recurring tasks")
}
}
}

View File

@ -1,51 +1,52 @@
package main package main
import ( import (
"log"
"os" "os"
"strconv" "strconv"
"git.ewintr.nl/go-kit/log"
"git.ewintr.nl/gte/internal/process"
"git.ewintr.nl/gte/internal/task" "git.ewintr.nl/gte/internal/task"
"git.ewintr.nl/gte/pkg/msend"
"git.ewintr.nl/gte/pkg/mstore" "git.ewintr.nl/gte/pkg/mstore"
) )
func main() { func main() {
config := &mstore.ImapConfiguration{ logger := log.New(os.Stdout).WithField("cmd", "generate-recurring")
ImapUrl: os.Getenv("IMAP_URL"), IMAPConfig := &mstore.IMAPConfig{
ImapUsername: os.Getenv("IMAP_USERNAME"), IMAPURL: os.Getenv("IMAP_URL"),
ImapPassword: os.Getenv("IMAP_PASSWORD"), IMAPUsername: os.Getenv("IMAP_USERNAME"),
IMAPPassword: os.Getenv("IMAP_PASSWORD"),
} }
if !config.Valid() { msgStore := mstore.NewIMAP(IMAPConfig)
log.Fatal("please set IMAP_USER, IMAP_PASSWORD, etc environment variables")
SMTPConfig := &msend.SSLSMTPConfig{
URL: os.Getenv("SMTP_URL"),
Username: os.Getenv("SMTP_USERNAME"),
Password: os.Getenv("SMTP_PASSWORD"),
From: os.Getenv("SMTP_FROM"),
To: os.Getenv("SMTP_TO"),
} }
if !SMTPConfig.Valid() {
logger.Error("please set SMTP_URL, SMTP_USERNAME, etc environment variables")
os.Exit(1)
}
mailSend := msend.NewSSLSMTP(SMTPConfig)
daysAhead, err := strconv.Atoi(os.Getenv("GTE_DAYS_AHEAD")) daysAhead, err := strconv.Atoi(os.Getenv("GTE_DAYS_AHEAD"))
if err != nil { if err != nil {
daysAhead = 0 daysAhead = 0
} }
mailStore, err := mstore.ImapConnect(config) taskRepo := task.NewRepository(msgStore)
taskDisp := task.NewDispatcher(mailSend)
recur := process.NewRecur(taskRepo, taskDisp, daysAhead)
result, err := recur.Process()
if err != nil { if err != nil {
log.Fatal(err) logger.WithErr(err).Error("unable to process recurring")
} os.Exit(1)
defer mailStore.Disconnect()
taskRepo := task.NewRepository(mailStore)
tasks, err := taskRepo.FindAll(task.FOLDER_RECURRING)
if err != nil {
log.Fatal(err)
}
rDate := task.Today.AddDays(daysAhead)
for _, t := range tasks {
if t.RecursOn(rDate) {
subject, body, err := t.CreateDueMessage(rDate)
if err != nil {
log.Fatal(err)
}
if err := mailStore.Add(task.FOLDER_PLANNED, subject, body); err != nil {
log.Fatal(err)
}
}
} }
logger.WithField("count", result.Count).Info("finished generating recurring tasks")
} }

View File

@ -1,46 +1,28 @@
package main package main
import ( import (
"log"
"os" "os"
"git.ewintr.nl/go-kit/log"
"git.ewintr.nl/gte/internal/process"
"git.ewintr.nl/gte/internal/task" "git.ewintr.nl/gte/internal/task"
"git.ewintr.nl/gte/pkg/mstore" "git.ewintr.nl/gte/pkg/mstore"
) )
func main() { func main() {
config := &mstore.ImapConfiguration{ logger := log.New(os.Stdout).WithField("cmd", "process-inbox")
ImapUrl: os.Getenv("IMAP_URL"), config := &mstore.IMAPConfig{
ImapUsername: os.Getenv("IMAP_USERNAME"), IMAPURL: os.Getenv("IMAP_URL"),
ImapPassword: os.Getenv("IMAP_PASSWORD"), IMAPUsername: os.Getenv("IMAP_USERNAME"),
} IMAPPassword: os.Getenv("IMAP_PASSWORD"),
if !config.Valid() {
log.Fatal("please set IMAP_USER, IMAP_PASSWORD, etc environment variables")
} }
msgStore := mstore.NewIMAP(config)
mailStore, err := mstore.ImapConnect(config) inboxProcessor := process.NewInbox(task.NewRepository(msgStore))
result, err := inboxProcessor.Process()
if err != nil { if err != nil {
log.Fatal(err) logger.WithErr(err).Error("unable to process inbox")
} os.Exit(1)
defer mailStore.Disconnect()
taskRepo := task.NewRepository(mailStore)
tasks, err := taskRepo.FindAll(task.FOLDER_INBOX)
if err != nil {
log.Fatal(err)
}
var cleanupNeeded bool
for _, t := range tasks {
if t.Dirty {
if err := taskRepo.Update(t); err != nil {
log.Fatal(err)
}
cleanupNeeded = true
}
}
if cleanupNeeded {
if err := taskRepo.CleanUp(); err != nil {
log.Fatal(err)
}
} }
logger.WithField("count", result.Count).Info("finished processing inbox")
} }

2
go.mod
View File

@ -3,7 +3,7 @@ module git.ewintr.nl/gte
go 1.14 go 1.14
require ( require (
git.ewintr.nl/go-kit v0.0.0-20210509123609-19e474005502 git.ewintr.nl/go-kit v0.0.0-20210513091124-da7006c2c242
github.com/emersion/go-imap v1.1.0 github.com/emersion/go-imap v1.1.0
github.com/emersion/go-message v0.14.1 github.com/emersion/go-message v0.14.1
github.com/google/uuid v1.2.0 github.com/google/uuid v1.2.0

12
go.sum
View File

@ -1,7 +1,9 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.ewintr.nl/go-kit v0.0.0-20210509123609-19e474005502 h1:h3PYDz6uWAz2mBPOFShhkZv7SapfLWXcBDsmHqFCq5w= git.ewintr.nl/go-kit v0.0.0-20210513084754-6c0524f3de86 h1:jVP4muIBqQ5poAuOlDgW/PhYscSnRHdqEqX1WfAK++A=
git.ewintr.nl/go-kit v0.0.0-20210509123609-19e474005502/go.mod h1:eYANz1nepfc6lHxa9UcNZFvLaezBrihttH/Pdc5+0Vk= git.ewintr.nl/go-kit v0.0.0-20210513084754-6c0524f3de86/go.mod h1:eYANz1nepfc6lHxa9UcNZFvLaezBrihttH/Pdc5+0Vk=
git.ewintr.nl/go-kit v0.0.0-20210513091124-da7006c2c242 h1:9UIbgqTOIot2rAXqVWhd0Q9MyWru1kIzr52HPWpCCTM=
git.ewintr.nl/go-kit v0.0.0-20210513091124-da7006c2c242/go.mod h1:eYANz1nepfc6lHxa9UcNZFvLaezBrihttH/Pdc5+0Vk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
@ -65,11 +67,14 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -224,6 +229,7 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@ -237,7 +243,6 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -310,6 +315,7 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f h1:68K/z8GLUxV76xGSqwTWw2gyk/jwn79LUL43rES2g8o=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

52
internal/process/inbox.go Normal file
View File

@ -0,0 +1,52 @@
package process
import (
"errors"
"fmt"
"git.ewintr.nl/gte/internal/task"
)
var (
ErrInboxProcess = errors.New("could not process inbox")
)
type Inbox struct {
taskRepo *task.TaskRepo
}
type InboxResult struct {
Count int
}
func NewInbox(repo *task.TaskRepo) *Inbox {
return &Inbox{
taskRepo: repo,
}
}
func (inbox *Inbox) Process() (*InboxResult, error) {
tasks, err := inbox.taskRepo.FindAll(task.FOLDER_INBOX)
if err != nil {
return &InboxResult{}, fmt.Errorf("%w: %v", ErrInboxProcess, err)
}
var cleanupNeeded bool
for _, t := range tasks {
if t.Dirty {
if err := inbox.taskRepo.Update(t); err != nil {
return &InboxResult{}, fmt.Errorf("%w: %v", ErrInboxProcess, err)
}
cleanupNeeded = true
}
}
if cleanupNeeded {
if err := inbox.taskRepo.CleanUp(); err != nil {
return &InboxResult{}, fmt.Errorf("%w: %v", ErrInboxProcess, err)
}
}
return &InboxResult{
Count: len(tasks),
}, nil
}

View File

@ -0,0 +1,134 @@
package process_test
import (
"testing"
"git.ewintr.nl/go-kit/test"
"git.ewintr.nl/gte/internal/process"
"git.ewintr.nl/gte/internal/task"
"git.ewintr.nl/gte/pkg/mstore"
)
func TestInboxProcess(t *testing.T) {
for _, tc := range []struct {
name string
messages map[string][]*mstore.Message
expResult *process.InboxResult
expMsgs map[string][]*mstore.Message
}{
{
name: "empty",
messages: map[string][]*mstore.Message{
task.FOLDER_INBOX: {},
},
expResult: &process.InboxResult{},
expMsgs: map[string][]*mstore.Message{
task.FOLDER_INBOX: {},
},
},
{
name: "all flavors",
messages: map[string][]*mstore.Message{
task.FOLDER_INBOX: {
{
Subject: "to new",
},
{
Subject: "to recurring",
Body: "recur: 2021-05-14, daily\nid: xxx-xxx\nversion: 1",
},
{
Subject: "to planned",
Body: "due: 2021-05-14\nid: xxx-xxx\nversion: 1",
},
{
Subject: "to unplanned",
Body: "id: xxx-xxx\nversion: 1",
},
},
},
expResult: &process.InboxResult{
Count: 4,
},
expMsgs: map[string][]*mstore.Message{
task.FOLDER_INBOX: {},
task.FOLDER_NEW: {{Subject: "to new"}},
task.FOLDER_RECURRING: {{Subject: "to recurring"}},
task.FOLDER_PLANNED: {{Subject: "2021-05-14 (friday) - to planned"}},
task.FOLDER_UNPLANNED: {{Subject: "to unplanned"}},
},
},
{
name: "cleanup",
messages: map[string][]*mstore.Message{
task.FOLDER_INBOX: {{
Subject: "new version",
Body: "id: xxx-xxx\nversion: 3",
}},
task.FOLDER_UNPLANNED: {{
Subject: "old version",
Body: "id: xxx-xxx\nversion: 3",
}},
},
expResult: &process.InboxResult{
Count: 1,
},
expMsgs: map[string][]*mstore.Message{
task.FOLDER_INBOX: {},
task.FOLDER_UNPLANNED: {{Subject: "new version"}},
},
},
{
name: "cleanup version conflict",
messages: map[string][]*mstore.Message{
task.FOLDER_INBOX: {{
Subject: "new version",
Body: "id: xxx-xxx\nversion: 3",
}},
task.FOLDER_UNPLANNED: {{
Subject: "not really old version",
Body: "id: xxx-xxx\nversion: 5",
}},
},
expResult: &process.InboxResult{
Count: 1,
},
expMsgs: map[string][]*mstore.Message{
task.FOLDER_INBOX: {},
task.FOLDER_UNPLANNED: {{Subject: "not really old version"}},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
mstorer, err := mstore.NewMemory([]string{
task.FOLDER_INBOX,
task.FOLDER_NEW,
task.FOLDER_RECURRING,
task.FOLDER_PLANNED,
task.FOLDER_UNPLANNED,
})
test.OK(t, err)
for folder, messages := range tc.messages {
for _, m := range messages {
test.OK(t, mstorer.Add(folder, m.Subject, m.Body))
}
}
inboxProc := process.NewInbox(task.NewRepository(mstorer))
actResult, err := inboxProc.Process()
test.OK(t, err)
test.Equals(t, tc.expResult, actResult)
for folder, expMessages := range tc.expMsgs {
actMessages, err := mstorer.Messages(folder)
test.OK(t, err)
test.Equals(t, len(expMessages), len(actMessages))
if len(expMessages) == 0 {
continue
}
test.Equals(t, expMessages[0].Subject, actMessages[0].Subject)
}
})
}
}

56
internal/process/recur.go Normal file
View File

@ -0,0 +1,56 @@
package process
import (
"errors"
"fmt"
"git.ewintr.nl/gte/internal/task"
)
var (
ErrRecurProcess = errors.New("could not generate tasks from recurrer")
)
type Recur struct {
taskRepo *task.TaskRepo
taskDispatcher *task.Dispatcher
daysAhead int
}
type RecurResult struct {
Count int
}
func NewRecur(repo *task.TaskRepo, disp *task.Dispatcher, daysAhead int) *Recur {
return &Recur{
taskRepo: repo,
taskDispatcher: disp,
daysAhead: daysAhead,
}
}
func (recur *Recur) Process() (*RecurResult, error) {
tasks, err := recur.taskRepo.FindAll(task.FOLDER_RECURRING)
if err != nil {
return &RecurResult{}, fmt.Errorf("%w: %v", ErrRecurProcess, err)
}
rDate := task.Today.AddDays(recur.daysAhead)
var count int
for _, t := range tasks {
if t.RecursOn(rDate) {
newTask, err := t.GenerateFromRecurrer(rDate)
if err != nil {
return &RecurResult{}, fmt.Errorf("%w: %v", ErrRecurProcess, err)
}
if err := recur.taskDispatcher.Dispatch(newTask); err != nil {
return &RecurResult{}, fmt.Errorf("%w: %v", ErrRecurProcess, err)
}
count++
}
}
return &RecurResult{
Count: count,
}, nil
}

View File

@ -0,0 +1,69 @@
package process_test
import (
"testing"
"git.ewintr.nl/go-kit/test"
"git.ewintr.nl/gte/internal/process"
"git.ewintr.nl/gte/internal/task"
"git.ewintr.nl/gte/pkg/msend"
"git.ewintr.nl/gte/pkg/mstore"
)
func TestRecurProcess(t *testing.T) {
task.Today = task.NewDate(2021, 5, 14)
for _, tc := range []struct {
name string
recurMsgs []*mstore.Message
expResult *process.RecurResult
expMsgs []*msend.Message
}{
{
name: "empty",
expResult: &process.RecurResult{},
expMsgs: []*msend.Message{},
},
{
name: "one of two recurring",
recurMsgs: []*mstore.Message{
{
Subject: "not recurring",
Body: "recur: 2021-05-20, daily\nid: xxx-xxx\nversion: 1",
},
{
Subject: "recurring",
Body: "recur: 2021-05-10, daily\nid: xxx-xxx\nversion: 1",
},
},
expResult: &process.RecurResult{
Count: 1,
},
expMsgs: []*msend.Message{
{Subject: "2021-05-15 (saturday) - recurring"},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
mstorer, err := mstore.NewMemory([]string{
task.FOLDER_INBOX,
task.FOLDER_NEW,
task.FOLDER_RECURRING,
task.FOLDER_PLANNED,
task.FOLDER_UNPLANNED,
})
test.OK(t, err)
for _, m := range tc.recurMsgs {
test.OK(t, mstorer.Add(task.FOLDER_RECURRING, m.Subject, m.Body))
}
msender := msend.NewMemory()
recurProc := process.NewRecur(task.NewRepository(mstorer), task.NewDispatcher(msender), 1)
actResult, err := recurProc.Process()
test.OK(t, err)
test.Equals(t, tc.expResult, actResult)
for i, expMsg := range tc.expMsgs {
test.Equals(t, expMsg.Subject, msender.Messages[i].Subject)
}
})
}
}

20
internal/task/dispatch.go Normal file
View File

@ -0,0 +1,20 @@
package task
import "git.ewintr.nl/gte/pkg/msend"
type Dispatcher struct {
msender msend.MSender
}
func NewDispatcher(msender msend.MSender) *Dispatcher {
return &Dispatcher{
msender: msender,
}
}
func (d *Dispatcher) Dispatch(t *Task) error {
return d.msender.Send(&msend.Message{
Subject: t.FormatSubject(),
Body: t.FormatBody(),
})
}

View File

@ -52,8 +52,8 @@ func (tr *TaskRepo) Update(t *Task) error {
} }
// add new // add new
if err := tr.mstore.Add(t.Folder, t.FormatSubject(), t.FormatBody()); err != nil { if err := tr.Add(t); err != nil {
return fmt.Errorf("%w: %s", ErrMStoreError, err) return err
} }
// remove old // remove old
@ -66,6 +66,18 @@ func (tr *TaskRepo) Update(t *Task) error {
return nil return nil
} }
func (tr *TaskRepo) Add(t *Task) error {
if t == nil {
return ErrInvalidTask
}
if err := tr.mstore.Add(t.Folder, t.FormatSubject(), t.FormatBody()); err != nil {
return fmt.Errorf("%w: %v", ErrMStoreError, err)
}
return nil
}
// Cleanup removes older versions of tasks // Cleanup removes older versions of tasks
func (tr *TaskRepo) CleanUp() error { func (tr *TaskRepo) CleanUp() error {
// loop through folders, get all task version info // loop through folders, get all task version info

View File

@ -261,20 +261,18 @@ func (t *Task) RecursOn(date Date) bool {
return t.Recur.RecursOn(date) return t.Recur.RecursOn(date)
} }
func (t *Task) CreateDueMessage(date Date) (string, string, error) { func (t *Task) GenerateFromRecurrer(date Date) (*Task, error) {
if !t.IsRecurrer() { if !t.IsRecurrer() || !t.RecursOn(date) {
return "", "", ErrTaskIsNotRecurring return &Task{}, ErrTaskIsNotRecurring
} }
tempTask := &Task{ return &Task{
Id: uuid.New().String(), Id: uuid.New().String(),
Version: 1, Version: 1,
Action: t.Action, Action: t.Action,
Project: t.Project, Project: t.Project,
Due: date, Due: date,
} }, nil
return tempTask.FormatSubject(), tempTask.FormatBody(), nil
} }
func FieldFromBody(field, body string) (string, bool) { func FieldFromBody(field, body string) (string, bool) {

17
pkg/msend/memory.go Normal file
View File

@ -0,0 +1,17 @@
package msend
type Memory struct {
Messages []*Message
}
func NewMemory() *Memory {
return &Memory{
Messages: []*Message{},
}
}
func (mem *Memory) Send(msg *Message) error {
mem.Messages = append(mem.Messages, msg)
return nil
}

21
pkg/msend/memory_test.go Normal file
View File

@ -0,0 +1,21 @@
package msend_test
import (
"testing"
"git.ewintr.nl/go-kit/test"
"git.ewintr.nl/gte/pkg/msend"
)
func TestMemorySend(t *testing.T) {
mem := msend.NewMemory()
test.Equals(t, []*msend.Message{}, mem.Messages)
msg1 := &msend.Message{Subject: "sub1", Body: "body1"}
test.OK(t, mem.Send(msg1))
test.Equals(t, []*msend.Message{msg1}, mem.Messages)
msg2 := &msend.Message{Subject: "sub2", Body: "body2"}
test.OK(t, mem.Send(msg2))
test.Equals(t, []*msend.Message{msg1, msg2}, mem.Messages)
}

16
pkg/msend/msend.go Normal file
View File

@ -0,0 +1,16 @@
package msend
import "errors"
var (
ErrSendFail = errors.New("could not send message")
)
type Message struct {
Subject string
Body string
}
type MSender interface {
Send(msg *Message) error
}

127
pkg/msend/smtp.go Normal file
View File

@ -0,0 +1,127 @@
package msend
import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/mail"
"net/smtp"
)
var (
ErrSMTPInvalidConfig = errors.New("invalid smtp configuration")
ErrSMTPConnectionFailed = errors.New("connection to smtp server failed")
ErrSendMessageFailed = errors.New("could not send message")
)
type SSLSMTPConfig struct {
URL string
Username string
Password string
From string
To string
}
func (ssc *SSLSMTPConfig) Valid() bool {
if _, _, err := net.SplitHostPort(ssc.URL); err != nil {
return false
}
return ssc.Username != "" && ssc.Password != "" && ssc.To != "" && ssc.From != ""
}
type SSLSMTP struct {
config *SSLSMTPConfig
client *smtp.Client
connected bool
}
func NewSSLSMTP(config *SSLSMTPConfig) *SSLSMTP {
return &SSLSMTP{
config: config,
}
}
func (s *SSLSMTP) Connect() error {
if !s.config.Valid() {
return ErrSMTPInvalidConfig
}
host, _, _ := net.SplitHostPort(s.config.URL)
auth := smtp.PlainAuth("", s.config.Username, s.config.Password, host)
conn, err := tls.Dial("tcp", s.config.URL, &tls.Config{ServerName: host})
if err != nil {
return fmt.Errorf("%w: %v", ErrSMTPConnectionFailed, err)
}
client, err := smtp.NewClient(conn, host)
if err != nil {
return fmt.Errorf("%w: %v", ErrSMTPConnectionFailed, err)
}
if err := client.Auth(auth); err != nil {
return fmt.Errorf("%w: %v", ErrSMTPConnectionFailed, err)
}
s.client = client
s.connected = true
return nil
}
func (s *SSLSMTP) Close() error {
if !s.connected {
return nil
}
if err := s.client.Quit(); err != nil {
return fmt.Errorf("%w: %v", ErrSMTPConnectionFailed, err)
}
s.connected = false
return nil
}
func (s *SSLSMTP) Send(msg *Message) error {
if err := s.Connect(); err != nil {
return err
}
defer s.Close()
from := mail.Address{
Name: "gte",
Address: s.config.From,
}
to := mail.Address{
Name: "todo",
Address: s.config.To,
}
headers := make(map[string]string)
headers["From"] = from.String()
headers["To"] = to.String()
headers["Subject"] = msg.Subject
message := ""
for k, v := range headers {
message += fmt.Sprintf("%s: %s\r\n", k, v)
}
message += fmt.Sprintf("\r\n%s", msg.Body)
if err := s.client.Mail(s.config.From); err != nil {
return fmt.Errorf("%w: %v", ErrSendMessageFailed, err)
}
if err := s.client.Rcpt(s.config.To); err != nil {
return fmt.Errorf("%w: %v", ErrSendMessageFailed, err)
}
wc, err := s.client.Data()
if err != nil {
return fmt.Errorf("%w: %v", ErrSendMessageFailed, err)
}
if _, err := wc.Write([]byte(message)); err != nil {
return fmt.Errorf("%w: %v", ErrSendMessageFailed, err)
}
if err := wc.Close(); err != nil {
return fmt.Errorf("%w: %v", ErrSendMessageFailed, err)
}
return nil
}

View File

@ -1,6 +1,7 @@
package mstore package mstore
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -12,71 +13,97 @@ import (
"github.com/emersion/go-message/mail" "github.com/emersion/go-message/mail"
) )
type Body struct { var (
ErrIMAPInvalidConfig = errors.New("invalid imap configuration")
ErrIMAPConnFailure = errors.New("could not connect with imap")
ErrIMAPNotConnected = errors.New("unable to perform, not connected to imap")
ErrIMAPServerProblem = errors.New("imap server was unable to perform operation")
)
type IMAPBody struct {
reader io.Reader reader io.Reader
length int length int
} }
func NewBody(msg string) *Body { func NewIMAPBody(msg string) *IMAPBody {
return &IMAPBody{
return &Body{
reader: strings.NewReader(msg), reader: strings.NewReader(msg),
length: len([]byte(msg)), length: len([]byte(msg)),
} }
} }
func (b *Body) Read(p []byte) (int, error) { func (b *IMAPBody) Read(p []byte) (int, error) {
return b.reader.Read(p) return b.reader.Read(p)
} }
func (b *Body) Len() int { func (b *IMAPBody) Len() int {
return b.length return b.length
} }
type ImapConfiguration struct { type IMAPConfig struct {
ImapUrl string IMAPURL string
ImapUsername string IMAPUsername string
ImapPassword string IMAPPassword string
} }
func (esc *ImapConfiguration) Valid() bool { func (esc *IMAPConfig) Valid() bool {
if esc.ImapUrl == "" { if esc.IMAPURL == "" {
return false return false
} }
if esc.ImapUsername == "" || esc.ImapPassword == "" { if esc.IMAPUsername == "" || esc.IMAPPassword == "" {
return false return false
} }
return true return true
} }
type Imap struct { type IMAP struct {
imap *client.Client config *IMAPConfig
connected bool
client *client.Client
mboxStatus *imap.MailboxStatus mboxStatus *imap.MailboxStatus
} }
func ImapConnect(conf *ImapConfiguration) (*Imap, error) { func NewIMAP(config *IMAPConfig) *IMAP {
imap, err := client.DialTLS(conf.ImapUrl, nil) return &IMAP{
config: config,
}
}
func (im *IMAP) Connect() error {
if !im.config.Valid() {
return ErrIMAPInvalidConfig
}
if im.connected {
return nil
}
cl, err := client.DialTLS(im.config.IMAPURL, nil)
if err != nil { if err != nil {
return &Imap{}, err return fmt.Errorf("%w: %v", ErrIMAPConnFailure, err)
} }
if err := imap.Login(conf.ImapUsername, conf.ImapPassword); err != nil { if err := cl.Login(im.config.IMAPUsername, im.config.IMAPPassword); err != nil {
return &Imap{}, err return fmt.Errorf("%w: %v", ErrIMAPConnFailure, err)
} }
return &Imap{ im.client = cl
imap: imap, im.connected = true
}, nil
return nil
} }
func (es *Imap) Disconnect() { func (im *IMAP) Close() {
es.imap.Logout() im.client.Logout()
im.connected = false
} }
func (es *Imap) Folders() ([]string, error) { func (im *IMAP) Folders() ([]string, error) {
im.Connect()
defer im.Close()
boxes, done := make(chan *imap.MailboxInfo), make(chan error) boxes, done := make(chan *imap.MailboxInfo), make(chan error)
go func() { go func() {
done <- es.imap.List("", "*", boxes) done <- im.client.List("", "*", boxes)
}() }()
folders := []string{} folders := []string{}
@ -91,28 +118,35 @@ func (es *Imap) Folders() ([]string, error) {
return folders, nil return folders, nil
} }
func (es *Imap) selectFolder(folder string) error { func (im *IMAP) selectFolder(folder string) error {
status, err := es.imap.Select(folder, false) if !im.connected {
if err != nil { return ErrIMAPNotConnected
return err
} }
es.mboxStatus = status status, err := im.client.Select(folder, false)
if err != nil {
return fmt.Errorf("%w, %v", ErrIMAPServerProblem, err)
}
im.mboxStatus = status
return nil return nil
} }
func (es *Imap) Messages(folder string) ([]*Message, error) { func (im *IMAP) Messages(folder string) ([]*Message, error) {
if err := es.selectFolder(folder); err != nil { im.Connect()
defer im.Close()
if err := im.selectFolder(folder); err != nil {
return []*Message{}, err return []*Message{}, err
} }
if es.mboxStatus.Messages == 0 { if im.mboxStatus.Messages == 0 {
return []*Message{}, nil return []*Message{}, nil
} }
seqset := new(imap.SeqSet) seqset := new(imap.SeqSet)
seqset.AddRange(uint32(1), es.mboxStatus.Messages) seqset.AddRange(uint32(1), im.mboxStatus.Messages)
// Get the whole message body // Get the whole message body
section := &imap.BodySectionName{} section := &imap.BodySectionName{}
@ -120,7 +154,7 @@ func (es *Imap) Messages(folder string) ([]*Message, error) {
imsg, done := make(chan *imap.Message), make(chan error) imsg, done := make(chan *imap.Message), make(chan error)
go func() { go func() {
done <- es.imap.Fetch(seqset, items, imsg) done <- im.client.Fetch(seqset, items, imsg)
}() }()
messages := []*Message{} messages := []*Message{}
@ -169,30 +203,39 @@ func (es *Imap) Messages(folder string) ([]*Message, error) {
} }
if err := <-done; err != nil { if err := <-done; err != nil {
return []*Message{}, err return []*Message{}, fmt.Errorf("%w: %v", ErrIMAPServerProblem, err)
} }
return messages, nil return messages, nil
} }
func (es *Imap) Add(folder, subject, body string) error { func (im *IMAP) Add(folder, subject, body string) error {
im.Connect()
defer im.Close()
msgStr := fmt.Sprintf(`From: todo <mstore@erikwinter.nl> msgStr := fmt.Sprintf(`From: todo <mstore@erikwinter.nl>
Date: %s Date: %s
Subject: %s Subject: %s
%s`, time.Now().Format(time.RFC822Z), subject, body) %s`, time.Now().Format(time.RFC822Z), subject, body)
msg := NewBody(msgStr) msg := NewIMAPBody(msgStr)
return es.imap.Append(folder, nil, time.Time{}, imap.Literal(msg)) if err := im.client.Append(folder, nil, time.Time{}, imap.Literal(msg)); err != nil {
return fmt.Errorf("%w: %v", ErrIMAPServerProblem, err)
}
return nil
} }
func (es *Imap) Remove(msg *Message) error { func (im *IMAP) Remove(msg *Message) error {
if msg == nil || !msg.Valid() { if msg == nil || !msg.Valid() {
return ErrInvalidMessage return ErrInvalidMessage
} }
im.Connect()
defer im.Close()
if err := es.selectFolder(msg.Folder); err != nil { if err := im.selectFolder(msg.Folder); err != nil {
return err return err
} }
@ -200,14 +243,14 @@ func (es *Imap) Remove(msg *Message) error {
seqset := new(imap.SeqSet) seqset := new(imap.SeqSet)
seqset.AddRange(msg.Uid, msg.Uid) seqset.AddRange(msg.Uid, msg.Uid)
storeItem := imap.FormatFlagsOp(imap.SetFlags, true) storeItem := imap.FormatFlagsOp(imap.SetFlags, true)
err := es.imap.UidStore(seqset, storeItem, imap.FormatStringList([]string{imap.DeletedFlag}), nil) err := im.client.UidStore(seqset, storeItem, imap.FormatStringList([]string{imap.DeletedFlag}), nil)
if err != nil { if err != nil {
return err return fmt.Errorf("%w: %v", ErrIMAPServerProblem, err)
} }
// expunge box // expunge box
if err := es.imap.Expunge(nil); err != nil { if err := im.client.Expunge(nil); err != nil {
return err return fmt.Errorf("%w: %v", ErrIMAPServerProblem, err)
} }
return nil return nil