summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorErik Winter <ik@erikwinter.nl>2021-05-13 08:15:14 +0200
committerErik Winter <ik@erikwinter.nl>2021-05-15 09:11:58 +0200
commitc031bef006751089a3d02ff53abe7a25469be3c2 (patch)
tree16f6a546e3218918f70aac0e5e61883a82ea7131
parent5636958dc99d6a43e08b253ccabc1c6c7ee982a7 (diff)
imap daemon
-rw-r--r--cmd/daemon/service.go79
-rw-r--r--cmd/generate-recurring/main.go57
-rw-r--r--cmd/process-inbox/main.go44
-rw-r--r--go.mod2
-rw-r--r--go.sum12
-rw-r--r--internal/process/inbox.go52
-rw-r--r--internal/process/inbox_test.go134
-rw-r--r--internal/process/recur.go56
-rw-r--r--internal/process/recur_test.go69
-rw-r--r--internal/task/dispatch.go20
-rw-r--r--internal/task/repo.go16
-rw-r--r--internal/task/task.go12
-rw-r--r--pkg/msend/memory.go17
-rw-r--r--pkg/msend/memory_test.go21
-rw-r--r--pkg/msend/msend.go16
-rw-r--r--pkg/msend/smtp.go127
-rw-r--r--pkg/mstore/imap.go135
17 files changed, 751 insertions, 118 deletions
diff --git a/cmd/daemon/service.go b/cmd/daemon/service.go
new file mode 100644
index 0000000..f2230df
--- /dev/null
+++ b/cmd/daemon/service.go
@@ -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")
+ }
+ }
+}
diff --git a/cmd/generate-recurring/main.go b/cmd/generate-recurring/main.go
index 766ae7c..72a0b1b 100644
--- a/cmd/generate-recurring/main.go
+++ b/cmd/generate-recurring/main.go
@@ -1,51 +1,52 @@
package main
import (
- "log"
"os"
"strconv"
+ "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() {
- config := &mstore.ImapConfiguration{
- ImapUrl: os.Getenv("IMAP_URL"),
- ImapUsername: os.Getenv("IMAP_USERNAME"),
- ImapPassword: os.Getenv("IMAP_PASSWORD"),
+ logger := log.New(os.Stdout).WithField("cmd", "generate-recurring")
+ IMAPConfig := &mstore.IMAPConfig{
+ IMAPURL: os.Getenv("IMAP_URL"),
+ 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(IMAPConfig)
+
+ 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"))
if err != nil {
daysAhead = 0
}
- mailStore, err := mstore.ImapConnect(config)
- if err != nil {
- log.Fatal(err)
- }
- defer mailStore.Disconnect()
+ taskRepo := task.NewRepository(msgStore)
+ taskDisp := task.NewDispatcher(mailSend)
- taskRepo := task.NewRepository(mailStore)
- tasks, err := taskRepo.FindAll(task.FOLDER_RECURRING)
+ recur := process.NewRecur(taskRepo, taskDisp, daysAhead)
+ result, err := recur.Process()
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.WithErr(err).Error("unable to process recurring")
+ os.Exit(1)
}
+ logger.WithField("count", result.Count).Info("finished generating recurring tasks")
}
diff --git a/cmd/process-inbox/main.go b/cmd/process-inbox/main.go
index 6cf2fac..ba19369 100644
--- a/cmd/process-inbox/main.go
+++ b/cmd/process-inbox/main.go
@@ -1,46 +1,28 @@
package main
import (
- "log"
"os"
+ "git.ewintr.nl/go-kit/log"
+ "git.ewintr.nl/gte/internal/process"
"git.ewintr.nl/gte/internal/task"
"git.ewintr.nl/gte/pkg/mstore"
)
func main() {
- config := &mstore.ImapConfiguration{
- ImapUrl: os.Getenv("IMAP_URL"),
- ImapUsername: os.Getenv("IMAP_USERNAME"),
- ImapPassword: os.Getenv("IMAP_PASSWORD"),
+ logger := log.New(os.Stdout).WithField("cmd", "process-inbox")
+ config := &mstore.IMAPConfig{
+ IMAPURL: os.Getenv("IMAP_URL"),
+ IMAPUsername: os.Getenv("IMAP_USERNAME"),
+ IMAPPassword: os.Getenv("IMAP_PASSWORD"),
}
- if !config.Valid() {
- log.Fatal("please set IMAP_USER, IMAP_PASSWORD, etc environment variables")
- }
-
- mailStore, err := mstore.ImapConnect(config)
- if err != nil {
- log.Fatal(err)
- }
- defer mailStore.Disconnect()
+ msgStore := mstore.NewIMAP(config)
- taskRepo := task.NewRepository(mailStore)
- tasks, err := taskRepo.FindAll(task.FOLDER_INBOX)
+ inboxProcessor := process.NewInbox(task.NewRepository(msgStore))
+ result, err := inboxProcessor.Process()
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.WithErr(err).Error("unable to process inbox")
+ os.Exit(1)
}
+ logger.WithField("count", result.Count).Info("finished processing inbox")
}
diff --git a/go.mod b/go.mod
index 2cf8496..95df948 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module git.ewintr.nl/gte
go 1.14
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-message v0.14.1
github.com/google/uuid v1.2.0
diff --git a/go.sum b/go.sum
index 42f60a1..77914be 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,9 @@
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=
-git.ewintr.nl/go-kit v0.0.0-20210509123609-19e474005502 h1:h3PYDz6uWAz2mBPOFShhkZv7SapfLWXcBDsmHqFCq5w=
-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 h1:jVP4muIBqQ5poAuOlDgW/PhYscSnRHdqEqX1WfAK++A=
+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/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
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/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.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo=
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.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-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/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
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/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.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
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/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.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
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-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-20191220142924-d4481acd189f h1:68K/z8GLUxV76xGSqwTWw2gyk/jwn79LUL43rES2g8o=
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
diff --git a/internal/process/inbox.go b/internal/process/inbox.go
new file mode 100644
index 0000000..39ca635
--- /dev/null
+++ b/internal/process/inbox.go
@@ -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
+}
diff --git a/internal/process/inbox_test.go b/internal/process/inbox_test.go
new file mode 100644
index 0000000..9507c55
--- /dev/null
+++ b/internal/process/inbox_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/internal/process/recur.go b/internal/process/recur.go
new file mode 100644
index 0000000..7347b94
--- /dev/null
+++ b/internal/process/recur.go
@@ -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
+}
diff --git a/internal/process/recur_test.go b/internal/process/recur_test.go
new file mode 100644
index 0000000..511a3b1
--- /dev/null
+++ b/internal/process/recur_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/internal/task/dispatch.go b/internal/task/dispatch.go
new file mode 100644
index 0000000..0852347
--- /dev/null
+++ b/internal/task/dispatch.go
@@ -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(),
+ })
+}
diff --git a/internal/task/repo.go b/internal/task/repo.go
index f61dcf8..5275921 100644
--- a/internal/task/repo.go
+++ b/internal/task/repo.go
@@ -52,8 +52,8 @@ func (tr *TaskRepo) Update(t *Task) error {
}
// add new
- if err := tr.mstore.Add(t.Folder, t.FormatSubject(), t.FormatBody()); err != nil {
- return fmt.Errorf("%w: %s", ErrMStoreError, err)
+ if err := tr.Add(t); err != nil {
+ return err
}
// remove old
@@ -66,6 +66,18 @@ func (tr *TaskRepo) Update(t *Task) error {
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
func (tr *TaskRepo) CleanUp() error {
// loop through folders, get all task version info
diff --git a/internal/task/task.go b/internal/task/task.go
index b4724f7..cac809c 100644
--- a/internal/task/task.go
+++ b/internal/task/task.go
@@ -261,20 +261,18 @@ func (t *Task) RecursOn(date Date) bool {
return t.Recur.RecursOn(date)
}
-func (t *Task) CreateDueMessage(date Date) (string, string, error) {
- if !t.IsRecurrer() {
- return "", "", ErrTaskIsNotRecurring
+func (t *Task) GenerateFromRecurrer(date Date) (*Task, error) {
+ if !t.IsRecurrer() || !t.RecursOn(date) {
+ return &Task{}, ErrTaskIsNotRecurring
}
- tempTask := &Task{
+ return &Task{
Id: uuid.New().String(),
Version: 1,
Action: t.Action,
Project: t.Project,
Due: date,
- }
-
- return tempTask.FormatSubject(), tempTask.FormatBody(), nil
+ }, nil
}
func FieldFromBody(field, body string) (string, bool) {
diff --git a/pkg/msend/memory.go b/pkg/msend/memory.go
new file mode 100644
index 0000000..6f15c0d
--- /dev/null
+++ b/pkg/msend/memory.go
@@ -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
+}
diff --git a/pkg/msend/memory_test.go b/pkg/msend/memory_test.go
new file mode 100644
index 0000000..8583914
--- /dev/null
+++ b/pkg/msend/memory_test.go
@@ -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)
+}
diff --git a/pkg/msend/msend.go b/pkg/msend/msend.go
new file mode 100644
index 0000000..5d61875
--- /dev/null
+++ b/pkg/msend/msend.go
@@ -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
+}
diff --git a/pkg/msend/smtp.go b/pkg/msend/smtp.go
new file mode 100644
index 0000000..186db68
--- /dev/null
+++ b/pkg/msend/smtp.go
@@ -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
+}
diff --git a/pkg/mstore/imap.go b/pkg/mstore/imap.go
index 5535002..4a4a2ae 100644
--- a/pkg/mstore/imap.go
+++ b/pkg/mstore/imap.go
@@ -1,6 +1,7 @@
package mstore
import (
+ "errors"
"fmt"
"io"
"io/ioutil"
@@ -12,71 +13,97 @@ import (
"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
length int
}
-func NewBody(msg string) *Body {
-
- return &Body{
+func NewIMAPBody(msg string) *IMAPBody {
+ return &IMAPBody{
reader: strings.NewReader(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)
}
-func (b *Body) Len() int {
+func (b *IMAPBody) Len() int {
return b.length
}
-type ImapConfiguration struct {
- ImapUrl string
- ImapUsername string
- ImapPassword string
+type IMAPConfig struct {
+ IMAPURL string
+ IMAPUsername string
+ IMAPPassword string
}
-func (esc *ImapConfiguration) Valid() bool {
- if esc.ImapUrl == "" {
+func (esc *IMAPConfig) Valid() bool {
+ if esc.IMAPURL == "" {
return false
}
- if esc.ImapUsername == "" || esc.ImapPassword == "" {
+ if esc.IMAPUsername == "" || esc.IMAPPassword == "" {
return false
}
return true
}
-type Imap struct {
- imap *client.Client
+type IMAP struct {
+ config *IMAPConfig
+ connected bool
+ client *client.Client
mboxStatus *imap.MailboxStatus
}
-func ImapConnect(conf *ImapConfiguration) (*Imap, error) {
- imap, err := client.DialTLS(conf.ImapUrl, nil)
+func NewIMAP(config *IMAPConfig) *IMAP {
+ 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 {
- return &Imap{}, err
+ return fmt.Errorf("%w: %v", ErrIMAPConnFailure, err)
}
- if err := imap.Login(conf.ImapUsername, conf.ImapPassword); err != nil {
- return &Imap{}, err
+ if err := cl.Login(im.config.IMAPUsername, im.config.IMAPPassword); err != nil {
+ return fmt.Errorf("%w: %v", ErrIMAPConnFailure, err)
}
- return &Imap{
- imap: imap,
- }, nil
+ im.client = cl
+ im.connected = true
+
+ return nil
}
-func (es *Imap) Disconnect() {
- es.imap.Logout()
+func (im *IMAP) Close() {
+ 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)
go func() {
- done <- es.imap.List("", "*", boxes)
+ done <- im.client.List("", "*", boxes)
}()
folders := []string{}
@@ -91,28 +118,35 @@ func (es *Imap) Folders() ([]string, error) {
return folders, nil
}
-func (es *Imap) selectFolder(folder string) error {
- status, err := es.imap.Select(folder, false)
+func (im *IMAP) selectFolder(folder string) error {
+ if !im.connected {
+ return ErrIMAPNotConnected
+ }
+
+ status, err := im.client.Select(folder, false)
if err != nil {
- return err
+ return fmt.Errorf("%w, %v", ErrIMAPServerProblem, err)
}
- es.mboxStatus = status
+ im.mboxStatus = status
return nil
}
-func (es *Imap) Messages(folder string) ([]*Message, error) {
- if err := es.selectFolder(folder); err != nil {
+func (im *IMAP) Messages(folder string) ([]*Message, error) {
+ im.Connect()
+ defer im.Close()
+
+ if err := im.selectFolder(folder); err != nil {
return []*Message{}, err
}
- if es.mboxStatus.Messages == 0 {
+ if im.mboxStatus.Messages == 0 {
return []*Message{}, nil
}
seqset := new(imap.SeqSet)
- seqset.AddRange(uint32(1), es.mboxStatus.Messages)
+ seqset.AddRange(uint32(1), im.mboxStatus.Messages)
// Get the whole message body
section := &imap.BodySectionName{}
@@ -120,7 +154,7 @@ func (es *Imap) Messages(folder string) ([]*Message, error) {
imsg, done := make(chan *imap.Message), make(chan error)
go func() {
- done <- es.imap.Fetch(seqset, items, imsg)
+ done <- im.client.Fetch(seqset, items, imsg)
}()
messages := []*Message{}
@@ -169,30 +203,39 @@ func (es *Imap) Messages(folder string) ([]*Message, error) {
}
if err := <-done; err != nil {
- return []*Message{}, err
+ return []*Message{}, fmt.Errorf("%w: %v", ErrIMAPServerProblem, err)
}
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>
Date: %s
Subject: %s
%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() {
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
}
@@ -200,14 +243,14 @@ func (es *Imap) Remove(msg *Message) error {
seqset := new(imap.SeqSet)
seqset.AddRange(msg.Uid, msg.Uid)
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 {
- return err
+ return fmt.Errorf("%w: %v", ErrIMAPServerProblem, err)
}
// expunge box
- if err := es.imap.Expunge(nil); err != nil {
- return err
+ if err := im.client.Expunge(nil); err != nil {
+ return fmt.Errorf("%w: %v", ErrIMAPServerProblem, err)
}
return nil