diff --git a/src/sbbs3/js_archive.c b/src/sbbs3/js_archive.c
index 0df62c4742e7d456933b7f348f8923b6288864aa..29592f7c96a40ae29a679f5062b5a5207b689047 100644
--- a/src/sbbs3/js_archive.c
+++ b/src/sbbs3/js_archive.c
@@ -429,12 +429,113 @@ js_list(JSContext *cx, uintN argc, jsval *arglist)
 	return retval;
 }
 
+static JSBool
+js_read(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval *argv = JS_ARGV(cx, arglist);
+	JSObject *obj = JS_THIS_OBJECT(cx, arglist);
+	jsrefcount	rc;
+	struct archive *ar;
+	struct archive_entry *entry;
+	int	result;
+	const char* filename;
+	char pattern[MAX_PATH + 1] = "";
+
+	if((filename = js_GetClassPrivate(cx, obj, &js_archive_class)) == NULL)
+		return JS_FALSE;
+
+ 	if(!js_argc(cx, argc, 1))
+		return JS_FALSE;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_NULL);
+
+	uintN argn = 0;
+	if(argc > argn && JSVAL_IS_STRING(argv[argn])) {
+		JSString* js_str = JS_ValueToString(cx, argv[argn]);
+		if(js_str == NULL) {
+			JS_ReportError(cx, "string conversion error");
+			return JS_FALSE;
+		}
+		JSSTRING_TO_STRBUF(cx, js_str, pattern, sizeof(pattern), NULL);
+		argn++;
+	}
+
+	if((ar = archive_read_new()) == NULL) {
+		JS_ReportError(cx, "archive_read_new() returned NULL");
+		return JS_FALSE;
+	}
+	archive_read_support_filter_all(ar);
+	archive_read_support_format_all(ar);
+	if((result = archive_read_open_filename(ar, filename, 10240)) != ARCHIVE_OK) {
+		JS_ReportError(cx, "archive_read_open_filename() returned %d: %s"
+			,result, archive_error_string(ar));
+		archive_read_free(ar);
+		return JS_FALSE;
+	}
+
+	JSBool retval = JS_TRUE;
+	rc = JS_SUSPENDREQUEST(cx);
+	jsint len = 0;
+	while(1) {
+		result = archive_read_next_header(ar, &entry);
+		if(result != ARCHIVE_OK) {
+			if(result != ARCHIVE_EOF) {
+				JS_ReportError(cx, "archive_read_next_header() returned %d: %s"
+					,result, archive_error_string(ar));
+				retval = JS_FALSE;
+			}
+			break;
+		}
+
+		if(archive_entry_filetype(entry) != AE_IFREG)
+			continue;
+
+		const char* pathname = archive_entry_pathname(entry);
+		if(pathname == NULL)
+			continue;
+
+		if(stricmp(pathname, pattern) != 0)
+			continue;
+
+		char* p = NULL;
+		size_t total = 0;
+		const void *buff;
+		size_t size;
+		la_int64_t offset;
+
+		for(;;) {
+			result = archive_read_data_block(ar, &buff, &size, &offset);
+			if(result != ARCHIVE_OK)
+				break;
+			char* np = realloc(p, total + size);
+			if(np == NULL)
+				break;
+			p = np;
+			memcpy(p + total, buff, size);
+			total += size;
+		}
+		JSString* js_str = JS_NewStringCopyN(cx, p, total);
+		JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(js_str));
+		free(p);
+		break;
+	}
+	archive_read_free(ar);
+	JS_RESUMEREQUEST(cx, rc);
+
+	return retval;
+}
+
 static jsSyncMethodSpec js_archive_functions[] = {
 	{ "create",		js_create,		1,	JSTYPE_NUMBER
 		,JSDOCSTR("[string format] [,boolean with_path = false] [,array file_list]")
 		,JSDOCSTR("create an archive of the specified format, returns the number of files archived, will throw exception upon error")
 		,31900
 	},
+	{ "read",		js_read,		1,	JSTYPE_STRING
+		,JSDOCSTR("string path/filename")
+		,JSDOCSTR("read and return the contents of the specified archived text file")
+		,31900
+	},
 	{ "extract",	js_extract,		1,	JSTYPE_NUMBER
 		,JSDOCSTR("output_directory [,boolean with_path = false] [,number max_files = 0] [,string file/pattern [...]]")
 		,JSDOCSTR("extract files from an archive to specified output directory, returns the number of files extracted, will throw exception upon error")