package main

import (
	"bytes"
	"errors"
	"flag"
	"fmt"
	"io"
	"log"
	"net"
	"strings"
	"time"

	"github.com/emersion/go-imap"
	"github.com/emersion/go-imap/client"
)

var (
	serverAddr        = flag.String("server", "", "server address")
	username          = flag.String("username", "", "username")
	password          = flag.String("password", "", "password")
	oauth2Token       = flag.String("bearer-token", "", "OAuth2 bearer token")
	connectionTimeout = flag.Duration("timeout", 5*time.Second, "initial connection timeout")
	maxAgeDays        = flag.Int("days", 15, "cleanup messages older than `n` days")
	filterStr         = flag.String("filter", "", "additional message filters, in IMAP SEARCH syntax")
	dryRun            = flag.Bool("dry-run", false, "do not actually delete messages")
	folder            = flag.String("folder", "INBOX", "remote IMAP folder")
	verbose           = flag.Bool("verbose", false, "print additional debug messages")
	batchSize         = flag.Int("batch-size", 100, "deletion batch size")
	moveToTrash       = flag.Bool("move-to-gmail-trash", false, "move messages to Trash folder before deleting them (GMail workaround)")
)

func vlog(fmt string, args ...interface{}) {
	if *verbose {
		log.Printf("debug: "+fmt, args...)
	}
}

func dial() (*client.Client, error) {
	dialer := &net.Dialer{
		Timeout: *connectionTimeout,
	}
	addr := *serverAddr
	if addr == "" {
		return nil, errors.New("server not specified")
	}
	if !strings.Contains(addr, ":") {
		addr += ":993"
	}
	vlog("connecting to %s", addr)
	return client.DialWithDialerTLS(dialer, addr, nil)
}

func authenticate(c *client.Client) error {
	if *username == "" {
		return errors.New("username not specified")
	}

	if *oauth2Token != "" {
		if ok, _ := c.SupportAuth(XOAuth2); !ok {
			return errors.New("XOAUTH2 not supported by the server")
		}

		vlog("authenticating as '%s' (XOAUTH2)", *username)
		saslClient := newXOAuth2Client(*username, *oauth2Token)
		return c.Authenticate(saslClient)
	}

	vlog("authenticating as '%s' (PLAIN)", *username)
	return c.Login(*username, *password)
}

func buildCriteria() (*imap.SearchCriteria, error) {
	crit := imap.NewSearchCriteria()

	// Optionally parse the IMAP filter string.
	if *filterStr != "" {
		imapFilterStr := "(" + *filterStr + ")"
		r := imap.NewReader(bytes.NewBuffer([]byte(imapFilterStr)))
		fields, err := r.ReadFields()
		if err != nil && err != io.EOF {
			return nil, fmt.Errorf("imap.ReadFields: %w", err)
		}
		if err := crit.ParseWithCharset(fields[0].([]interface{}), nil); err != nil {
			return nil, fmt.Errorf("imap.SearchCriteria.ParseWithCharset: %w", err)
		}
	}

	// Always add the 'before' criteria, and exclude already
	// deleted messages.
	crit.Before = time.Now().AddDate(0, 0, -*maxAgeDays)
	crit.WithoutFlags = append(crit.WithoutFlags, imap.DeletedFlag)

	return crit, nil
}

func uidBatch(c *client.Client, uids []uint32, f func([]uint32) error) error {
	for i := 0; i < len(uids); i += *batchSize {
		j := i + *batchSize
		if j > len(uids) {
			j = len(uids)
		}
		if err := f(uids[i:j]); err != nil {
			return err
		}
	}
	return nil
}

func actuallyDelete(c *client.Client, uids *imap.SeqSet) error {
	// Do the GMail move-to-Trash dance.
	if *moveToTrash {
		if err := c.UidStore(
			uids,
			"+X-GM-LABELS",
			[]interface{}{"\\Trash"},
			nil,
		); err != nil {
			return err
		}
	}

	// Just set the \\Deleted flag as one normally would.
	return c.UidStore(
		uids,
		imap.FormatFlagsOp(imap.AddFlags, true),
		[]interface{}{imap.DeletedFlag},
		nil,
	)
}

func main() {
	log.SetFlags(0)
	flag.Parse()

	c, err := dial()
	if err != nil {
		log.Fatalf("connection error: %v", err)
	}
	defer c.Logout()
	if err := authenticate(c); err != nil {
		log.Fatalf("login error: %v", err)
	}
	_, err = c.Select(*folder, false)
	if err != nil {
		log.Fatalf("could not select folder %s: %v", *folder, err)
	}

	crit, err := buildCriteria()
	if err != nil {
		log.Fatalf("error building search filter: %v", err)
	}

	uids, err := c.UidSearch(crit)
	if err != nil {
		log.Fatalf("search error: %v", err)
	}
	vlog("found %d messages to delete", len(uids))
	if len(uids) == 0 {
		return
	}

	if !*dryRun {
		err = uidBatch(c, uids, func(uids []uint32) error {
			// Must convert uints to SeqSet.
			set := new(imap.SeqSet)
			for _, uid := range uids {
				set.AddNum(uid)
			}

			vlog("deleting %d messages...", len(uids))
			return actuallyDelete(c, set)
		})
		if err != nil {
			log.Fatalf("delete error: %v", err)
		}

		vlog("expunging mailbox")
		if err := c.Expunge(nil); err != nil {
			log.Fatalf("expunge error: %v", err)
		}
	}

	fmt.Printf("deleted %d messages\n", len(uids))
}