Skip to content
Snippets Groups Projects
Select Git revision
  • 74bfcc461c975ceeba1382ccefd2405d0479bf86
  • release default protected
  • more-testing
  • attempt-vax90b1
  • attempt-1
  • conversion protected
  • linux
  • v0.9.1 protected
  • v0.9.0 protected
9 results

messages.go

Blame
  • 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
    }