This repository has been archived on 2023-12-27. You can view files and clone it, but cannot push or open issues or pull requests.
autosnooze/pkg/mailbox/mailbox.go
2022-12-16 19:45:01 -08:00

182 lines
5.0 KiB
Go

package mailbox
import (
"fmt"
"strings"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
imapClient "github.com/emersion/go-imap/client"
"github.com/tj/go-naturaldate"
"go.uber.org/zap"
"git.sr.ht/~tgrosinger/autosnooze/pkg/config"
)
type AccountProcessor struct {
Client *imapClient.Client
Logger *zap.Logger
Config config.Config
}
type Mailbox struct {
// Prefix is the portion of the name which comes before the portion that
// should be parsed as a date or relative date. e.g. "AutoSnooze/"
Prefix string
// FullName is the name as returned from the IMAP server.
FullName string
}
func (m *Mailbox) StrippedName() string {
return strings.TrimPrefix(m.FullName, m.Prefix)
}
func (m *Mailbox) Date(format string) (time.Time, error) {
return time.Parse(format, m.StrippedName())
}
func (p *AccountProcessor) Process() {
// List mailboxes
mailboxes := make(chan *imap.MailboxInfo, 10)
done := make(chan error, 1)
go func() {
done <- p.Client.List("", "*", mailboxes)
}()
// The IMAP library does not seem to be thread-safe, so we will collect the
// mailboxes and process them sequentially.
toProcess := make([]*Mailbox, 0, 10)
for m := range mailboxes {
if !strings.HasPrefix(m.Name, p.Config.Mailbox.Prefix) {
p.Logger.Debug("skipping mailbox", zap.String("mailbox", m.Name))
continue
}
toProcess = append(toProcess, &Mailbox{
Prefix: p.Config.Mailbox.Prefix,
FullName: m.Name,
})
}
if err := <-done; err != nil {
p.Logger.Error("failed while processing mailboxes", zap.Error(err))
return
}
for _, mailbox := range toProcess {
// Separate mailboxes into absolute and relative
if _, err := mailbox.Date(p.Config.Mailbox.DateFormat); err == nil {
p.processAbsoluteMailbox(mailbox)
} else {
p.processRelativeMailbox(mailbox)
}
}
}
// processAbsoluteMailbox checks if the date represented by the mailbox name is
// today and if so, moves the contained mail into the inbox.
func (p *AccountProcessor) processAbsoluteMailbox(mailbox *Mailbox) {
logger := p.Logger.With(zap.String("mailbox", mailbox.FullName))
logger.Debug("processing absolute mailbox")
date, err := mailbox.Date(p.Config.Mailbox.DateFormat)
if err != nil {
logger.Warn("failed to parse absolute mailbox name as date", zap.Error(err))
return
}
if !date.Before(time.Now()) {
logger.Debug("skipping absolute mailbox which is not for today")
return
}
mBox, err := p.Client.Select(mailbox.FullName, false)
if err != nil {
logger.Warn("failed to select mailbox to check for messages", zap.Error(err))
return
}
if mBox.Messages == 0 {
logger.Debug("mailbox contains no messages")
return
}
logger.Info("moving messages from mailbox into inbox")
seqset := new(imap.SeqSet)
seqset.AddRange(1, mBox.Messages) // All messages
err = p.Client.Move(seqset, "INBOX")
if err != nil {
logger.Warn("unable to move messages to target mailbox", zap.Error(err))
return
}
p.deleteMailboxIfEmpty(mailbox)
}
// processRelativeMailbox checks if there is any mail in this folder, if so it
// finds or creates the cooresponding absolute folder and moves the mail to that
// folder.
// e.g. AutoSnooze/tomorrow => AutoSnooze/2022-12-31
func (p *AccountProcessor) processRelativeMailbox(mailbox *Mailbox) {
logger := p.Logger.With(zap.String("mailbox", mailbox.FullName))
logger.Debug("processing relative mailbox")
mBox, err := p.Client.Select(mailbox.FullName, false)
if err != nil {
logger.Warn("failed to select mailbox to check for messages", zap.Error(err))
return
}
if mBox.Messages == 0 {
logger.Debug("mailbox contains no messages")
return
}
d, err := naturaldate.Parse(mailbox.StrippedName(), time.Now(),
naturaldate.WithDirection(naturaldate.Future))
if err != nil {
logger.Debug("unable to parse mailbox as relative time", zap.Error(err))
return
}
targetMailboxName := fmt.Sprintf(mailbox.Prefix + d.Format(p.Config.Mailbox.DateFormat))
logger = logger.With(zap.String("target_mailbox", targetMailboxName))
err = p.Client.Create(targetMailboxName)
// Create does not actually return this sential error, just a matching message.
if err != nil && err.Error() != backend.ErrMailboxAlreadyExists.Error() {
logger.Warn("unable to create target mailbox", zap.Error(err))
return
}
logger.Info("moving messages from mailbox into target")
seqset := new(imap.SeqSet)
seqset.AddRange(1, mBox.Messages) // All messages
err = p.Client.Move(seqset, targetMailboxName)
if err != nil {
logger.Warn("unable to move messages to target mailbox", zap.Error(err))
}
}
func (p *AccountProcessor) deleteMailboxIfEmpty(mailbox *Mailbox) {
logger := p.Logger.With(zap.String("mailbox", mailbox.FullName))
mBox, err := p.Client.Select(mailbox.FullName, false)
if err != nil {
logger.Warn("failed to select mailbox to check for messages", zap.Error(err))
return
}
if mBox.Messages != 0 {
logger.Debug("mailbox contains messages, not deleting")
return
}
err = p.Client.Delete(mailbox.FullName)
if err != nil {
logger.Warn("failed to delete mailbox", zap.Error(err))
}
}