Index: trunk/downstream.go
===================================================================
--- trunk/downstream.go	(revision 515)
+++ trunk/downstream.go	(revision 516)
@@ -1802,9 +1802,20 @@
 			return err
 		}
-		var target, criteria, limitStr string
-		if err := parseMessageParams(msg, nil, &target, &criteria, &limitStr); err != nil {
+		var target, limitStr string
+		var boundsStr [2]string
+		switch subcommand {
+		case "AFTER", "BEFORE":
+			if err := parseMessageParams(msg, nil, &target, &boundsStr[0], &limitStr); err != nil {
+				return err
+			}
+		case "BETWEEN":
+			if err := parseMessageParams(msg, nil, &target, &boundsStr[0], &boundsStr[1], &limitStr); err != nil {
+				return err
+			}
+		default:
+			// TODO: support LATEST, AROUND
 			return ircError{&irc.Message{
 				Command: "FAIL",
-				Params:  []string{"CHATHISTORY", "NEED_MORE_PARAMS", subcommand, "Missing parameters"},
+				Params:  []string{"CHATHISTORY", "INVALID_PARAMS", subcommand, "Unknown command"},
 			}}
 		}
@@ -1825,18 +1836,21 @@
 
 		// TODO: support msgid criteria
-		criteriaParts := strings.SplitN(criteria, "=", 2)
-		if len(criteriaParts) != 2 || criteriaParts[0] != "timestamp" {
+		var bounds [2]time.Time
+		bounds[0] = parseChatHistoryBound(boundsStr[0])
+		if bounds[0].IsZero() {
 			return ircError{&irc.Message{
 				Command: "FAIL",
-				Params:  []string{"CHATHISTORY", "INVALID_PARAMS", subcommand, criteria, "Unknown criteria"},
+				Params:  []string{"CHATHISTORY", "INVALID_PARAMS", subcommand, boundsStr[0], "Invalid first bound"},
 			}}
 		}
 
-		timestamp, err := time.Parse(serverTimeLayout, criteriaParts[1])
-		if err != nil {
-			return ircError{&irc.Message{
-				Command: "FAIL",
-				Params:  []string{"CHATHISTORY", "INVALID_PARAMS", subcommand, criteria, "Invalid criteria"},
-			}}
+		if boundsStr[1] != "" {
+			bounds[1] = parseChatHistoryBound(boundsStr[1])
+			if bounds[1].IsZero() {
+				return ircError{&irc.Message{
+					Command: "FAIL",
+					Params:  []string{"CHATHISTORY", "INVALID_PARAMS", subcommand, boundsStr[1], "Invalid second bound"},
+				}}
+			}
 		}
 
@@ -1852,13 +1866,13 @@
 		switch subcommand {
 		case "BEFORE":
-			history, err = store.LoadBeforeTime(uc.network, entity, timestamp, limit)
+			history, err = store.LoadBeforeTime(uc.network, entity, bounds[0], time.Time{}, limit)
 		case "AFTER":
-			history, err = store.LoadAfterTime(uc.network, entity, timestamp, limit)
-		default:
-			// TODO: support LATEST, BETWEEN
-			return ircError{&irc.Message{
-				Command: "FAIL",
-				Params:  []string{"CHATHISTORY", "UNKNOWN_COMMAND", subcommand, "Unknown command"},
-			}}
+			history, err = store.LoadAfterTime(uc.network, entity, bounds[0], time.Now(), limit)
+		case "BETWEEN":
+			if bounds[0].Before(bounds[1]) {
+				history, err = store.LoadAfterTime(uc.network, entity, bounds[0], bounds[1], limit)
+			} else {
+				history, err = store.LoadBeforeTime(uc.network, entity, bounds[0], bounds[1], limit)
+			}
 		}
 		if err != nil {
Index: trunk/irc.go
===================================================================
--- trunk/irc.go	(revision 515)
+++ trunk/irc.go	(revision 516)
@@ -5,4 +5,5 @@
 	"sort"
 	"strings"
+	"time"
 	"unicode"
 	"unicode/utf8"
@@ -638,2 +639,21 @@
 	}
 }
+
+// parseChatHistoryBound parses the given CHATHISTORY parameter as a bound.
+// The zero time is returned on error.
+func parseChatHistoryBound(param string) time.Time {
+	parts := strings.SplitN(param, "=", 2)
+	if len(parts) != 2 {
+		return time.Time{}
+	}
+	switch parts[0] {
+	case "timestamp":
+		timestamp, err := time.Parse(serverTimeLayout, parts[1])
+		if err != nil {
+			return time.Time{}
+		}
+		return timestamp
+	default:
+		return time.Time{}
+	}
+}
Index: trunk/msgstore.go
===================================================================
--- trunk/msgstore.go	(revision 515)
+++ trunk/msgstore.go	(revision 516)
@@ -27,6 +27,12 @@
 	messageStore
 
-	LoadBeforeTime(network *network, entity string, t time.Time, limit int) ([]*irc.Message, error)
-	LoadAfterTime(network *network, entity string, t time.Time, limit int) ([]*irc.Message, error)
+	// LoadBeforeTime loads up to limit messages before start down to end. The
+	// returned messages must be between and excluding the provided bounds.
+	// end is before start.
+	LoadBeforeTime(network *network, entity string, start, end time.Time, limit int) ([]*irc.Message, error)
+	// LoadBeforeTime loads up to limit messages after start up to end. The
+	// returned messages must be between and excluding the provided bounds.
+	// end is after start.
+	LoadAfterTime(network *network, entity string, start, end time.Time, limit int) ([]*irc.Message, error)
 }
 
Index: trunk/msgstore_fs.go
===================================================================
--- trunk/msgstore_fs.go	(revision 515)
+++ trunk/msgstore_fs.go	(revision 516)
@@ -258,5 +258,5 @@
 }
 
