diff --git a/NOTES.md b/NOTES.md
index 25b966959058313cddea018ca9dfac2ee59dee44..317f95e3648abc060083ce61250a3742b72f57f9 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -23,19 +23,25 @@ sqlite trigger tracing: `.trace stdout --row --profile --stmt --expanded --plain
 
 ## Things to do
 
-  * Implement a better dclish parser.
   * Implement each command.
-    * Next: ????
+    * Next: folder commands - ~~CREATE~~, ~~REMOVE~~, MODIFY, INDEX, SELECT
+    * Messages: ADD, CURRENT, DIRECTORY
   * Editor - need an embedded editor
     * An EDT inspired [editor](https://sourceforge.net/projects/edt-text-editor/)
     * [gkilo](https://github.com/vcnovaes/gkilo)
     * This [kilo editor](https://viewsourcecode.org/snaptoken/kilo/) tutorial
-  * Should there be a way to upload text files for commands that use
-    files?  How would it work?
   * Cleanup help output.
-    * Remove the node related flags.
+    * Remove the node/cluster/newsgroup/mailing-list related flags.
   * Database
-    * trigger to limit values for 'visibility';
+    * trigger to limit values for 'visibility'?
+  * Add some of the early announcements from the sources - see the
+    conversion branch - to the GENERAL folder.
+  * Add commands:
+    * A way to add / delete ssh keys.
+    * A way to manage files?
+    * Commands for a local mail system?
+    * Commands to connect to Mattermost or mastodon?
+    * Commands to manage users.
 
 ## Module links
 
diff --git a/accounts/accounts.go b/accounts/accounts.go
index 186a1c9f08947cb6dd81c809edac64c1bc8205e0..9fee3ccd0473ee3ae59ab4a04e3f659964cf0994 100644
--- a/accounts/accounts.go
+++ b/accounts/accounts.go
@@ -7,13 +7,14 @@ import (
 	"strings"
 
 	"git.lyda.ie/kevin/bulletin/folders"
+	"github.com/chzyer/readline"
 	_ "modernc.org/sqlite" // Loads sqlite driver.
 )
 
 // UserData is the type for holding user data. Things like preferences,
 // unread message counts, signatures, etc.
 type UserData struct {
-	Account        string
+	Login          string
 	FullName       string
 	Folders        *folders.Store
 	CurrentFolder  string
@@ -24,30 +25,57 @@ type UserData struct {
 var User *UserData
 
 // ValidName makes sure that an account name is a valid name.
-func ValidName(acc string) error {
-	if acc == "" {
+func ValidName(login string) error {
+	if login == "" {
 		return errors.New("empty account is invalid")
 	}
-	if strings.ContainsAny(acc, "./") {
-		return fmt.Errorf("account name '%s' is invalid", acc)
+	if strings.ContainsAny(login, "./") {
+		return fmt.Errorf("account name '%s' is invalid", login)
 	}
 	return nil
 }
 
 // Open verifies that an account exists.
-func Open(acc string) error {
-	err := ValidName(acc)
+func Open(login string) error {
+	err := ValidName(login)
 	if err != nil {
 		return err
 	}
 	User = &UserData{
-		Account: acc,
+		Login: login,
 	}
 
-	User.Folders, err = folders.Open(acc)
+	User.Folders, err = folders.Open(login)
 	if err != nil {
 		return err
 	}
+	user, _ := User.Folders.GetUser(login)
+
+	if user.Login != login {
+		user.Login = login
+		user.Admin = 0
+
+		fmt.Printf("Welcome new user %s\n", login)
+		rl, err := readline.New("please enter your name: ")
+		if err != nil {
+			return err
+		}
+		user.Name, err = rl.Readline()
+		rl.Close()
+
+		err = User.Folders.AddUser(*user)
+		if err != nil {
+			return err
+		}
+		fmt.Println("User successfully created. Enjoy!")
+	}
+	if user.Disabled == 1 {
+		return errors.New("User is disabled")
+	}
+	User.FullName = user.Name
+	User.CurrentFolder = "GENERAL"
+	// TODO: get the most recent unread message.
+	// User.CurrentMessage =
 
 	return nil
 }
@@ -56,12 +84,3 @@ func Open(acc string) error {
 func (u *UserData) Close() {
 	u.Folders.Close()
 }
-
-// IsAdmin returns true if the user is an admin
-func IsAdmin(acc string) bool {
-	if acc == "admin" {
-		return true
-	}
-	// TODO: Look up account otherwise.
-	return false
-}
diff --git a/folders/connection.go b/folders/connection.go
new file mode 100644
index 0000000000000000000000000000000000000000..039ff10e22cce3dfba66266d75cde67ab62e82b9
--- /dev/null
+++ b/folders/connection.go
@@ -0,0 +1,64 @@
+// Package folders are all the routines and sql for managing folders.
+package folders
+
+import (
+	"embed"
+	"errors"
+	"os"
+	"path"
+
+	"github.com/adrg/xdg"
+	"github.com/golang-migrate/migrate/v4"
+	"github.com/golang-migrate/migrate/v4/source/iofs"
+	"github.com/jmoiron/sqlx"
+
+	// Included to connect to sqlite.
+	_ "github.com/golang-migrate/migrate/v4/database/sqlite"
+	_ "modernc.org/sqlite"
+)
+
+//go:embed sql/*.sql
+var fs embed.FS
+
+// Store is the store for folders.
+type Store struct {
+	user string
+	db   *sqlx.DB
+}
+
+// Open opens the folders database.
+func Open(user string) (*Store, error) {
+	fdir := path.Join(xdg.DataHome, "BULLETIN")
+	err := os.MkdirAll(fdir, 0700)
+	if err != nil {
+		return nil, errors.New("bulletin directory problem")
+	}
+	fdb := path.Join(fdir, "bboard.db")
+
+	// Run db migrations if needed.
+	sqldir, err := iofs.New(fs, "sql")
+	if err != nil {
+		return nil, err
+	}
+	m, err := migrate.NewWithSourceInstance("iofs", sqldir, "sqlite://"+fdb+"?_pragma=foreign_keys(1)")
+	if err != nil {
+		return nil, err
+	}
+	err = m.Up()
+	if err != nil && err != migrate.ErrNoChange {
+		return nil, err
+	}
+	m.Close()
+
+	store := &Store{user: user}
+	store.db, err = sqlx.Connect("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)")
+	if err != nil {
+		return nil, errors.New("bulletin database problem")
+	}
+	return store, nil
+}
+
+// Close closes the db backing the store.
+func (fstore *Store) Close() {
+	fstore.db.Close()
+}
diff --git a/folders/folders.go b/folders/folders.go
index 039ff10e22cce3dfba66266d75cde67ab62e82b9..bfafdd21806ff73a81b141a33327fd86d0001953 100644
--- a/folders/folders.go
+++ b/folders/folders.go
@@ -2,63 +2,133 @@
 package folders
 
 import (
-	"embed"
 	"errors"
-	"os"
-	"path"
+	"fmt"
 
-	"github.com/adrg/xdg"
-	"github.com/golang-migrate/migrate/v4"
-	"github.com/golang-migrate/migrate/v4/source/iofs"
 	"github.com/jmoiron/sqlx"
+)
+
+// FolderVisibility is the folder visibility level.
+type FolderVisibility string
 
-	// Included to connect to sqlite.
-	_ "github.com/golang-migrate/migrate/v4/database/sqlite"
-	_ "modernc.org/sqlite"
+// Values for FolderVisibility.
+const (
+	FolderPublic      FolderVisibility = "public"
+	FolderSemiPrivate                  = "semi-private"
+	FolderPrivate                      = "private"
 )
 
-//go:embed sql/*.sql
-var fs embed.FS
+// FolderCreateOptions are a list of folder options.
+type FolderCreateOptions struct {
+	Always      int
+	Brief       int
+	Description string
+	Notify      int
+	Owner       string
+	Readnew     int
+	Shownew     int
+	System      int
+	Expire      int
+	Visibility  FolderVisibility
+}
+
+// CreateFolder creates a new folder.
+func (s *Store) CreateFolder(name string, options FolderCreateOptions) error {
+	_, err := s.db.Exec(
+		`INSERT INTO folders
+			(name, always, brief, description, notify, owner, readnew,
+			 shownew, system, expire, visibility)
+			VALUES
+			($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
+		name,
+		options.Always,
+		options.Brief,
+		options.Description,
+		options.Notify,
+		options.Owner,
+		options.Readnew,
+		options.Shownew,
+		options.System,
+		options.Expire,
+		options.Visibility,
+	)
+	// TODO: process this error a bit more to give a better error message.
+	return err
+}
+
+// FolderListOptions are a list of folder options.
+type FolderListOptions struct {
+}
 
