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)) }