-func (ms *fsMessageStore) parseMessagesBefore(network *network, entity string, ref time.Time, limit int, afterOffset int64) ([]*irc.Message, error) {
+func (ms *fsMessageStore) parseMessagesBefore(network *network, entity string, ref time.Time, end time.Time, limit int, afterOffset int64) ([]*irc.Message, error) {
 	path := ms.logPath(network, entity, ref)
 	f, err := os.Open(path)
@@ -285,5 +285,5 @@
 		if err != nil {
 			return nil, err
-		} else if msg == nil {
+		} else if msg == nil || !t.After(end) {
 			continue
 		} else if !t.Before(ref) {
@@ -314,5 +314,5 @@
 }
 
-func (ms *fsMessageStore) parseMessagesAfter(network *network, entity string, ref time.Time, limit int) ([]*irc.Message, error) {
+func (ms *fsMessageStore) parseMessagesAfter(network *network, entity string, ref time.Time, end time.Time, limit int) ([]*irc.Message, error) {
 	path := ms.logPath(network, entity, ref)
 	f, err := os.Open(path)
@@ -333,4 +333,6 @@
 		} else if msg == nil || !t.After(ref) {
 			continue
+		} else if !t.Before(end) {
+			break
 		}
 
@@ -344,10 +346,10 @@
 }
 
-func (ms *fsMessageStore) LoadBeforeTime(network *network, entity string, t time.Time, limit int) ([]*irc.Message, error) {
+func (ms *fsMessageStore) LoadBeforeTime(network *network, entity string, start time.Time, end time.Time, limit int) ([]*irc.Message, error) {
 	history := make([]*irc.Message, limit)
 	remaining := limit
 	tries := 0
-	for remaining > 0 && tries < fsMessageStoreMaxTries {
-		buf, err := ms.parseMessagesBefore(network, entity, t, remaining, -1)
+	for remaining > 0 && tries < fsMessageStoreMaxTries && end.Before(start) {
+		buf, err := ms.parseMessagesBefore(network, entity, start, end, remaining, -1)
 		if err != nil {
 			return nil, err
@@ -360,6 +362,6 @@
 		copy(history[remaining-len(buf):], buf)
 		remaining -= len(buf)
-		year, month, day := t.Date()
-		t = time.Date(year, month, day, 0, 0, 0, 0, t.Location()).Add(-1)
+		year, month, day := start.Date()
+		start = time.Date(year, month, day, 0, 0, 0, 0, start.Location()).Add(-1)
 	}
 
@@ -367,11 +369,10 @@
 }
 
-func (ms *fsMessageStore) LoadAfterTime(network *network, entity string, t time.Time, limit int) ([]*irc.Message, error) {
+func (ms *fsMessageStore) LoadAfterTime(network *network, entity string, start time.Time, end time.Time, limit int) ([]*irc.Message, error) {
 	var history []*irc.Message
 	remaining := limit
 	tries := 0
-	now := time.Now()
-	for remaining > 0 && tries < fsMessageStoreMaxTries && t.Before(now) {
-		buf, err := ms.parseMessagesAfter(network, entity, t, remaining)
+	for remaining > 0 && tries < fsMessageStoreMaxTries && start.Before(end) {
+		buf, err := ms.parseMessagesAfter(network, entity, start, end, remaining)
 		if err != nil {
 			return nil, err
@@ -384,6 +385,6 @@
 		history = append(history, buf...)
 		remaining -= len(buf)
-		year, month, day := t.Date()
-		t = time.Date(year, month, day+1, 0, 0, 0, 0, t.Location())
+		year, month, day := start.Date()
+		start = time.Date(year, month, day+1, 0, 0, 0, 0, start.Location())
 	}
 	return history, nil
@@ -421,5 +422,5 @@
 		}
 
-		buf, err := ms.parseMessagesBefore(network, entity, t, remaining, offset)
+		buf, err := ms.parseMessagesBefore(network, entity, t, time.Time{}, remaining, offset)
 		if err != nil {
 			return nil, err
