Index: /trunk/downstream.go
===================================================================
--- /trunk/downstream.go	(revision 359)
+++ /trunk/downstream.go	(revision 360)
@@ -1599,7 +1599,8 @@
 		}
 
+		batchRef := "history"
+		maxTries := 100
 		switch subcommand {
 		case "BEFORE":
-			batchRef := "history"
 			dc.SendMessage(&irc.Message{
 				Prefix:  dc.srv.prefix(),
@@ -1612,5 +1613,5 @@
 
 			tries := 0
-			for remaining > 0 {
+			for remaining > 0 && tries < maxTries {
 				buf, err := parseMessagesBefore(uc.network, entity, timestamp, remaining)
 				if err != nil {
@@ -1620,7 +1621,4 @@
 				if len(buf) == 0 {
 					tries++
-					if tries >= 100 {
-						break
-					}
 				} else {
 					tries = 0
@@ -1632,7 +1630,7 @@
 			}
 
-			for _, m := range history[remaining:] {
-				m.Tags["batch"] = irc.TagValue(batchRef)
-				dc.SendMessage(dc.marshalMessage(m, uc.network))
+			for _, msg := range history[remaining:] {
+				msg.Tags["batch"] = irc.TagValue(batchRef)
+				dc.SendMessage(dc.marshalMessage(msg, uc.network))
 			}
 
@@ -1642,6 +1640,41 @@
 				Params:  []string{"-" + batchRef},
 			})
+		case "AFTER":
+			dc.SendMessage(&irc.Message{
+				Prefix:  dc.srv.prefix(),
+				Command: "BATCH",
+				Params:  []string{"+" + batchRef, "chathistory", target},
+			})
+
+			remaining := limit
+			tries := 0
+			now := time.Now()
+			for remaining > 0 && tries < maxTries && timestamp.Before(now) {
+				buf, err := parseMessagesAfter(uc.network, entity, timestamp, remaining)
+				if err != nil {
+					dc.logger.Printf("failed parsing log messages for chathistory: %v", err)
+					return newChatHistoryError(subcommand, target)
+				}
+				if len(buf) == 0 {
+					tries++
+				} else {
+					tries = 0
+				}
+				for _, msg := range buf {
+					msg.Tags["batch"] = irc.TagValue(batchRef)
+					dc.SendMessage(dc.marshalMessage(msg, uc.network))
+				}
+				remaining -= len(buf)
+				year, month, day := timestamp.Date()
+				timestamp = time.Date(year, month, day + 1, 0, 0, 0, 0, timestamp.Location())
+			}
+
+			dc.SendMessage(&irc.Message{
+				Prefix:  dc.srv.prefix(),
+				Command: "BATCH",
+				Params:  []string{"-" + batchRef},
+			})
 		default:
-			// TODO: support AFTER, LATEST, BETWEEN
+			// TODO: support LATEST, BETWEEN
 			return ircError{&irc.Message{
 				Command: "FAIL",
Index: /trunk/logger.go
===================================================================
--- /trunk/logger.go	(revision 359)
+++ /trunk/logger.go	(revision 360)
@@ -135,7 +135,39 @@
 }
 
-func parseMessagesBefore(network *network, entity string, timestamp time.Time, limit int) ([]*irc.Message, error) {
-	year, month, day := timestamp.Date()
-	path := logPath(network, entity, timestamp)
+func parseMessage(line, entity string, ref time.Time) (*irc.Message, time.Time, error) {
+	var hour, minute, second int
+	_, err := fmt.Sscanf(line, "[%02d:%02d:%02d] ", &hour, &minute, &second)
+	if err != nil {
+		return nil, time.Time{}, err
+	}
+	line = line[11:]
+
+	// TODO: support NOTICE
+	if !strings.HasPrefix(line, "<") {
+		return nil, time.Time{}, nil
+	}
+	i := strings.Index(line, "> ")
+	if i < 0 {
+		return nil, time.Time{}, nil
+	}
+
+	year, month, day := ref.Date()
+	t := time.Date(year, month, day, hour, minute, second, 0, time.Local)
+
+	sender := line[1:i]
+	text := line[i+2:]
+	msg := &irc.Message{
+		Tags: map[string]irc.TagValue{
+			"time": irc.TagValue(t.UTC().Format(serverTimeLayout)),
+		},
+		Prefix: &irc.Prefix{Name: sender},
+		Command: "PRIVMSG",
+		Params:  []string{entity, text},
+	}
+	return msg, t, nil
+}
+
+func parseMessagesBefore(network *network, entity string, ref time.Time, limit int) ([]*irc.Message, error) {
+	path := logPath(network, entity, ref)
 	f, err := os.Open(path)
 	if err != nil {
@@ -152,36 +184,14 @@
 	sc := bufio.NewScanner(f)
 	for sc.Scan() {
-		line := sc.Text()
-		var hour, minute, second int
-		_, err := fmt.Sscanf(line, "[%02d:%02d:%02d] ", &hour, &minute, &second)
+		msg, t, err := parseMessage(sc.Text(), entity, ref)
 		if err != nil {
 			return nil, err
-		}
-		message := line[11:]
-		// TODO: support NOTICE
-		if !strings.HasPrefix(message, "<") {
+		} else if msg == nil {
 			continue
-		}
-		i := strings.Index(message, "> ")
-		if i == -1 {
-			continue
-		}
-		t := time.Date(year, month, day, hour, minute, second, 0, time.Local)
-		if !t.Before(timestamp) {
+		} else if !t.Before(ref) {
 			break
 		}
 
-		sender := message[1:i]
-		text := message[i+2:]
-		historyRing[cur%limit] = &irc.Message{
-			Tags: map[string]irc.TagValue{
-				"time": irc.TagValue(t.UTC().Format(serverTimeLayout)),
-			},
-			Prefix: &irc.Prefix{
-				Name: sender,
-			},
-			Command: "PRIVMSG",
-			Params:  []string{entity, text},
-		}
+		historyRing[cur%limit] = msg
 		cur++
 	}
@@ -205,2 +215,32 @@
 	}
 }
+
+func parseMessagesAfter(network *network, entity string, ref time.Time, limit int) ([]*irc.Message, error) {
+	path := logPath(network, entity, ref)
+	f, err := os.Open(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil, nil
+		}
+		return nil, err
+	}
+	defer f.Close()
+
+	var history []*irc.Message
+	sc := bufio.NewScanner(f)
+	for sc.Scan() && len(history) < limit {
+		msg, t, err := parseMessage(sc.Text(), entity, ref)
+		if err != nil {
+			return nil, err
+		} else if msg == nil || !t.After(ref) {
+			continue
+		}
+
+		history = append(history, msg)
+	}
+	if sc.Err() != nil {
+		return nil, sc.Err()
+	}
+
+	return history, nil
+}
