aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRené 'Necoro' Neumann <necoro@necoro.eu>2020-05-03 00:41:36 +0200
committerRené 'Necoro' Neumann <necoro@necoro.eu>2020-05-03 00:41:36 +0200
commit168cce8a935de864eef95f423f128a7bf49aafda (patch)
tree9cc240dd479b1e85fbb55e1cda4a14d3a938d50e
parentfec3ecd257c34fba37703b2999ab5ea902314657 (diff)
downloadfeed2imap-go-168cce8a935de864eef95f423f128a7bf49aafda.tar.gz
feed2imap-go-168cce8a935de864eef95f423f128a7bf49aafda.tar.bz2
feed2imap-go-168cce8a935de864eef95f423f128a7bf49aafda.zip
Update support for IMAP
-rw-r--r--internal/feed/mail.go25
-rw-r--r--internal/imap/cmds.go16
-rw-r--r--internal/imap/connection.go109
-rw-r--r--internal/msg/msg.go64
-rw-r--r--main.go8
5 files changed, 205 insertions, 17 deletions
diff --git a/internal/feed/mail.go b/internal/feed/mail.go
index 8037221..290c965 100644
--- a/internal/feed/mail.go
+++ b/internal/feed/mail.go
@@ -17,6 +17,7 @@ import (
"github.com/gabriel-vasile/mimetype"
"github.com/Necoro/feed2imap-go/internal/feed/template"
+ "github.com/Necoro/feed2imap-go/internal/msg"
"github.com/Necoro/feed2imap-go/pkg/config"
"github.com/Necoro/feed2imap-go/pkg/log"
)
@@ -53,9 +54,9 @@ func (item *item) buildHeader() message.Header {
h.SetContentType("multipart/alternative", nil)
h.SetAddressList("From", item.fromAddress())
h.SetAddressList("To", item.toAddress())
- h.Set("X-Feed2Imap-Version", config.Version())
- h.Set("X-Feed2Imap-Reason", strings.Join(item.reasons, ","))
- h.Set("X-Feed2Imap-Item", item.id())
+ h.Set(msg.VersionHeader, config.Version())
+ h.Set(msg.ReasonHeader, strings.Join(item.reasons, ","))
+ h.Set(msg.IdHeader, item.id())
h.Set("Message-Id", item.messageId())
{ // date
@@ -160,23 +161,29 @@ func (item *item) writeToBuffer(b *bytes.Buffer) error {
return nil
}
-func (item *item) asMail() (string, error) {
+func (item *item) message() (msg.Message, error) {
var b bytes.Buffer
if err := item.writeToBuffer(&b); err != nil {
- return "", err
+ return msg.Message{}, err
}
- return b.String(), nil
+ msg := msg.Message{
+ Content: b.String(),
+ IsUpdate: item.updateOnly,
+ ID: item.id(),
+ }
+
+ return msg, nil
}
-func (feed *Feed) ToMails() ([]string, error) {
+func (feed *Feed) Messages() (msg.Messages, error) {
var (
err error
- mails = make([]string, len(feed.items))
+ mails = make([]msg.Message, len(feed.items))
)
for idx := range feed.items {
- if mails[idx], err = feed.items[idx].asMail(); err != nil {
+ if mails[idx], err = feed.items[idx].message(); err != nil {
return nil, fmt.Errorf("creating mails for %s: %w", feed.Name, err)
}
}
diff --git a/internal/imap/cmds.go b/internal/imap/cmds.go
index d978d80..7c99fc3 100644
--- a/internal/imap/cmds.go
+++ b/internal/imap/cmds.go
@@ -24,3 +24,19 @@ func (cmd addCommando) execute(conn *connection) error {
func (client *Client) PutMessages(folder Folder, messages []string) error {
return client.commander.execute(addCommando{folder, messages})
}
+
+type replaceCommando struct {
+ folder Folder
+ header string
+ value string
+ newContent string
+ force bool
+}
+
+func (cmd replaceCommando) execute(conn *connection) error {
+ return conn.replace(cmd.folder, cmd.header, cmd.value, cmd.newContent, cmd.force)
+}
+
+func (client *Client) Replace(folder Folder, header, value, newContent string, force bool) error {
+ return client.commander.execute(replaceCommando{folder, header, value, newContent, force})
+}
diff --git a/internal/imap/connection.go b/internal/imap/connection.go
index 5f62586..68b7e6b 100644
--- a/internal/imap/connection.go
+++ b/internal/imap/connection.go
@@ -128,16 +128,117 @@ func (conn *connection) ensureFolder(folder Folder) error {
}
}
+func (conn *connection) delete(uids []uint32) error {
+ storeItem := imap.FormatFlagsOp(imap.AddFlags, true)
+ seqSet := new(imap.SeqSet)
+ seqSet.AddNum(uids...)
+
+ if err := conn.c.UidStore(seqSet, storeItem, imap.DeletedFlag, nil); err != nil {
+ return fmt.Errorf("marking as deleted: %w", err)
+ }
+
+ if err := conn.c.Expunge(nil); err != nil {
+ return fmt.Errorf("expunging: %w", err)
+ }
+
+ return nil
+}
+
+func (conn *connection) fetchFlags(uid uint32) ([]string, error) {
+ fetchItem := []imap.FetchItem{imap.FetchFlags}
+
+ seqSet := new(imap.SeqSet)
+ seqSet.AddNum(uid)
+
+ messages := make(chan *imap.Message, 1)
+ done := make(chan error, 1)
+ go func() {
+ done <- conn.c.UidFetch(seqSet, fetchItem, messages)
+ }()
+
+ msg := <-messages
+ err := <-done
+
+ if err != nil {
+ return nil, fmt.Errorf("fetching flags: %w", err)
+ }
+ return msg.Flags, nil
+}
+
+func (conn *connection) replace(folder Folder, header, value, newContent string, force bool) error {
+ var err error
+ var msgIds []uint32
+
+ if err = conn.selectFolder(folder); err != nil {
+ return err
+ }
+
+ if msgIds, err = conn.searchHeader(header, value); err != nil {
+ return err
+ }
+
+ if len(msgIds) == 0 {
+ if force {
+ return conn.append(folder, nil, newContent)
+ }
+ return nil // nothing to do
+ }
+
+ var flags []string
+ if flags, err = conn.fetchFlags(msgIds[0]); err != nil {
+ return err
+ }
+
+ if err = conn.delete(msgIds); err != nil {
+ return err
+ }
+
+ if err = conn.append(folder, flags, newContent); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (conn *connection) searchHeader(header, value string) ([]uint32, error) {
+ criteria := imap.NewSearchCriteria()
+ criteria.Header.Set(header, value)
+ ids, err := conn.search(criteria)
+ if err != nil {
+ return nil, fmt.Errorf("searching for header %q=%q: %w", header, value, err)
+ }
+ return ids, nil
+}
+
+func (conn *connection) search(criteria *imap.SearchCriteria) ([]uint32, error) {
+ return conn.c.UidSearch(criteria)
+}
+
+func (conn *connection) selectFolder(folder Folder) error {
+ if _, err := conn.c.Select(folder.str, false); err != nil {
+ return fmt.Errorf("selecting folder %s: %w", folder, err)
+ }
+
+ return nil
+}
+
+func (conn *connection) append(folder Folder, flags []string, msg string) error {
+ reader := strings.NewReader(msg)
+ if err := conn.c.Append(folder.str, flags, time.Now(), reader); err != nil {
+ return fmt.Errorf("uploading message to %s: %w", folder, err)
+ }
+
+ return nil
+}
+
func (conn *connection) putMessages(folder Folder, messages []string) error {
if len(messages) == 0 {
return nil
}
- now := time.Now()
for _, msg := range messages {
- reader := strings.NewReader(msg)
- if err := conn.c.Append(folder.str, nil, now, reader); err != nil {
- return fmt.Errorf("uploading message to %s: %w", folder, err)
+ if err := conn.append(folder, nil, msg); err != nil {
+ return err
}
}
diff --git a/internal/msg/msg.go b/internal/msg/msg.go
new file mode 100644
index 0000000..c71ddaf
--- /dev/null
+++ b/internal/msg/msg.go
@@ -0,0 +1,64 @@
+package msg
+
+import (
+ "fmt"
+
+ "github.com/Necoro/feed2imap-go/internal/imap"
+ "github.com/Necoro/feed2imap-go/pkg/log"
+)
+
+// headers
+const (
+ VersionHeader = "X-Feed2Imap-Version"
+ ReasonHeader = "X-Feed2Imap-Reason"
+ IdHeader = "X-Feed2Imap-Item"
+)
+
+type Messages []Message
+
+type Message struct {
+ Content string
+ IsUpdate bool
+ ID string
+}
+
+func (m Messages) Upload(client *imap.Client, folder imap.Folder, reupload bool) error {
+ toStore := make([]string, 0, len(m))
+
+ msgs := make(chan Message, 5)
+ ok := make(chan bool)
+ go func() {
+ errHappened := false
+ for msg := range msgs {
+ if err := client.Replace(folder, IdHeader, msg.ID, msg.Content, reupload); err != nil {
+ log.Errorf("Error while updating mail with id '%s' in folder '%s'. Skipping.: %s",
+ msg.ID, folder, err)
+ errHappened = true
+ }
+ }
+
+ ok <- errHappened
+ }()
+
+ for _, msg := range m {
+ if !msg.IsUpdate {
+ toStore = append(toStore, msg.Content)
+ } else {
+ msgs <- msg
+ }
+ }
+
+ close(msgs)
+
+ putErr := client.PutMessages(folder, toStore)
+ updOk := <-ok
+
+ if putErr != nil {
+ return putErr
+ }
+ if updOk {
+ return fmt.Errorf("Errors during updating mails.")
+ }
+
+ return nil
+}
diff --git a/main.go b/main.go
index 7f31a48..ec2e91f 100644
--- a/main.go
+++ b/main.go
@@ -20,13 +20,13 @@ var dryRun = flag.Bool("dry-run", false, "do everything short of uploading and w
var buildCache = flag.Bool("build-cache", false, "only (re)build the cache; useful after migration or when the cache is lost or corrupted")
func processFeed(feed *feed.Feed, client *imap.Client, dryRun bool) {
- mails, err := feed.ToMails()
+ msgs, err := feed.Messages()
if err != nil {
log.Errorf("Processing items of feed %s: %s", feed.Name, err)
return
}
- if dryRun || len(mails) == 0 {
+ if dryRun || len(msgs) == 0 {
feed.MarkSuccess()
return
}
@@ -37,12 +37,12 @@ func processFeed(feed *feed.Feed, client *imap.Client, dryRun bool) {
return
}
- if err = client.PutMessages(folder, mails); err != nil {
+ if err = msgs.Upload(client, folder, feed.Reupload); err != nil {
log.Errorf("Uploading messages of feed %s: %s", feed.Name, err)
return
}
- log.Printf("Uploaded %d messages to '%s' @ %s", len(mails), feed.Name, folder)
+ log.Printf("Uploaded %d messages to '%s' @ %s", len(msgs), feed.Name, folder)
feed.MarkSuccess()
}