Loading NOTES.md +6 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. Loading @@ -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 Loading folders/folders.go +4 −0 Original line number Diff line number Diff line Loading @@ -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, Loading repl/command.go +2 −1 Original line number Diff line number Diff line Loading @@ -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,[...]) Loading repl/messages.go +169 −3 Original line number Diff line number Diff line Loading @@ -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" ) Loading Loading @@ -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" { Loading Loading @@ -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} if len(cmd.Args) == 1 { var err error if len(cmd.Args) == 1 { 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 { Loading Loading @@ -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 } repl/storage.go 0 → 100644 +10 −0 Original line number Diff line number Diff line package repl import "database/sql" func nullStr(s string) sql.NullString { return sql.NullString{ String: s, Valid: true, } } Loading
NOTES.md +6 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. Loading @@ -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 Loading
folders/folders.go +4 −0 Original line number Diff line number Diff line Loading @@ -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, Loading
repl/command.go +2 −1 Original line number Diff line number Diff line Loading @@ -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,[...]) Loading
repl/messages.go +169 −3 Original line number Diff line number Diff line Loading @@ -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" ) Loading Loading @@ -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" { Loading Loading @@ -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} if len(cmd.Args) == 1 { var err error if len(cmd.Args) == 1 { 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 { Loading Loading @@ -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 }
repl/storage.go 0 → 100644 +10 −0 Original line number Diff line number Diff line package repl import "database/sql" func nullStr(s string) sql.NullString { return sql.NullString{ String: s, Valid: true, } }