182 lines
5.0 KiB
Go
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))
|
|
}
|
|
}
|