-// Store is the store for folders.
-type Store struct {
-	user string
-	db   *sqlx.DB
+// FolderListRow are a list of folder options.
+type FolderListRow struct {
+	Name        string `db:"name"`
+	Count       int    `db:"count"`
+	Description string `db:"description"`
 }
 
-// Open opens the folders database.
-func Open(user string) (*Store, error) {
-	fdir := path.Join(xdg.DataHome, "BULLETIN")
-	err := os.MkdirAll(fdir, 0700)
+// ListFolder creates a new folder.
+func (s *Store) ListFolder(user string, _ FolderListOptions) ([]FolderListRow, error) {
+	// TODO: get message counts.
+	var rows *sqlx.Rows
+	var err error
+	if s.IsUserAdmin(user) {
+		rows, err = s.db.Queryx(
+			`SELECT name, 0 as count, description FROM folders
+			ORDER BY name`,
+		)
+	} else {
+		// TODO: limit access.
+		rows, err = s.db.Queryx(
+			`SELECT f.name, count(m.id) as count, f.description FROM folders AS f
+			LEFT JOIN messages AS m ON f.name = m.folder
+			GROUP By f.name
+			ORDER BY f.name`)
+	}
+	flr := []FolderListRow{}
 	if err != nil {
-		return nil, errors.New("bulletin directory problem")
+		// TODO: process this error a bit more to give a better error message.
+		return flr, err
 	}
-	fdb := path.Join(fdir, "bboard.db")
+	for rows.Next() {
+		row := FolderListRow{}
+		err := rows.StructScan(&row)
+		if err != nil {
+			// TODO: process this error a bit more to give a better error message.
+			return flr, err
+		}
+		flr = append(flr, row)
+	}
+	return flr, nil
+}
+
+// IsFolderOwner checks if a user is a folder owner.
+func (s *Store) IsFolderOwner(name, user string) bool {
+	found := 0
+	s.db.Get(&found,
+		`SELECT 1 FROM folders AS f LEFT JOIN co_owners AS c
+	   ON f.name = c.folder
+		 WHERE f.name = $1 (f.owner = '$2' OR c.OWNER = '$2')`,
+		name, user)
+	return found == 1
+}
 
-	// Run db migrations if needed.
-	sqldir, err := iofs.New(fs, "sql")
+// DeleteFolder creates a new folder.
+func (s *Store) DeleteFolder(name string) error {
+	results, err := s.db.Exec("DELETE FROM folders WHERE name=$1", name)
+	// TODO: process this error a bit more to give a better error message.
 	if err != nil {
-		return nil, err
+		return err
 	}
-	m, err := migrate.NewWithSourceInstance("iofs", sqldir, "sqlite://"+fdb+"?_pragma=foreign_keys(1)")
+	rows, err := results.RowsAffected()
 	if err != nil {
-		return nil, err
+		return err
 	}
-	err = m.Up()
-	if err != nil && err != migrate.ErrNoChange {
-		return nil, err
+	if rows == 0 {
+		return errors.New("No such folder found")
 	}
-	m.Close()
-
-	store := &Store{user: user}
-	store.db, err = sqlx.Connect("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)")
-	if err != nil {
-		return nil, errors.New("bulletin database problem")
+	if rows != 1 {
+		return fmt.Errorf("Unexpected number (%d) of folders removed", rows)
 	}
-	return store, nil
-}
-
-// Close closes the db backing the store.
-func (fstore *Store) Close() {
-	fstore.db.Close()
+	return nil
 }
diff --git a/folders/manage-folders.go b/folders/manage-folders.go
deleted file mode 100644
index 8beaf58b7eca969165b011972630b007591dee37..0000000000000000000000000000000000000000
--- a/folders/manage-folders.go
+++ /dev/null
@@ -1,75 +0,0 @@
-// Package folders are all the routines and sql for managing folders.
-package folders
-
-import (
-	"errors"
-	"fmt"
-)
-
-// FolderVisibility is the folder visibility level.
-type FolderVisibility string
-
-// Values for FolderVisibility.
-const (
-	FolderPublic      FolderVisibility = "public"
-	FolderSemiPrivate                  = "semi-private"
-	FolderPrivate                      = "private"
-)
-
-// FolderOptions are a list of folder options.
-type FolderOptions struct {
-	Always      int
-	Brief       int
-	Description string
-	Notify      int
-	Owner       string
-	Readnew     int
-	Shownew     int
-	System      int
-	Expire      int
-	Visibility  FolderVisibility
-}
-
-// CreateFolder creates a new folder.
-func (s *Store) CreateFolder(name string, options FolderOptions) error {
-	_, err := s.db.Exec(
-		`INSERT INTO folders
-			(name, always, brief, description, notify, owner, readnew,
-			 shownew, system, expire, visibility)
-			VALUES
-			($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
-		name,
-		options.Always,
-		options.Brief,
-		options.Description,
-		options.Notify,
-		options.Owner,
-		options.Readnew,
-		options.Shownew,
-		options.System,
-		options.Expire,
-		options.Visibility,
-	)
-	// TODO: process this error a bit more to give a better error message.
-	return err
-}
-
-// DeleteFolder creates a new folder.
-func (s *Store) DeleteFolder(name string) error {
-	results, err := s.db.Exec("DELETE FROM folders WHERE name=$1", name)
-	// TODO: process this error a bit more to give a better error message.
-	if err != nil {
-		return err
-	}
-	rows, err := results.RowsAffected()
-	if err != nil {
-		return err
-	}
-	if rows == 0 {
-		return errors.New("No such folder found")
-	}
-	if rows != 1 {
-		return fmt.Errorf("Unexpected number (%d) of folders removed", rows)
-	}
-	return nil
-}
diff --git a/folders/sql/1_create_table.up.sql b/folders/sql/1_create_table.up.sql
index 02b78c886f35f3f59b0c5e6159d98bbd65e10695..208bc9755639b0225d48440c5b9c8a14011df76a 100644
--- a/folders/sql/1_create_table.up.sql
+++ b/folders/sql/1_create_table.up.sql
@@ -2,6 +2,7 @@ CREATE TABLE users (
   login       VARCHAR(25)  NOT NULL PRIMARY KEY,
   name        VARCHAR(53)  NOT NULL,
   admin       INT          DEFAULT 0,
+  disabled    INT          DEFAULT 0,
   create_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
   update_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL
 ) WITHOUT ROWID;
@@ -99,3 +100,20 @@ CREATE TRIGGER co_owners_after_update_update_at
 BEGIN
   UPDATE co_owners SET update_at=CURRENT_TIMESTAMP WHERE folder=NEW.folder AND owner=NEW.owner;
 END;
+
+CREATE TABLE messages (
+  id          INT          NOT NULL,
+  folder      VARCHAR(25)  REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
+  author      VARCHAR(25)  REFERENCES users(login) ON UPDATE CASCADE,
+  subject     VARCHAR(53)  NOT NULL,
+  message     TEXT         NOT NULL,
+  permanent   INT          DEFAULT 0,
+  shutdown    INT          DEFAULT 0,
+  expiration  TIMESTAMP    NOT NULL,
+  create_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
+  update_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
+  PRIMARY KEY (id, folder)
+) WITHOUT ROWID;
+CREATE INDEX messages_idx_shutdown ON messages(shutdown);
+CREATE INDEX messages_idx_expiration ON messages(expiration);
+
diff --git a/folders/users.go b/folders/users.go
new file mode 100644
index 0000000000000000000000000000000000000000..e1b20ab01a4c96ff9245b4e2544cd470669a6788
--- /dev/null
+++ b/folders/users.go
@@ -0,0 +1,46 @@
+// Package folders are all the routines and sql for managing folders.
+package folders
+
+import "errors"
+
+// User is the user structure.
+type User struct {
+	Login    string `db:"login"`
+	Name     string `db:"name"`
+	Admin    int    `db:"admin"`
+	Disabled int    `db:"disabled"`
+}
+
+// GetUser gets a user.
+func (s *Store) GetUser(login string) (*User, error) {
+	user := &User{}
+	err := s.db.Get(user,
+		"SELECT login, name, admin, disabled FROM users WHERE login = $1",
+		login)
+	return user, err
+}
+
+// AddUser gets a user.
+func (s *Store) AddUser(user User) error {
+	result, err := s.db.NamedExec(
+		`INSERT INTO users (login, name, admin) VALUES (:login, :name, :admin)`,
+		user)
+	if err != nil {
+		return err
+	}
+	rows, err := result.RowsAffected()
+	if err != nil {
+		return err
+	}
+	if rows != 1 {
+		return errors.New("Failed to add user")
+	}
+	return nil
+}
+
+// IsUserAdmin checks if a user is an admin.
+func (s *Store) IsUserAdmin(user string) bool {
+	found := 0
+	s.db.Get(&found, "SELECT admin FROM users WHERE login = $1", user)
+	return found == 1
+}
diff --git a/main.go b/main.go
index caeb3111336ca16b504e799e53b45e62683adf39..87ef32a80af81ba9b2f9f8df4bab8218db0381cd 100644
--- a/main.go
+++ b/main.go
@@ -23,17 +23,40 @@ func main() {
 				Name:     "user",
 				Aliases:  []string{"u"},
 				Usage:    "user to run bulletin as",
-				Value:    "",
 				Required: true,
 			},
+			&cli.StringFlag{
+				Name:    "batch",
+				Aliases: []string{"b"},
+				Usage:   "batch command",
+			},
 		},
 		Action: func(_ context.Context, cmd *cli.Command) error {
 			user := cmd.String("user")
+			batch := cmd.String("batch")
+
+			if batch != "" {
+				if user != "SYSTEM" {
+					fmt.Println("ERROR: can only run batch commands as SYSTEM.")
+					os.Exit(1)
+				}
+				exitcode := 0
+				switch batch {
+				case "after-boot":
+					fmt.Println("TODO: Delete messages with shutdown != 0.")
+				case "expire":
+					fmt.Println("TODO: expire messages.")
+				default:
+					fmt.Println("ERROR: can only run batch commands as SYSTEM.")
+					exitcode = 1
+				}
+				os.Exit(exitcode)
+			}
 			err := accounts.Open(user)
 			if err != nil {
 				return err
 			}
-			err = repl.Loop(user)
+			err = repl.Loop()
 			if err != nil {
 				return err
 			}
@@ -43,6 +66,6 @@ func main() {
 
 	err := cmd.Run(context.Background(), os.Args)
 	if err != nil {
-		fmt.Printf("ERROR: %s.", err)
+		fmt.Printf("ERROR: %s.\n", err)
 	}
 }
diff --git a/repl/command.go b/repl/command.go
index f913639f04d16faef1041d61ae7480c463542d95..c1ebcd1192bed96f2b1b3b4cc5e95830185f9f43 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -59,24 +59,11 @@ suppressed with /NOINDENT.`,
 				Description: `/FOLDER=(foldername,[...])
 
 Specifies the foldername into which the message is to  be  added.   Does
-not  change the current selected folder.  Folders can be either local or
-remote folders.  Thus, a  nodename  can  precede  the  foldername  (this
-assumes that the remote node is capable of supporting this feature, i.e.
-the BULLCP process is running on that node.  If  it  is  not,  you  will
-receive an error message).  If the the foldername is specified with only
-a nodename, i.e. FOO::, the foldername is assumed to be GENERAL.   NOTE:
-Specifying  remote nodes is only possible if that remote node is running
-a special BULLCP process.  If it isn't, the only way to add messages  to
-that  remote  node is via the /NODE command.  However, /FOLDER is a much
-quicker method, and much more versatile.
+not  change the current selected folder.
 
 You can specify logical names which translate  to  one  or  more  folder
 names.   I.e.   $  DEFINE ALL_FOLDERS "VAX1,VAX2,VAX3", and then specify
-ALL_FOLDERS after /FOLDER=.  Note that the quotation marks are required.
-
-When using /FOLDER for remote nodes, proxy logins are used to  determine
-if privileged options are allowed.  If they are not allowed, the message
-will still be added, but without the privileged settings.`,
+ALL_FOLDERS after /FOLDER=.  Note that the quotation marks are required.`,
 				OptArg: true,
 			},
 			"/INDENT": {
@@ -307,25 +294,7 @@ more information.)`,
 				Description: `/DESCRIPTION=description
 
 Specifies the description of the folder, which is displayed using the
-SHOW FOLDER command.  If omitted, you are prompted for a description.
-
-If this folder is to receive messages from a network mailing list
-via the BBOARD feature, and you wish to use the POST and RESPOND/LIST
-commands, the address of the mailing list should be included in the
-description.  This is done by enclosing the address using <> and
-placing it at the end of the description, i.e.
-
-              INFOVAX MAILING LIST <INFO-VAX@KL.SRI.COM>
-
-If a mailer protocol is needs to be added to the network address in
-order for it to be sent by VMS MAIL, i.e. protocol%"address",  the
-appropriate protocol can be specified by either hardcoding it into the
-file BULLNEWS.INC before compiling BULLETIN, or by defining the system
-logical name BULL_NEWS_MAILER (it is the same protocol used by the NEWS
-feature in order to respond to NEWS messages).  The default protocol is
-IN%.  If desired, you can specify the protocol with the address, i.e.
-
-              INFOVAX MAILING LIST <IN%"INFO-VAX@KL.SRI.COM">`,
+SHOW FOLDER command.  If omitted, you are prompted for a description.`,
 				OptArg: true,
 			},
 			"/EXPIRE": {
@@ -1246,16 +1215,13 @@ folders that have been created.
 
  Format:
 
-     SELECT [node-name::][folder-name]
+     SELECT [folder-name]
 
 The complete folder name need not be specified.  BULLETIN  will  try  to
 find the closest matching name.  I.e. INFOV can be used for INFOVAX.
 
 Omitting the folder name will select the default general messages.
 
-The  node  name can be specified only if the remote node has the special
-BULLCP process running (invoked by BULLETIN/STARTUP command.)
-
 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.`,
@@ -1380,9 +1346,6 @@ users must be at least 2.
 
     SET BBOARD [username]
 
-BBOARD cannot be set for remote folders.   See  also  the  command  SET
-DIGEST for options on formatting BBOARD messages.
-
 If BULLCP is running, BBOARD is updated every 15 minutes.  If you want
 to length this period, define BULL_BBOARD_UPDATE to be the number of
 minutes, between updates.  I.e. DEFINE/SYSTEM BULL_BBOARD_UPDATE "30"
@@ -1601,7 +1564,7 @@ on that command for more information.
 
   Format:
 
-    SET FOLDER [node-name::][folder-name]
+    SET FOLDER [folder-name]
 3 /MARKED
 Selects messages that have been marked (indicated by an asterisk).
 After using /MARKED, in order to see all messages, the folder will have
diff --git a/repl/folders.go b/repl/folders.go
index 4dbbdade011eed3434455b00a69ba1bcd97ff296..bfa3bdfb11bed7761f75b581f054cd80892622d4 100644
--- a/repl/folders.go
+++ b/repl/folders.go
@@ -5,35 +5,30 @@ import (
 	"errors"
 	"fmt"
 	"strconv"
-	"strings"
 
 	"git.lyda.ie/kevin/bulletin/accounts"
 	"git.lyda.ie/kevin/bulletin/dclish"
 	"git.lyda.ie/kevin/bulletin/folders"
 )
 
-// ActionDirectory handles the `DIRECTORY` command.  This lists all the
-// messages in the current folder.
-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.
-		accounts.User.CurrentFolder = folder
-	}
-	fmt.Println("TODO: List messages in folder")
-	return nil
-}
-
 // ActionIndex handles the `INDEX` command.  This lists all the folders.
-func ActionIndex(cmd *dclish.Command) error {
-	fmt.Printf("TODO: implement INDEX:\n%s\n\n", cmd.Description)
+func ActionIndex(_ *dclish.Command) error {
+	options := folders.FolderListOptions{}
+	rows, err := accounts.User.Folders.ListFolder(accounts.User.Login, options)
+	if err != nil {
+		return err
+	}
+	fmt.Println("The following folders are present")
+	fmt.Println("Name                       Count Description")
+	for _, row := range rows {
+		fmt.Printf("%-25s  %5d %s\n", row.Name, row.Count, row.Description)
+	}
 	return nil
 }
 
 // ActionCreate handles the `CREATE` command.  This creates a folder.
 func ActionCreate(cmd *dclish.Command) error {
-	options := folders.FolderOptions{}
+	options := folders.FolderCreateOptions{}
 	if cmd.Flags["/ALWAYS"].Value == "true" {
 		options.Always = 1
 	}
@@ -41,6 +36,7 @@ func ActionCreate(cmd *dclish.Command) error {
 		options.Brief = 1
 	}
 	if cmd.Flags["/DESCRIPTION"].Value == "" {
+		// TODO: prompt the user for a description.
 		return errors.New("Description is required - use /DESCRIPTION")
 	}
 	options.Description = cmd.Flags["/DESCRIPTION"].Value
@@ -50,7 +46,7 @@ func ActionCreate(cmd *dclish.Command) error {
 	if cmd.Flags["/OWNER"].Value != "" {
 		options.Owner = cmd.Flags["/OWNER"].Value
 	} else {
-		options.Owner = accounts.User.Account
+		options.Owner = accounts.User.Login
 	}
 	if cmd.Flags["/READNEW"].Value == "true" {
 		options.Readnew = 1
diff --git a/repl/help.go b/repl/help.go
index e7b6ae555d0cfbfc16f51a76b3558631a1d822ac..52a0a8cd95e7ce231ae1008aaabf6933ae3caf39 100644
--- a/repl/help.go
+++ b/repl/help.go
@@ -84,7 +84,7 @@ func init() {
 			}
 			sort.Strings(flgs)
 			for i := range flgs {
-				fmt.Fprintf(buf, "\n%s %s", flgs[i], commands[c].Flags[flgs[i]].Description)
+				fmt.Fprintf(buf, "\n\n%s %s", flgs[i], commands[c].Flags[flgs[i]].Description)
 			}
 		}
 		helpmap[c] = buf.String()
diff --git a/repl/messages.go b/repl/messages.go
new file mode 100644
index 0000000000000000000000000000000000000000..213f6b3556eb65bece1f668498fb153d50ed7f2a
--- /dev/null
+++ b/repl/messages.go
@@ -0,0 +1,23 @@
+// Package repl implements the main event loop.
+package repl
+
+import (
+	"fmt"
+	"strings"
+
+	"git.lyda.ie/kevin/bulletin/accounts"
+	"git.lyda.ie/kevin/bulletin/dclish"
+)
+
+// ActionDirectory handles the `DIRECTORY` command.  This lists all the
+// messages in the current folder.
+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.
+		accounts.User.CurrentFolder = folder
+	}
+	fmt.Println("TODO: List messages in folder")
+	return nil
+}
diff --git a/repl/repl.go b/repl/repl.go
index fea8113eb9b94f495449022a4acbec5b5d1e9029..88e97a4e41e30ab6321da51f00f94da5ababae01 100644
--- a/repl/repl.go
+++ b/repl/repl.go
@@ -5,17 +5,19 @@ import (
 	"fmt"
 	"path"
 
+	"git.lyda.ie/kevin/bulletin/accounts"
 	"github.com/adrg/xdg"
 	"github.com/chzyer/readline"
 )
 
 // Loop is the main event loop.
-func Loop(user string) error {
-	fmt.Printf("TODO: get config for user %s using xdg.\n", user)
+func Loop() error {
+	// fmt.Printf("TODO: get config for user %s using xdg.\n", user)
 	rl, err := readline.NewEx(
 		&readline.Config{
-			Prompt:      "BULLETIN> ",
-			HistoryFile: path.Join(xdg.ConfigHome, "BULLETIN", fmt.Sprintf("%s.history", user)),
+			Prompt: "BULLETIN> ",
+			HistoryFile: path.Join(xdg.ConfigHome, "BULLETIN",
+				fmt.Sprintf("%s.history", accounts.User.Login)),
 			// TODO: AutoComplete:    completer,
 			InterruptPrompt:   "^C",
 			EOFPrompt:         "EXIT",
@@ -26,6 +28,19 @@ func Loop(user string) error {
 	}
 	defer rl.Close()
 
+	// TODO: Remove once commands are implemented.
+	unimplemented := 0
+	total := 0
+	for c := range commands {
+		if commands[c].Action == nil {
+			unimplemented++
+		}
+		total++
+	}
+	fmt.Printf("TODO: %d out of %d commands still to be implemented.\n",
+		unimplemented, total)
+	// TODO: END
+
 	for {
 		line, err := rl.Readline()
 		if err != nil {