package store import "database/sql" // EnsureFolderBaseline initialises folder_state on first contact, or resets it // when the server's UIDVALIDITY differs from what we stored. func (s *Store) EnsureFolderBaseline(account, folder string, uidvalidity, maxUID uint32) error { a, err := s.GetAccount(account) if err != nil { return err } var ( curUIDValidity uint32 haveRow bool ) row := s.db.QueryRow( "SELECT uidvalidity FROM folder_state WHERE account_id=? AND folder=?", a.ID, folder) switch err := row.Scan(&curUIDValidity); err { case nil: haveRow = true case sql.ErrNoRows: haveRow = false default: return err } if haveRow && curUIDValidity == uidvalidity { return nil // already baselined, same validity } floor := maxUID if a.ProcessBacklog { floor = 0 } tx, err := s.db.Begin() if err != nil { return err } defer tx.Rollback() if _, err := tx.Exec("DELETE FROM acked WHERE account_id=? AND folder=?", a.ID, folder); err != nil { return err } if _, err := tx.Exec(` INSERT INTO folder_state(account_id,folder,uidvalidity,floor_uid) VALUES(?,?,?,?) ON CONFLICT(account_id,folder) DO UPDATE SET uidvalidity=excluded.uidvalidity, floor_uid=excluded.floor_uid`, a.ID, folder, uidvalidity, floor); err != nil { return err } return tx.Commit() } func (s *Store) floor(account, folder string) (uint32, error) { id, err := s.accountID(account) if err != nil { return 0, err } var f uint32 err = s.db.QueryRow( "SELECT floor_uid FROM folder_state WHERE account_id=? AND folder=?", id, folder).Scan(&f) return f, err } // IsNew reports whether uid is unread: above the floor and not acked. func (s *Store) IsNew(account, folder string, uid uint32) (bool, error) { id, err := s.accountID(account) if err != nil { return false, err } var floor uint32 if err := s.db.QueryRow( "SELECT floor_uid FROM folder_state WHERE account_id=? AND folder=?", id, folder).Scan(&floor); err != nil { return false, err } if uid <= floor { return false, nil } var one int err = s.db.QueryRow( "SELECT 1 FROM acked WHERE account_id=? AND folder=? AND uid=?", id, folder, uid).Scan(&one) if err == sql.ErrNoRows { return true, nil } if err != nil { return false, err } return false, nil // present in acked } // FilterNew returns the subset of uids that are new, preserving order. func (s *Store) FilterNew(account, folder string, uids []uint32) ([]uint32, error) { out := make([]uint32, 0, len(uids)) for _, u := range uids { n, err := s.IsNew(account, folder, u) if err != nil { return nil, err } if n { out = append(out, u) } } return out, nil } // Ack records uids as processed (ignoring any at or below the floor) then compacts. func (s *Store) Ack(account, folder string, uidvalidity uint32, uids ...uint32) error { id, err := s.accountID(account) if err != nil { return err } tx, err := s.db.Begin() if err != nil { return err } defer tx.Rollback() var floor uint32 if err := tx.QueryRow( "SELECT floor_uid FROM folder_state WHERE account_id=? AND folder=?", id, folder).Scan(&floor); err != nil { return err } for _, u := range uids { if u <= floor { continue } if _, err := tx.Exec( "INSERT OR IGNORE INTO acked(account_id,folder,uidvalidity,uid) VALUES(?,?,?,?)", id, folder, uidvalidity, u); err != nil { return err } } // Compact: while floor+1 is acked, advance floor and drop that row. for { next := floor + 1 var present int err := tx.QueryRow( "SELECT 1 FROM acked WHERE account_id=? AND folder=? AND uid=?", id, folder, next).Scan(&present) if err == sql.ErrNoRows { break } if err != nil { return err } if _, err := tx.Exec( "DELETE FROM acked WHERE account_id=? AND folder=? AND uid=?", id, folder, next); err != nil { return err } floor = next } if _, err := tx.Exec( "UPDATE folder_state SET floor_uid=? WHERE account_id=? AND folder=?", floor, id, folder); err != nil { return err } return tx.Commit() }