// Package repl implements the main event loop. package repl import ( "errors" "fmt" "strconv" "strings" "time" "git.lyda.ie/kevin/bulletin/ask" "git.lyda.ie/kevin/bulletin/dclish" "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" ) // ActionDirectory handles the `DIRECTORY` command. This lists all the // messages in the current folder. func ActionDirectory(cmd *dclish.Command) error { // TODO: flag parsing. showExpiration := false if cmd.Flags["/EXPIRATION"].Value == "true" { showExpiration = true } if len(cmd.Args) == 1 { folder, err := folders.ValidFolder(cmd.Args[0]) if err != nil { fmt.Println("Folder does not exist.") return nil } if !folders.IsFolderAccess(folder.Name, this.User.Login) { fmt.Println("No permission to access folder.") return nil } this.Folder = folder this.ReadFirstCall = true } msgs, err := folders.ListMessages(this.Folder.Name) if err != nil { return err } if len(msgs) == 0 { fmt.Println("There are no messages present.") return nil } buf := strings.Builder{} buf.WriteString(fmt.Sprintf("%4s %-43s %-12s %-10s\n", "#", "Subject", "From", "Date")) for _, msg := range msgs { buf.WriteString(fmt.Sprint(msg.OneLine(showExpiration))) } pager.Pager(buf.String()) return nil } // ActionAdd handles the `ADD` command. This adds a message to a folder. func ActionAdd(cmd *dclish.Command) error { optAll := 0 optBell := 0 optBroadcast := 0 optEdit := 0 optExpiration := &time.Time{} optExtract := 0 optFolder := []string{} optIndent := 0 optPermanent := 0 optShutdown := 0 optSignature := 0 optSubject := "" optSystem := 0 if cmd.Flags["/ALL"].Value == "true" { optAll = 1 } if cmd.Flags["/BELL"].Value == "true" { optBell = 1 } if cmd.Flags["/BROADCAST"].Value == "true" { optBroadcast = 1 } if cmd.Flags["/EDIT"].Value == "true" { optEdit = 1 } if cmd.Flags["/EXPIRATION"].Value != "" { // dd-mmm-yyyy, or delta time: dddd exp, err := time.Parse("2006-01-02", cmd.Flags["/EXPIRATION"].Value) if err != nil { days, err := strconv.Atoi(cmd.Flags["/EXPIRATION"].Value) if err != nil { optExpiration = nil } exp := time.Now().AddDate(0, 0, days) optExpiration = &exp } else { optExpiration = &exp } } if cmd.Flags["/EXTRACT"].Value == "true" { optExtract = 1 } if cmd.Flags["/FOLDER"].Value != "" { optFolder = strings.Split(cmd.Flags["/FOLDER"].Value, ",") } if cmd.Flags["/INDENT"].Value == "true" { optIndent = 1 } if cmd.Flags["/PERMANENT"].Value == "true" { optPermanent = 1 } if cmd.Flags["/SHUTDOWN"].Value == "true" { optShutdown = 1 } if cmd.Flags["/SIGNATURE"].Value == "true" { optSignature = 1 } if cmd.Flags["/SUBJECT"].Value != "" { optSubject = cmd.Flags["/SUBJECT"].Value } if cmd.Flags["/SYSTEM"].Value == "true" { optSystem = 1 } fmt.Printf("TODO: optAll is not yet implemented - you set it to %d\n", optAll) fmt.Printf("TODO: optBell is not yet implemented - you set it to %d\n", optBell) fmt.Printf("TODO: optBroadcast is not yet implemented - you set it to %d\n", optBroadcast) fmt.Printf("TODO: optEdit is not yet implemented - you set it to %d\n", optEdit) fmt.Printf("TODO: optExtract is not yet implemented - you set it to %d\n", optExtract) fmt.Printf("TODO: optIndent is not yet implemented - you set it to %d\n", optIndent) fmt.Printf("TODO: optSignature is not yet implemented - you set it to %d\n", optSignature) fmt.Printf("TODO: optSystem is not yet implemented - you set it to %d\n", optSystem) if len(optFolder) == 0 { optFolder = []string{this.Folder.Name} } // TODO: check if folders exist. if optSubject == "" { optSubject, _ = ask.GetLine("Enter subject of message: ") if optSubject == "" { return errors.New("Must enter a subject") } } // TODO: check we have permission for shutdown and permanent message, err := editor.Editor( fmt.Sprintf("Enter message for '%s'...", optSubject), "Edit message", "") if err != nil { return err } for i := range optFolder { err = folders.CreateMessage(this.User.Login, optSubject, message, optFolder[i], optPermanent, optShutdown, optExpiration) } return nil } // ActionCurrent handles the `CURRENT` command. func ActionCurrent(_ *dclish.Command) error { msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, this.MsgID) if err != nil { return err } lines := strings.Split(msg.String(), "\n") if len(lines) > 10 { lines = lines[:10] } fmt.Printf("%s\n", strings.Join(lines, "\n")) return nil } // ActionBack handles the `BACK` command. func ActionBack(_ *dclish.Command) error { msgid := folders.PrevMsgid(this.User.Login, this.Folder.Name, this.MsgID) if msgid == 0 { fmt.Println("No previous messages") return nil } msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid) if err != nil { return err } if pager.Pager(msg.String()) { this.MsgID = msgid folders.MarkSeen([]int64{msgid}) } return nil } // ActionChange handles the `CHANGE` command. This replaces or modifies // an existing message. func ActionChange(cmd *dclish.Command) error { var err error optAll := false if cmd.Flags["/ALL"].Value == "true" { optAll = true } optExpiration := &time.Time{} if cmd.Flags["/EXPIRATION"].Value != "" { // dd-mmm-yyyy, or delta time: dddd exp, err := time.Parse("2006-01-02", cmd.Flags["/EXPIRATION"].Value) if err != nil { days, err := strconv.Atoi(cmd.Flags["/EXPIRATION"].Value) if err != nil { optExpiration = nil } exp := time.Now().AddDate(0, 0, days) optExpiration = &exp } else { optExpiration = &exp } } optGeneral := false if cmd.Flags["/GENERAL"].Set && this.Folder.Name != "GENERAL" { return errors.New("Can only be used in the GENERAL folder") } if cmd.Flags["/GENERAL"].Value == "true" { optGeneral = true } if cmd.Flags["/GENERAL"].Set && !optGeneral { return errors.New("Can't specify /NOGENERAL - see /SYSTEM") } optNew := false if cmd.Flags["/NEW"].Value == "true" { optNew = true } optNumber := []int64{this.MsgID} if cmd.Flags["/NUMBER"].Set && cmd.Flags["/NUMBER"].Value == "" { return errors.New("Must supply message number(s) if /NUMBER is set") } if cmd.Flags["/NUMBER"].Value != "" { optNumber, err = ParseNumberList(cmd.Flags["/NUMBER"].Value) if err != nil { return err } } optPermanent := false if cmd.Flags["/PERMANENT"].Value == "true" { optPermanent = true } optShutdown := false if cmd.Flags["/SHUTDOWN"].Value == "true" { optShutdown = true } optSubject := "" if cmd.Flags["/SUBJECT"].Set && cmd.Flags["/SUBJECT"].Value == "" { return errors.New("Must supply subject text if /SUBJECT is set") } if cmd.Flags["/SUBJECT"].Value != "" { optSubject = cmd.Flags["/SUBJECT"].Value } optSystem := false if cmd.Flags["/SYSTEM"].Set { if this.User.Admin == 0 { return errors.New("Must be admin") } if this.Folder.Name != "GENERAL" { return errors.New("Can only be used in the GENERAL folder") } } if cmd.Flags["/SYSTEM"].Value == "true" { optSystem = true } if cmd.Flags["/SYSTEM"].Set && !optSystem { return errors.New("Can't specify /NOSYSTEM - see /GENERAL") } optText := false if cmd.Flags["/TEXT"].Value == "true" { optText = true } // Sanity checking. if optSystem && optGeneral { return errors.New("Can't specify /SYSTEM and /GENERAL") } fmt.Println("TODO: flags parsed, now need to do something.") return nil } // ActionFirst handles the `FIRST` command. func ActionFirst(_ *dclish.Command) error { msgid := folders.FirstMessage(this.Folder.Name) if msgid == 0 { fmt.Println("No messages in folder") return nil } this.MsgID = msgid msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid) if err != nil { return err } if pager.Pager(msg.String()) { folders.MarkSeen([]int64{msgid}) } return nil } // ActionLast handles the `LAST` command. func ActionLast(_ *dclish.Command) error { msgid := folders.LastMessage(this.Folder.Name) if msgid == 0 { fmt.Println("No messages in folder") return nil } this.MsgID = msgid msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid) if err != nil { return err } if pager.Pager(msg.String()) { folders.MarkSeen([]int64{msgid}) } return nil } // ActionNext handles the `NEXT` command. func ActionNext(_ *dclish.Command) error { msgid := folders.NextMsgid(this.User.Login, this.Folder.Name, this.MsgID) if msgid == 0 { fmt.Println("No next messages") return nil } msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid) if err != nil { return err } pager.Pager(msg.String()) if pager.Pager(msg.String()) { folders.MarkSeen([]int64{msgid}) this.MsgID = msgid } return nil } // ActionPrint handles the `PRINT` command. func ActionPrint(cmd *dclish.Command) error { 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 { 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 { msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid) if err != nil { fmt.Printf("Message %d not found.\n", msgid) } else { fmt.Print(msg.String()) } print("\n\v") } print("\033[4i") return nil } // ActionRead handles the `READ` command. func ActionRead(cmd *dclish.Command) error { defer func() { this.ReadFirstCall = false }() msgid := this.MsgID if !this.ReadFirstCall && len(cmd.Args) == 0 { msgid = folders.NextMsgid(this.User.Login, this.Folder.Name, msgid) if msgid < this.MsgID { fmt.Println("No more unread messages.") return nil } } else if len(cmd.Args) == 1 { var err error msgid, err = strconv.ParseInt(cmd.Args[0], 10, 64) if err != nil { return err } } this.MsgID = msgid msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid) if err != nil { return err } if pager.Pager(msg.String()) { folders.MarkSeen([]int64{msgid}) } return nil } // ActionReply handles the `REPLY` command. func ActionReply(cmd *dclish.Command) error { extract := true if cmd.Flags["/EXTRACT"].Value == "false" { extract = false } indent := true if cmd.Flags["/INDENT"].Value == "false" { indent = false } original, err := folders.ReadMessage(this.User.Login, this.Folder.Name, this.MsgID) origMsg := "" if extract { if indent { origMsg = "> " + strings.Join(strings.Split(original.Message, "\n"), "\n> ") } else { origMsg = original.Message } } subject := "Re: " + original.Subject message, err := editor.Editor( fmt.Sprintf("Enter message for '%s'...", subject), "Edit message", origMsg) if err != nil { fmt.Printf("ERROR: Editor failure (%s).\n", err) return nil } err = folders.CreateMessage(this.User.Login, subject, message, this.Folder.Name, 0, 0, nil) if err != nil { fmt.Printf("ERROR: CreateMessage failure (%s).\n", err) return nil } return nil } // ActionForward handles the `FORWARD` command. func ActionForward(cmd *dclish.Command) error { fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description) return nil } // ActionSeen handles the `SEEN` command. func ActionSeen(cmd *dclish.Command) error { // TODO: review help. var err error msgids := []int64{this.MsgID} if len(cmd.Args) == 1 { msgids, err = ParseNumberList(cmd.Args[0]) if err != nil { return err } } err = folders.MarkSeen(msgids) if err != nil { fmt.Printf("ERROR: %s.\n", err) } return nil } // ActionUnseen handles the `UNSEEN` command. func ActionUnseen(cmd *dclish.Command) error { // TODO: review help. var err error msgids := []int64{this.MsgID} if len(cmd.Args) == 1 { msgids, err = ParseNumberList(cmd.Args[0]) if err != nil { return err } } err = folders.MarkUnseen(msgids) if err != nil { fmt.Printf("ERROR: %s.\n", err) } return nil } // ActionDelete handles the `DELETE` command. func ActionDelete(cmd *dclish.Command) error { // TODO: Follow permissions. var err error all := false if cmd.Flags["/ALL"].Value == "true" { all = true } if all { if len(cmd.Args) == 1 { fmt.Println("ERROR: Can't specify both message numbers and /ALL flag.") return nil } err := folders.DeleteAllMessages() if err != nil { fmt.Printf("ERROR: %s.\n", err) } return nil } msgids := []int64{this.MsgID} if len(cmd.Args) == 1 { msgids, err = ParseNumberList(cmd.Args[0]) if err != nil { fmt.Printf("ERROR: %s.\n", err) return nil } } err = folders.DeleteMessages(msgids) if err != nil { fmt.Printf("ERROR: %s.\n", err) } return nil } // ActionMark handles the `MARK` command. func ActionMark(cmd *dclish.Command) error { var err error msgids := []int64{this.MsgID} if len(cmd.Args) == 1 { msgids, err = ParseNumberList(cmd.Args[0]) if err != nil { return err } } err = folders.SetMark(msgids) if err != nil { fmt.Printf("ERROR: %s.\n", err) } return nil } // ActionUnmark handles the `UNMARK` command. func ActionUnmark(cmd *dclish.Command) error { var err error msgids := []int64{this.MsgID} if len(cmd.Args) == 1 { msgids, err = ParseNumberList(cmd.Args[0]) if err != nil { return err } } err = folders.UnsetMark(msgids) if err != nil { fmt.Printf("ERROR: %s.\n", err) } 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 }