diff --git a/NOTES.md b/NOTES.md
index 416091da9a4869c3909be5de12c0e0c78140f1df..f6ce8a1a3cf06b2e7986e387fa5298de18a38cf9 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -21,7 +21,7 @@ Switch between MAIL and BULLETIN modes? MAIL commands are documented
## Things to do
* Run [godoc](http://localhost:6060/) and then review where the help text is lacking.
- * Missing [MAIL] [RESPOND] [SET DEFAULT_EXPIRE] [SET READNEW] [SET NOREADNEW] [SET NOSHOWNEW] [SET NOPROMPT_EXPIRE] [SET EXPIRE_LIMIT] [SET PROMPT_EXPIRE] [SET SHOWNEW] [SHOW NEW] [SEARCH]
+ * Missing [RESPOND] [MAIL] [SET PROMPT_EXPIRE] [SET NOREADNEW] [SET NOSHOWNEW] [SET EXPIRE_LIMIT] [SET NOPROMPT_EXPIRE] [SET READNEW] [SET SHOWNEW] [SET DEFAULT_EXPIRE] [SHOW NEW]
* Run this.Skew.Safe() before... each command? each write?
* Handle broadcast messages - create a broadcast table and add an expiration column.
* Database
@@ -30,6 +30,9 @@ Switch between MAIL and BULLETIN modes? MAIL commands are documented
* Commands for a local mail system?
* Commands to connect to Mattermost or mastodon?
* Make a spreadsheet for signups.
+ * Pager:
+ * Make / work for search.
+ * Run [VACUUM](https://www.sqlite.org/lang_vacuum.html) on the expire run.
Polishing. Review each command and put a + next to each as it is
fully done.
@@ -40,8 +43,8 @@ Top level:
ADD +BACK BULLETIN CHANGE COPY CREATE
+Ctrl-C +CURRENT DELETE DIRECTORY +EXIT +FIRST
+Folders FORWARD +HELP INDEX +Keypad +LAST
- MAIL +MARK MODIFY MOVE +NEXT PRINT
- +QUIT +READ REMOVE +REPLY RESPOND SEARCH
+ MAIL +MARK MODIFY MOVE +NEXT +PRINT
+ +QUIT +READ REMOVE +REPLY RESPOND +SEARCH
SEEN SELECT +SET +SHOW +SSH +UNMARK
UNSEEN +USER
diff --git a/folders/folders.go b/folders/folders.go
index 7f2245ceeddda13656ef8e1a05bb9532120e4d37..ed5cae86fc1f0cd53066f06394cef3e6d0e8e625 100644
--- a/folders/folders.go
+++ b/folders/folders.go
@@ -90,6 +90,10 @@ func FindFolder(name string) storage.Folder {
// IsFolderAccess checks if a user can access a folder.
func IsFolderAccess(name, login string) bool {
ctx := storage.Context()
+ admin, _ := this.Q.IsUserAdmin(ctx, login)
+ if admin == 1 {
+ return true
+ }
found, _ := this.Q.IsFolderAccess(ctx, storage.IsFolderAccessParams{
Name: name,
Login: login,
diff --git a/repl/command.go b/repl/command.go
index 43ee9340e5961bb89b3414602d5fdea03f6bd32c..7ed1ccb8d956005407f9cfa5ee62db97d2a3ef52 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -916,8 +916,9 @@ header. If a "search-string" is not specified, a search is made using
the previously specified string, starting with the message following the
one you are currently reading (or have just read). Once started, a
search can be aborted by typing a CTRL-C.`,
- MinArgs: 1,
+ MinArgs: 0,
MaxArgs: 1,
+ Action: ActionSearch,
Flags: dclish.Flags{
"/FOLDER": {
Description: `/FOLDER=(folder,[...])
diff --git a/repl/messages.go b/repl/messages.go
index ff2611fa55549dfdd6e779aec0794cb9d1923a7b..a0ffe012c4605845dca3464f9d97be164defd33a 100644
--- a/repl/messages.go
+++ b/repl/messages.go
@@ -13,6 +13,7 @@ import (
"git.lyda.ie/kevin/bulletin/editor"
"git.lyda.ie/kevin/bulletin/folders"
"git.lyda.ie/kevin/bulletin/pager"
+ "git.lyda.ie/kevin/bulletin/storage"
"git.lyda.ie/kevin/bulletin/this"
)
@@ -101,7 +102,6 @@ func ActionAdd(cmd *dclish.Command) error {
optExtract = 1
}
if cmd.Flags["/FOLDER"].Value != "" {
- fmt.Printf("/FOLDER = %s\n", cmd.Flags["/FOLDER"].Value)
optFolder = strings.Split(cmd.Flags["/FOLDER"].Value, ",")
}
if cmd.Flags["/INDENT"].Value == "true" {
@@ -253,14 +253,24 @@ func ActionNext(_ *dclish.Command) error {
// ActionPrint handles the `PRINT` command.
func ActionPrint(cmd *dclish.Command) error {
- // TODO: handle flags.
+ all := false
+ if cmd.Flags["/ALL"].Value == "true" {
+ all = true
+ }
+
+ ctx := storage.Context()
msgids := []int64{this.MsgID}
+ var err error
if len(cmd.Args) == 1 {
- var err error
+ if all {
+ return errors.New("Can't provide a message list and /ALL")
+ }
msgids, err = ParseNumberList(cmd.Args[0])
if err != nil {
return err
}
+ } else if all {
+ msgids, err = this.Q.ListMessageIDs(ctx, this.Folder.Name)
}
print("\033[5i")
for _, msgid := range msgids {
@@ -453,3 +463,159 @@ func ActionUnmark(cmd *dclish.Command) error {
}
return nil
}
+
+// ActionSearch handles the `SEARCH` command. This will show all messages
+// matching a search term.
+//
+// See subtoutines SEARCH and GET_SEARCH in bulletin2.for for the
+// original implementation.
+func ActionSearch(cmd *dclish.Command) error {
+ ctx := storage.Context()
+ var err error
+ optFolders := []string{this.Folder.Name}
+ if cmd.Flags["/FOLDER"].Value != "" {
+ optFolders = strings.Split(strings.ToUpper(cmd.Flags["/FOLDER"].Value), ",")
+ for i := range optFolders {
+ folder, _ := this.Q.FindFolderExact(ctx, optFolders[i])
+ if folder.Name != "" {
+ return fmt.Errorf("Folder '%s' not found", optFolders[i])
+ }
+ if folders.IsFolderAccess(optFolders[i], this.User.Login) {
+ return fmt.Errorf("Folder '%s' is not accessible", optFolders[i])
+ }
+ }
+ }
+
+ optReply := false
+ if cmd.Flags["/REPLY"].Value == "true" {
+ optReply = true
+ }
+
+ optReverse := false
+ if cmd.Flags["/REVERSE"].Value == "true" {
+ optReverse = true
+ }
+
+ optStart := int64(-1) // -1 means first message.
+ if optReverse {
+ optStart = 0 // 0 means last message.
+ }
+ if cmd.Flags["/START"].Set {
+ optStart, err = strconv.ParseInt(cmd.Flags["/START"].Value, 10, 64)
+ if err != nil {
+ return err
+ }
+ if optStart < 1 {
+ return errors.New("/START must be 1 or larger")
+ }
+ }
+
+ optSubject := false
+ if cmd.Flags["/SUBJECT"].Value == "true" {
+ optSubject = true
+ }
+
+ var searchTerm string
+ if optReply {
+ if optSubject || len(cmd.Args) == 1 {
+ return errors.New("Can't specify /REPLY and a search term or /SUBJECT")
+ }
+ msg, err := this.Q.ReadMessage(ctx, storage.ReadMessageParams{
+ Folder: this.Folder.Name,
+ ID: this.MsgID,
+ })
+ if err != nil {
+ return err
+ }
+ searchTerm = "Re: " + msg.Subject
+ } else {
+ searchTerm = cmd.Args[0]
+ }
+
+ allMsgs := []storage.Message{}
+ msgs := []storage.Message{}
+ var start int64
+ for i := range optFolders {
+ switch optStart {
+ case -1:
+ start = 1
+ case 0:
+ start, err := this.Q.LastMsgidIgnoringSeen(ctx, optFolders[i])
+ if err != nil || start == 0 {
+ continue
+ }
+ default:
+ start = optStart
+ }
+ if optReply {
+ if optReverse {
+ msgs, err = this.Q.SearchReplyReverse(ctx,
+ storage.SearchReplyReverseParams{
+ Subject: searchTerm,
+ ID: start,
+ Folder: optFolders[i],
+ })
+ } else {
+ msgs, err = this.Q.SearchReply(ctx,
+ storage.SearchReplyParams{
+ Subject: searchTerm,
+ ID: start,
+ Folder: optFolders[i],
+ })
+ }
+ } else if optSubject {
+ if optReverse {
+ msgs, err = this.Q.SearchSubjectReverse(ctx,
+ storage.SearchSubjectReverseParams{
+ Column1: nullStr(searchTerm),
+ ID: start,
+ Folder: optFolders[i],
+ })
+ } else {
+ msgs, err = this.Q.SearchSubject(ctx,
+ storage.SearchSubjectParams{
+ Column1: nullStr(searchTerm),
+ ID: start,
+ Folder: optFolders[i],
+ })
+ }
+ } else {
+ if optReverse {
+ msgs, err = this.Q.SearchReverse(ctx,
+ storage.SearchReverseParams{
+ Column1: nullStr(searchTerm),
+ ID: start,
+ Folder: optFolders[i],
+ })
+ } else {
+ msgs, err = this.Q.Search(ctx,
+ storage.SearchParams{
+ Column1: nullStr(searchTerm),
+ ID: start,
+ Folder: optFolders[i],
+ })
+ }
+ }
+ if err != nil {
+ continue
+ }
+ if len(allMsgs)+len(msgs) > 100 {
+ fmt.Println("Too many messages match; narrow search term.")
+ return nil
+ }
+ allMsgs = append(allMsgs, msgs...)
+ }
+
+ if len(allMsgs) == 0 {
+ fmt.Println("No messages found.")
+ return nil
+ }
+ buf := strings.Builder{}
+ for _, msg := range allMsgs {
+ buf.WriteString(msg.String())
+ buf.WriteString("\n\n")
+ }
+ pager.Pager(buf.String())
+
+ return nil
+}
diff --git a/repl/storage.go b/repl/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..10d8378bed27646b94a8778853391595b2c81bf8
--- /dev/null
+++ b/repl/storage.go
@@ -0,0 +1,10 @@
+package repl
+
+import "database/sql"
+
+func nullStr(s string) sql.NullString {
+ return sql.NullString{
+ String: s,
+ Valid: true,
+ }
+}
diff --git a/storage/messages.sql.go b/storage/messages.sql.go
index d804c873c029be4111beea24076c144d5af24a42..5a5fb40c24c50ea3bad2d6b292a3afe920827895 100644
--- a/storage/messages.sql.go
+++ b/storage/messages.sql.go
@@ -7,6 +7,7 @@ package storage
import (
"context"
+ "database/sql"
"time"
)
@@ -52,9 +53,9 @@ func (q *Queries) DeleteAllMessages(ctx context.Context, folder string) error {
const getLastRead = `-- name: GetLastRead :many
SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u
-WHERE folder = ? AND u.login == m.author
-GROUP BY m.author
-ORDER BY m.author
+ WHERE folder = ? AND u.login == m.author
+ GROUP BY m.author
+ ORDER BY m.author
`
type GetLastReadRow struct {
@@ -88,9 +89,9 @@ func (q *Queries) GetLastRead(ctx context.Context, folder string) ([]GetLastRead
const getLastReadByEnabled = `-- name: GetLastReadByEnabled :many
SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u
-WHERE folder = ? AND u.login == m.author AND u.disabled = ?
-GROUP BY m.author
-ORDER BY m.author
+ WHERE folder = ? AND u.login == m.author AND u.disabled = ?
+ GROUP BY m.author
+ ORDER BY m.author
`
type GetLastReadByEnabledParams struct {
@@ -128,7 +129,7 @@ func (q *Queries) GetLastReadByEnabled(ctx context.Context, arg GetLastReadByEna
const getLastReadByUser = `-- name: GetLastReadByUser :one
SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u
-WHERE folder = ? AND u.login == m.author AND m.author = ?
+ WHERE folder = ? AND u.login == m.author AND m.author = ?
`
type GetLastReadByUserParams struct {
@@ -258,6 +259,293 @@ func (q *Queries) ReadMessage(ctx context.Context, arg ReadMessageParams) (Messa
return i, err
}
+const search = `-- name: Search :many
+SELECT id, folder, author, subject, message, permanent, shutdown, expiration, create_at, update_at FROM messages
+ WHERE (message LIKE '%' || ?1 || '%'
+ OR subject LIKE '%' || ?1 || '%')
+ AND id >= ?2
+ AND folder = ?3
+`
+
+type SearchParams struct {
+ Column1 sql.NullString
+ ID int64
+ Folder string
+}
+
+func (q *Queries) Search(ctx context.Context, arg SearchParams) ([]Message, error) {
+ rows, err := q.db.QueryContext(ctx, search, arg.Column1, arg.ID, arg.Folder)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Message
+ for rows.Next() {
+ var i Message
+ if err := rows.Scan(
+ &i.ID,
+ &i.Folder,
+ &i.Author,
+ &i.Subject,
+ &i.Message,
+ &i.Permanent,
+ &i.Shutdown,
+ &i.Expiration,
+ &i.CreateAt,
+ &i.UpdateAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const searchReply = `-- name: SearchReply :many
+SELECT id, folder, author, subject, message, permanent, shutdown, expiration, create_at, update_at FROM messages
+ WHERE subject = ?
+ AND id >= ?
+ AND folder = ?
+`
+
+type SearchReplyParams struct {
+ Subject string
+ ID int64
+ Folder string
+}
+
+func (q *Queries) SearchReply(ctx context.Context, arg SearchReplyParams) ([]Message, error) {
+ rows, err := q.db.QueryContext(ctx, searchReply, arg.Subject, arg.ID, arg.Folder)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Message
+ for rows.Next() {
+ var i Message
+ if err := rows.Scan(
+ &i.ID,
+ &i.Folder,
+ &i.Author,
+ &i.Subject,
+ &i.Message,
+ &i.Permanent,
+ &i.Shutdown,
+ &i.Expiration,
+ &i.CreateAt,
+ &i.UpdateAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const searchReplyReverse = `-- name: SearchReplyReverse :many
+SELECT id, folder, author, subject, message, permanent, shutdown, expiration, create_at, update_at FROM messages
+ WHERE subject = ?
+ AND id <= ?
+ AND folder = ?
+ ORDER BY DESC
+`
+
+type SearchReplyReverseParams struct {
+ Subject string
+ ID int64
+ Folder string
+}
+
+func (q *Queries) SearchReplyReverse(ctx context.Context, arg SearchReplyReverseParams) ([]Message, error) {
+ rows, err := q.db.QueryContext(ctx, searchReplyReverse, arg.Subject, arg.ID, arg.Folder)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Message
+ for rows.Next() {
+ var i Message
+ if err := rows.Scan(
+ &i.ID,
+ &i.Folder,
+ &i.Author,
+ &i.Subject,
+ &i.Message,
+ &i.Permanent,
+ &i.Shutdown,
+ &i.Expiration,
+ &i.CreateAt,
+ &i.UpdateAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const searchReverse = `-- name: SearchReverse :many
+SELECT id, folder, author, subject, message, permanent, shutdown, expiration, create_at, update_at FROM messages
+ WHERE (message LIKE '%' || ?1 || '%'
+ OR subject LIKE '%' || ?1 || '%')
+ AND id <= ?2
+ AND folder = ?3
+ ORDER BY DESC
+`
+
+type SearchReverseParams struct {
+ Column1 sql.NullString
+ ID int64
+ Folder string
+}
+
+func (q *Queries) SearchReverse(ctx context.Context, arg SearchReverseParams) ([]Message, error) {
+ rows, err := q.db.QueryContext(ctx, searchReverse, arg.Column1, arg.ID, arg.Folder)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Message
+ for rows.Next() {
+ var i Message
+ if err := rows.Scan(
+ &i.ID,
+ &i.Folder,
+ &i.Author,
+ &i.Subject,
+ &i.Message,
+ &i.Permanent,
+ &i.Shutdown,
+ &i.Expiration,
+ &i.CreateAt,
+ &i.UpdateAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const searchSubject = `-- name: SearchSubject :many
+SELECT id, folder, author, subject, message, permanent, shutdown, expiration, create_at, update_at FROM messages
+ WHERE subject LIKE '%' || ? || '%'
+ AND id >= ?
+ AND folder = ?
+`
+
+type SearchSubjectParams struct {
+ Column1 sql.NullString
+ ID int64
+ Folder string
+}
+
+func (q *Queries) SearchSubject(ctx context.Context, arg SearchSubjectParams) ([]Message, error) {
+ rows, err := q.db.QueryContext(ctx, searchSubject, arg.Column1, arg.ID, arg.Folder)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Message
+ for rows.Next() {
+ var i Message
+ if err := rows.Scan(
+ &i.ID,
+ &i.Folder,
+ &i.Author,
+ &i.Subject,
+ &i.Message,
+ &i.Permanent,
+ &i.Shutdown,
+ &i.Expiration,
+ &i.CreateAt,
+ &i.UpdateAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const searchSubjectReverse = `-- name: SearchSubjectReverse :many
+SELECT id, folder, author, subject, message, permanent, shutdown, expiration, create_at, update_at FROM messages
+ WHERE subject LIKE '%' || ? || '%'
+ AND id <= ?
+ AND folder = ?
+ ORDER BY DESC
+`
+
+type SearchSubjectReverseParams struct {
+ Column1 sql.NullString
+ ID int64
+ Folder string
+}
+
+func (q *Queries) SearchSubjectReverse(ctx context.Context, arg SearchSubjectReverseParams) ([]Message, error) {
+ rows, err := q.db.QueryContext(ctx, searchSubjectReverse, arg.Column1, arg.ID, arg.Folder)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Message
+ for rows.Next() {
+ var i Message
+ if err := rows.Scan(
+ &i.ID,
+ &i.Folder,
+ &i.Author,
+ &i.Subject,
+ &i.Message,
+ &i.Permanent,
+ &i.Shutdown,
+ &i.Expiration,
+ &i.CreateAt,
+ &i.UpdateAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const setMessageSeen = `-- name: SetMessageSeen :exec
INSERT INTO seen (login, folder, msgid) VALUES (?, ?, ?)
`
diff --git a/storage/migrations/1_create_table.up.sql b/storage/migrations/1_create_table.up.sql
index 46dfee3deadfdb098d29d47f3c288e045e1d5d33..acec6f82f018420ea63ef35cbb93fce6db95f451 100644
--- a/storage/migrations/1_create_table.up.sql
+++ b/storage/migrations/1_create_table.up.sql
@@ -3,7 +3,7 @@ CREATE TABLE users (
name VARCHAR(53) NOT NULL,
admin INT DEFAULT 0 NOT NULL,
moderator INT DEFAULT 0 NOT NULL,
- alert INT NOT NULL DEFAULT 0, --- 0=no, 1=brief, 2=readnew
+ alert INT DEFAULT 0 NOT NULL, --- 0=no, 1=brief, 2=readnew
disabled INT DEFAULT 0 NOT NULL,
last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
diff --git a/storage/queries/messages.sql b/storage/queries/messages.sql
index d6f48c654c0157869d4a1a7012b61d6eeed1cd1d..901d71c0e471aebc4f27552895b7e7a6b3df4dd6 100644
--- a/storage/queries/messages.sql
+++ b/storage/queries/messages.sql
@@ -41,16 +41,57 @@ SELECT CAST(MAX(id) AS INT) FROM messages AS m WHERE m.folder = ?1;
--- TODO: These get the max message written, not read. Leaving for now; easier to test.
-- name: GetLastRead :many
SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u
-WHERE folder = ? AND u.login == m.author
-GROUP BY m.author
-ORDER BY m.author;
+ WHERE folder = ? AND u.login == m.author
+ GROUP BY m.author
+ ORDER BY m.author;
-- name: GetLastReadByEnabled :many
SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u
-WHERE folder = ? AND u.login == m.author AND u.disabled = ?
-GROUP BY m.author
-ORDER BY m.author;
+ WHERE folder = ? AND u.login == m.author AND u.disabled = ?
+ GROUP BY m.author
+ ORDER BY m.author;
-- name: GetLastReadByUser :one
SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u
-WHERE folder = ? AND u.login == m.author AND m.author = ?;
+ WHERE folder = ? AND u.login == m.author AND m.author = ?;
+
+-- name: Search :many
+SELECT * FROM messages
+ WHERE (message LIKE '%' || ?1 || '%'
+ OR subject LIKE '%' || ?1 || '%')
+ AND id >= ?2
+ AND folder = ?3;
+
+-- name: SearchReverse :many
+SELECT * FROM messages
+ WHERE (message LIKE '%' || ?1 || '%'
+ OR subject LIKE '%' || ?1 || '%')
+ AND id <= ?2
+ AND folder = ?3
+ ORDER BY DESC;
+
+-- name: SearchSubject :many
+SELECT * FROM messages
+ WHERE subject LIKE '%' || ? || '%'
+ AND id >= ?
+ AND folder = ?;
+
+-- name: SearchSubjectReverse :many
+SELECT * FROM messages
+ WHERE subject LIKE '%' || ? || '%'
+ AND id <= ?
+ AND folder = ?
+ ORDER BY DESC;
+
+-- name: SearchReply :many
+SELECT * FROM messages
+ WHERE subject = ?
+ AND id >= ?
+ AND folder = ?;
+
+-- name: SearchReplyReverse :many
+SELECT * FROM messages
+ WHERE subject = ?
+ AND id <= ?
+ AND folder = ?
+ ORDER BY DESC;
diff --git a/storage/queries/standard.sql b/storage/queries/standard.sql
index cff89bb0f1d82c98ae4510513db2397f47a73b04..bd37e86e8fef6674b656494ed03027088f59e6ea 100644
--- a/storage/queries/standard.sql
+++ b/storage/queries/standard.sql
@@ -37,6 +37,9 @@ INSERT INTO messages (id, folder) VALUES (?, ?);
-- name: ListMessages :many
SELECT * FROM messages WHERE folder = ? ORDER BY id;
+-- name: ListMessageIDs :many
+SELECT id FROM messages WHERE folder = ? ORDER BY id;
+
-- name: GetMessage :one
SELECT * FROM messages WHERE id = ? AND folder = ?;
diff --git a/storage/standard.sql.go b/storage/standard.sql.go
index 69f0f8f40daa617c9ac334bc6348fb51d197d827..f6c4615c285fbb883b1ae91d779314aa26cc9bb4 100644
--- a/storage/standard.sql.go
+++ b/storage/standard.sql.go
@@ -530,6 +530,33 @@ func (q *Queries) ListMark(ctx context.Context, arg ListMarkParams) ([]Mark, err
return items, nil
}
+const listMessageIDs = `-- name: ListMessageIDs :many
+SELECT id FROM messages WHERE folder = ? ORDER BY id
+`
+
+func (q *Queries) ListMessageIDs(ctx context.Context, folder string) ([]int64, error) {
+ rows, err := q.db.QueryContext(ctx, listMessageIDs, folder)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []int64
+ for rows.Next() {
+ var id int64
+ if err := rows.Scan(&id); err != nil {
+ return nil, err
+ }
+ items = append(items, id)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const listMessages = `-- name: ListMessages :many
SELECT id, folder, author, subject, message, permanent, shutdown, expiration, create_at, update_at FROM messages WHERE folder = ? ORDER BY id
`