diff --git a/NOTES.md b/NOTES.md
index 15e3277de66fe844ef30af6fd84b4ed5719f53dc..16aa923e155942dfdd07ac96fdfcde6441b590f0 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -29,7 +29,7 @@ repl.commands?
 
   * Implement each command.
     * Next: folder commands - ~~CREATE~~, ~~REMOVE~~, MODIFY, ~~INDEX~~, ~~SELECT~~
-    * Messages: ~~ADD~~, CURRENT, DIRECTORY, BACK, CHANGE, FIRST, REMOVE, NEXT, READ
+    * Messages: ~~ADD~~, CURRENT, ~~DIRECTORY~~, BACK, CHANGE, FIRST, REMOVE, NEXT, ~~READ~~
     * Messages edit: CHANGE, REPLY, FORWARD
     * Moving messages: COPY, MOVE
     * Compound commands: SET and SHOW
@@ -60,7 +60,8 @@ repl.commands?
     * Commands for a local mail system?
     * Commands to connect to Mattermost or mastodon?
     * Commands to manage users.
-  * `SHOW VERSION` - use [this](https://github.com/earthboundkid/versioninfo)
+  * `SHOW VERSION` - versioninfo doesn't work; what else?
+  * Check db version; notify user if it changes; refuse to write to db if it has.
 
 ## Module links
 
diff --git a/dclish/dclish.go b/dclish/dclish.go
index 0302781f228afc4fcd836574770c2a08d8d540eb..237f1978d6f0328c5241cf7d91aa803ea685b382 100644
--- a/dclish/dclish.go
+++ b/dclish/dclish.go
@@ -140,6 +140,10 @@ func (c Commands) ParseAndRun(line string) error {
 	}
 	cmd.Args = []string{}
 	if len(words) == 1 {
+		if len(cmd.Args) < cmd.MinArgs {
+			fmt.Println("ERROR: Not enough args.")
+			return nil
+		}
 		return cmd.Action(cmd)
 	}
 	// TODO: need to clean this up.
diff --git a/editor/tview.go b/editor/tview.go
index 541678df070270cc4140060d9df0b6bdee07641c..50f3ab9b747f5130dc8b41fb654580053a345935 100644
--- a/editor/tview.go
+++ b/editor/tview.go
@@ -10,6 +10,20 @@ import (
 
 // Editor is the editor for text files.
 func Editor(placeholder, title string) (string, error) {
+	theme := tview.Theme{
+		PrimitiveBackgroundColor:    tcell.ColorDefault,
+		ContrastBackgroundColor:     tcell.ColorDefault,
+		MoreContrastBackgroundColor: tcell.ColorDefault,
+		BorderColor:                 tcell.ColorDefault,
+		TitleColor:                  tcell.ColorDefault,
+		GraphicsColor:               tcell.ColorDefault,
+		PrimaryTextColor:            tcell.ColorDefault,
+		SecondaryTextColor:          tcell.ColorDefault,
+		TertiaryTextColor:           tcell.ColorDefault,
+		InverseTextColor:            tcell.ColorDefault,
+		ContrastSecondaryTextColor:  tcell.ColorDefault,
+	}
+	tview.Styles = theme
 	app := tview.NewApplication()
 
 	textArea := tview.NewTextArea().
diff --git a/folders/folders.go b/folders/folders.go
index 04b69a13ca5d24aefecbe0dfa7284dc2689dbc09..811f10bcedbff5e49d9c60fd2f7c95d5ea0c8114 100644
--- a/folders/folders.go
+++ b/folders/folders.go
@@ -4,6 +4,7 @@ package folders
 import (
 	"errors"
 	"fmt"
+	"strings"
 
 	"github.com/jmoiron/sqlx"
 )
@@ -34,6 +35,10 @@ type FolderCreateOptions struct {
 
 // CreateFolder creates a new folder.
 func (s *Store) CreateFolder(name string, options FolderCreateOptions) error {
+	if !IsAlphaNum(name) {
+		return errors.New("Folder can only have letters and numbers")
+	}
+	name = strings.ToUpper(name)
 	_, err := s.db.Exec(
 		`INSERT INTO folders
 			(name, always, brief, description, notify, owner, readnew,
@@ -52,6 +57,7 @@ func (s *Store) CreateFolder(name string, options FolderCreateOptions) error {
 		options.Expire,
 		options.Visibility,
 	)
+	// TODO: turn _ into rows and make sure it was added.
 	// TODO: process this error a bit more to give a better error message.
 	return err
 }
@@ -102,6 +108,19 @@ func (s *Store) ListFolder(user string, _ FolderListOptions) ([]FolderListRow, e
 	return flr, nil
 }
 
+// FindFolder finds a folder based on the prefix.
+func (s *Store) FindFolder(name string) string {
+	var folder string
+	s.db.Get(&folder, "SELECT name FROM folders where name = $1", name)
+	if folder != "" {
+		return folder
+	}
+	s.db.Get(&folder,
+		`SELECT name FROM folders where name LIKE $1
+	     ORDER BY name LIMIT 1`, name+"%")
+	return folder
+}
+
 // IsFolderAccess checks if a user can access a folder.
 func (s *Store) IsFolderAccess(name, user string) bool {
 	found := 0
diff --git a/folders/messages.go b/folders/messages.go
index abb310626039466f52182ccf7d2b33720566934a..bd08ec9e7b1ee7a089f1f2f9cb70b2e61bccbdaa 100644
--- a/folders/messages.go
+++ b/folders/messages.go
@@ -2,6 +2,7 @@
 package folders
 
 import (
+	"errors"
 	"fmt"
 	"strings"
 	"time"
@@ -66,16 +67,45 @@ func (m *Message) String() string {
 // ReadMessage reads a message for a user.
 func (s *Store) ReadMessage(login, folder string, msgid int) (*Message, error) {
 	msg := &Message{}
-	err := s.db.Get(msg,
+	s.db.Get(msg,
 		`SELECT id, folder, author, subject, message, expiration, create_at, update_at
-			      FROM messages WHERE folder = $1, id = $2`, folder, msgid)
-	if err != nil {
-		return nil, err
+			      FROM messages WHERE folder = $1 AND id = $2`, folder, msgid)
+	if msg.ID != msgid || msgid == 0 {
+		return nil, errors.New("Specified message was not found")
 	}
 	// TODO: replace _ with rows and check.
-	_, err = s.db.Exec(
+	s.db.Exec(
 		"INSERT INTO read (login, folder, msgid) VALUES ($1, $2, $3)",
 		login, folder, msgid)
 
-	return msg, err
+	return msg, nil
+}
+
+// ListMessagesOptions has the options for a ListMessages call.
+type ListMessagesOptions struct {
+	// TODO: is this needed?  Maybe for message order?
+}
+
+// ListMessages lists messages.
+func (s *Store) ListMessages(folder string, options *ListMessagesOptions) ([]Message, error) {
+	messages := []Message{}
+	if options != nil {
+		return messages, errors.New("TODO: options aren't implemented")
+	}
+	rows, err := s.db.Queryx(
+		`SELECT id, folder, author, subject, message, expiration, create_at, update_at
+			      FROM messages WHERE folder = $1`, folder)
+	if err != nil {
+		return messages, nil
+	}
+	for rows.Next() {
+		msg := Message{}
+		err = rows.StructScan(&msg)
+		if err != nil {
+			return []Message{}, nil
+		}
+		messages = append(messages, msg)
+	}
+	return messages, nil
+
 }
diff --git a/folders/sql/1_create_table.down.sql b/folders/sql/1_create_table.down.sql
index 4070921fe631b62d42878b713da994824eacb233..8a1a47efb2c60d66a1e51323a8ca98c71013d3e2 100644
--- a/folders/sql/1_create_table.down.sql
+++ b/folders/sql/1_create_table.down.sql
@@ -1,4 +1,11 @@
 --- Dropped in reverse order to deal with foreign keys.
+DROP TABLE mark;
+DROP TABLE read;
+
+DROP INDEX messages_idx_expiration;
+DROP INDEX messages_idx_shutdown;
+DROP TABLE messages;
+
 DROP TRIGGER co_owners_after_update_update_at;
 DROP TABLE co_owners;
 
diff --git a/folders/sql/1_create_table.up.sql b/folders/sql/1_create_table.up.sql
index 86631a20bf01f5baff868703dd5bf6badd3c1d9c..c8f55c2f0fe97672adcf357c6e56bc2f817c8b41 100644
--- a/folders/sql/1_create_table.up.sql
+++ b/folders/sql/1_create_table.up.sql
@@ -1,5 +1,5 @@
 CREATE TABLE users (
-  login       VARCHAR(25)  NOT NULL PRIMARY KEY,
+  login       VARCHAR(12)  NOT NULL PRIMARY KEY,
   name        VARCHAR(53)  NOT NULL,
   admin       INT          DEFAULT 0,
   disabled    INT          DEFAULT 0,
@@ -122,7 +122,19 @@ CREATE TABLE read (
   folder      VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
   msgid       INT,
   PRIMARY KEY (folder, login, msgid),
-  CONSTRAINT FK_id_folder
+  CONSTRAINT read_fk_id_folder
+    FOREIGN KEY (msgid, folder)
+    REFERENCES messages(id, folder)
+    ON DELETE CASCADE
+    ON UPDATE CASCADE
+) WITHOUT ROWID;
+
+CREATE TABLE mark (
+  login       VARCHAR(25) REFERENCES users(login) ON DELETE CASCADE ON UPDATE CASCADE,
+  folder      VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
+  msgid       INT,
+  PRIMARY KEY (folder, login, msgid),
+  CONSTRAINT mark_fk_id_folder
     FOREIGN KEY (msgid, folder)
     REFERENCES messages(id, folder)
     ON DELETE CASCADE
diff --git a/folders/verify.go b/folders/verify.go
new file mode 100644
index 0000000000000000000000000000000000000000..ab28a6ace05b2bbca1e9a3aec7be168ddd32f362
--- /dev/null
+++ b/folders/verify.go
@@ -0,0 +1,14 @@
+// Package folders are all the routines and sql for managing folders.
+package folders
+
+import "unicode"
+
+// IsAlphaNum checks that a string just contains a-z0-9.
+func IsAlphaNum(s string) bool {
+	for _, r := range s {
+		if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
+			return false
+		}
+	}
+	return true
+}
diff --git a/repl/command.go b/repl/command.go
index f597551573cb8caa247c6f9c24ab3999bd835bf0..11913d98e5e115c859d880e661a41e87cffa280e 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -1214,6 +1214,7 @@ After selecting a folder, the user will notified of the number of unread
 messages,  and  the  message  pointer will be placed at the first unread
 message.`,
 		Action:  ActionSelect,
+		MinArgs: 1,
 		MaxArgs: 1,
 		Flags: dclish.Flags{
 			"/MARKED": {
diff --git a/repl/folders.go b/repl/folders.go
index 2d9351f2bb65560b167fb1a9574becb64c1a1ee2..900f8b74dbd806b335238370b975aed3187305ed 100644
--- a/repl/folders.go
+++ b/repl/folders.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"strconv"
+	"strings"
 
 	"git.lyda.ie/kevin/bulletin/accounts"
 	"git.lyda.ie/kevin/bulletin/dclish"
@@ -28,6 +29,7 @@ func ActionIndex(_ *dclish.Command) error {
 
 // ActionCreate handles the `CREATE` command.  This creates a folder.
 func ActionCreate(cmd *dclish.Command) error {
+	// Populate options...
 	options := folders.FolderCreateOptions{}
 	if cmd.Flags["/ALWAYS"].Value == "true" {
 		options.Always = 1
@@ -35,11 +37,9 @@ func ActionCreate(cmd *dclish.Command) error {
 	if cmd.Flags["/BRIEF"].Value == "true" {
 		options.Brief = 1
 	}
-	if cmd.Flags["/DESCRIPTION"].Value == "" {
-		// TODO: prompt the user for a description.
-		return errors.New("Description is required - use /DESCRIPTION")
+	if cmd.Flags["/DESCRIPTION"].Value != "" {
+		options.Description = cmd.Flags["/DESCRIPTION"].Value
 	}
-	options.Description = cmd.Flags["/DESCRIPTION"].Value
 	if cmd.Flags["/NOTIFY"].Value == "true" {
 		options.Notify = 1
 	}
@@ -75,6 +75,17 @@ func ActionCreate(cmd *dclish.Command) error {
 		options.Visibility = folders.FolderSemiPrivate
 	}
 
+	// Verify options...
+	if options.Description == "" {
+		var err error
+		options.Description, err = accounts.GetLine("Enter one line description of folder: ")
+		if err != nil {
+			return nil
+		}
+	}
+	if options.Description == "" || len(options.Description) > 53 {
+		return errors.New("Description must exist and be under 53 characters")
+	}
 	err := accounts.User.Folders.CreateFolder(cmd.Args[0], options)
 	// TODO: handle the /ID flag.
 	return err
@@ -82,10 +93,22 @@ func ActionCreate(cmd *dclish.Command) error {
 
 // ActionSelect handles the `SELECT` command.  This selects a folder.
 func ActionSelect(cmd *dclish.Command) error {
-	if accounts.User.Folders.IsFolderAccess(cmd.Args[0], accounts.User.Login) {
-		accounts.User.CurrentFolder = cmd.Args[0]
-	}
-	return nil
+	if strings.Contains(cmd.Args[0], "%") {
+		return errors.New("Folder name cannot contain a %")
+	}
+	folder := accounts.User.Folders.FindFolder(cmd.Args[0])
+	if folder == "" {
+		return errors.New("Unable to select the folder")
+	}
+	if accounts.User.Folders.IsFolderAccess(folder, accounts.User.Login) {
+		accounts.User.CurrentFolder = folder
+		fmt.Printf("Folder has been set to '%s'.\n", folder)
+		return nil
+	}
+	// TODO: Should be:
+	//       WRITE(6,'('' You are not allowed to access folder.'')')
+	//       WRITE(6,'('' See '',A,'' if you wish to access folder.'')')
+	return errors.New("Unable to select the folder")
 }
 
 // ActionModify handles the `MODIFY` command.  This modifies a folder.
diff --git a/repl/messages.go b/repl/messages.go
index 507b091028bae05269690a4502b627ba74d0e751..ae292944a4f57175362566136746454e4d3c3128 100644
--- a/repl/messages.go
+++ b/repl/messages.go
@@ -18,11 +18,36 @@ import (
 func ActionDirectory(cmd *dclish.Command) error {
 	// TODO: flag parsing.
 	if len(cmd.Args) == 1 {
-		folder := strings.ToUpper(cmd.Args[0])
-		// TODO: Check folder is valid.
+		if strings.Contains(cmd.Args[0], "%") {
+			return errors.New("Folder name cannot contain a %")
+		}
+		folder := accounts.User.Folders.FindFolder(cmd.Args[0])
+		if folder == "" {
+			return errors.New("Unable to select the folder")
+		}
+		if !accounts.User.Folders.IsFolderAccess(folder, accounts.User.Login) {
+			// TODO: Should be:
+			//       WRITE(6,'('' You are not allowed to access folder.'')')
+			//       WRITE(6,'('' See '',A,'' if you wish to access folder.'')')
+			return errors.New("Unable to select the folder")
+		}
 		accounts.User.CurrentFolder = folder
 	}
-	fmt.Println("TODO: List messages in folder")
+	msgs, err := accounts.User.Folders.ListMessages(
+		accounts.User.CurrentFolder, nil)
+	if err != nil {
+		return err
+	}
+	if len(msgs) == 0 {
+		fmt.Println("There are no messages present.")
+		return nil
+	}
+	fmt.Printf("%4s %-43s %-12s %-10s\n", "#", "Subject", "From", "Date")
+	for _, msg := range msgs {
+		fmt.Printf("%4d %-43s %-12s %-10s\n", msg.ID, msg.Subject, msg.Author,
+			msg.CreateAt.Format("2006-05-04 15:02:01"))
+	}
+
 	return nil
 }
 
@@ -158,7 +183,22 @@ func ActionNext(cmd *dclish.Command) error {
 
 // ActionRead handles the `READ` command.
 func ActionRead(cmd *dclish.Command) error {
-	fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description)
+	// TODO: We need to set accounts.User.CurrentMessage when we change folder.
+	msgid := accounts.User.CurrentMessage
+	if len(cmd.Args) == 1 {
+		var err error
+		msgid, err = strconv.Atoi(cmd.Args[0])
+		if err != nil {
+			return err
+		}
+	}
+	msg, err := accounts.User.Folders.ReadMessage(
+		accounts.User.Login, accounts.User.CurrentFolder, msgid)
+	if err != nil {
+		return err
+	}
+	// TODO: update accounts.User.CurrentMessage
+	fmt.Printf("%s\n", msg)
 	return nil
 }
 
diff --git a/repl/repl.go b/repl/repl.go
index 88e97a4e41e30ab6321da51f00f94da5ababae01..07afdf823180cb9de6a1f48f59ccc8b17a996ddb 100644
--- a/repl/repl.go
+++ b/repl/repl.go
@@ -4,6 +4,7 @@ package repl
 import (
 	"fmt"
 	"path"
+	"unicode"
 
 	"git.lyda.ie/kevin/bulletin/accounts"
 	"github.com/adrg/xdg"
@@ -54,6 +55,19 @@ func Loop() error {
 		if len(line) == 0 {
 			continue
 		}
+		prependRead := false
+		for _, r := range line {
+			if unicode.IsDigit(r) {
+				prependRead = true
+				break
+			}
+			if !unicode.IsSpace(r) {
+				break
+			}
+		}
+		if prependRead {
+			line = "READ " + line
+		}
 		err = commands.ParseAndRun(line)
 		if err != nil {
 			fmt.Printf("ERROR: %s.\n", err)