From 168cce8a935de864eef95f423f128a7bf49aafda Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Sun, 3 May 2020 00:41:36 +0200 Subject: Update support for IMAP --- internal/feed/mail.go | 25 ++++++---- internal/imap/cmds.go | 16 +++++++ internal/imap/connection.go | 109 ++++++++++++++++++++++++++++++++++++++++++-- internal/msg/msg.go | 64 ++++++++++++++++++++++++++ main.go | 8 ++-- 5 files changed, 205 insertions(+), 17 deletions(-) create mode 100644 internal/msg/msg.go 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() } -- cgit v1.2.3-70-g09d2