Select Git revision
messages.go
messages.go 16.95 KiB
// 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
}