diff --git a/src/sbbs3/addfiles.c b/src/sbbs3/addfiles.c
index 1ed3b4866d232c660752bf5bfcccd95f52c78bff..b4df267505d2d85db8b93b44c04516224f5260e5 100644
--- a/src/sbbs3/addfiles.c
+++ b/src/sbbs3/addfiles.c
@@ -245,7 +245,7 @@ void addlist(char *inpath, uint dirnum, const char* uploader, uint dskip, uint s
 					reupload(&smb, &f);
 			}
 			else
-				result = smb_addfile(&smb, &f, SMB_SELFPACK, ext_desc, filepath);
+				result = smb_addfile(&smb, &f, SMB_SELFPACK, ext_desc, NULL, filepath);
 			smb_freefilemem(&f);
 			if(result != SMB_SUCCESS)
 				fprintf(stderr, "!Error %d (%s) adding file to %s", result, smb.last_error, smb.file);
@@ -415,7 +415,7 @@ void addlist(char *inpath, uint dirnum, const char* uploader, uint dskip, uint s
 				reupload(&smb, &f);
 		}
 		else
-			result = smb_addfile(&smb, &f, SMB_SELFPACK, ext_desc, filepath);
+			result = smb_addfile(&smb, &f, SMB_SELFPACK, ext_desc, NULL, filepath);
 		smb_freefilemem(&f);
 		if(result != SMB_SUCCESS)
 			fprintf(stderr, "!ERROR %d (%s) writing to %s\n"
@@ -795,7 +795,7 @@ int main(int argc, char **argv)
 					result = smb_updatemsg(&smb, &f);
 			}
 			else
-				result = smb_addfile(&smb, &f, SMB_SELFPACK, ext_desc, str);
+				result = smb_addfile(&smb, &f, SMB_SELFPACK, ext_desc, NULL, str);
 			if(mode&UL_STATS)
 				updatestats(l);
 			files++;
diff --git a/src/sbbs3/filedat.c b/src/sbbs3/filedat.c
index 298842dbf8d2be1b580b6d8c07c1aa9012889853..440ac762febb5ad1f1203eb2815b2d92e2f4214c 100644
--- a/src/sbbs3/filedat.c
+++ b/src/sbbs3/filedat.c
@@ -29,6 +29,7 @@
 #include "load_cfg.h"	// smb_open_dir()
 #include "scfglib.h"
 #include "sauce.h"
+#include "crc32.h"
 
 /* libarchive: */
 #include <archive.h>
@@ -636,8 +637,11 @@ bool addfile(scfg_t* cfg, uint dirnum, file_t* f, const char* extdesc, client_t*
 
 	getfilepath(cfg, f, fpath);
 	file_client_hfields(f, client);
-	int result = smb_addfile(&smb, f, SMB_SELFPACK, extdesc, fpath);
+	str_list_t list = list_archive_contents(fpath, /* pattern: */NULL
+		,(cfg->dir[dirnum]->misc & DIR_NOHASH) == 0, /* error: */NULL, /* size: */0);
+	int result = smb_addfile_withlist(&smb, f, SMB_SELFPACK, extdesc, list, fpath);
 	smb_close(&smb);
+	strListFree(&list);
 	return result == SMB_SUCCESS;
 }
 
@@ -698,6 +702,108 @@ int archive_type(const char* archive, char* str, size_t size)
 	return result;
 }
 
+str_list_t list_archive_contents(const char* filename, const char* pattern, bool hash, char* error, size_t maxerrlen)
+{
+	int result;
+	struct archive *ar;
+	struct archive_entry *entry;
+
+	if((ar = archive_read_new()) == NULL) {
+		safe_snprintf(error, maxerrlen, "archive_read_new() returned NULL");
+		return NULL;
+	}
+	archive_read_support_filter_all(ar);
+	archive_read_support_format_all(ar);
+	if((result = archive_read_open_filename(ar, filename, 10240)) != ARCHIVE_OK) {
+		safe_snprintf(error, maxerrlen, "archive_read_open_filename() returned %d: %s"
+			,result, archive_error_string(ar));
+		archive_read_free(ar);
+		return NULL;
+	}
+
+	str_list_t list = strListInit();
+    if(list == NULL) {
+		safe_snprintf(error, maxerrlen, "strListInit() returned NULL");
+		archive_read_free(ar);
+		return NULL;
+	}
+
+	while(1) {
+		result = archive_read_next_header(ar, &entry);
+		if(result != ARCHIVE_OK) {
+			if(result != ARCHIVE_EOF) {
+				safe_snprintf(error, maxerrlen, "archive_read_next_header() returned %d: %s"
+					,result, archive_error_string(ar));
+				archive_read_free(ar);
+				strListFree(&list);
+				return NULL;
+			}
+			break;
+		}
+
+		const char* pathname = archive_entry_pathname(entry);
+		if(pathname == NULL)
+			continue;
+
+		if(pattern != NULL && *pattern && !wildmatch(pathname, pattern, /* path: */false, /* case-sensitive: */false))
+			continue;
+
+		const char* type;
+		switch(archive_entry_filetype(entry)) {
+			case AE_IFREG:
+				type = "file";
+				break;
+			case AE_IFLNK:
+				type = "link";
+				break;
+			case AE_IFDIR:
+				type = "directory";
+				break;
+			default:
+				continue;
+		}
+
+		iniSetString(&list, pathname, "type", type, /* style: */NULL);
+		iniSetBytes(&list, pathname, "size", 1, archive_entry_size(entry), /* style: */NULL);
+		iniSetDateTime(&list, pathname, "time", true, archive_entry_mtime(entry), NULL);
+		iniSetInteger(&list, pathname, "mode", archive_entry_mode(entry), NULL);
+		iniSetString(&list, pathname, "format", archive_format_name(ar), NULL);
+		iniSetString(&list, pathname, "compression", archive_filter_name(ar, 0), NULL);
+
+		if(hash && archive_entry_filetype(entry) == AE_IFREG) {
+			MD5 md5_ctx;
+			SHA1_CTX sha1_ctx;
+			uint8_t md5[MD5_DIGEST_SIZE];
+			uint8_t sha1[SHA1_DIGEST_SIZE];
+			uint32_t crc32 = 0;
+
+			MD5_open(&md5_ctx);
+			SHA1Init(&sha1_ctx);
+
+			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;
+				crc32 = crc32i(~crc32, buff, size);
+				MD5_digest(&md5_ctx, buff, size);
+				SHA1Update(&sha1_ctx, buff, size);
+			}
+			MD5_close(&md5_ctx, md5);
+			SHA1Final(&sha1_ctx, sha1);
+			iniSetHexInt(&list, pathname, "crc32", crc32, NULL);
+			char hex[128];
+			iniSetString(&list, pathname, "md5", MD5_hex(hex, md5), NULL);
+			iniSetString(&list, pathname, "sha1", SHA1_hex(hex, sha1), NULL);
+		}
+	}
+	archive_read_free(ar);
+	return list;
+}
+
 str_list_t directory(const char* path)
 {
 	int			flags = GLOB_MARK;
diff --git a/src/sbbs3/filedat.h b/src/sbbs3/filedat.h
index c478d93436201030b66cd374dfdaed170fd62c50..0f96a15f5179664401e20cc1ec4562209ed6b8c2 100644
--- a/src/sbbs3/filedat.h
+++ b/src/sbbs3/filedat.h
@@ -67,6 +67,7 @@ DLLEXPORT int			file_sauce_hfields(file_t*, struct sauce_charinfo*);
 DLLEXPORT str_list_t	directory(const char* path);
 DLLEXPORT long			create_archive(const char* archive, const char* format
 						               ,bool with_path, str_list_t file_list, char* error, size_t maxerrlen);
+DLLEXPORT str_list_t	list_archive_contents(const char* archive, const char* pattern, bool hash, char* error, size_t maxerrlen);
 DLLEXPORT char*			cmdstr(scfg_t*, user_t*, const char* instr, const char* fpath, const char* fspec, char* cmd, size_t);
 DLLEXPORT long			extract_files_from_archive(const char* archive, const char* outdir, const char* allowed_filename_chars
 						                           ,bool with_path, long max_files, str_list_t file_list, char* error, size_t);
diff --git a/src/sbbs3/js_filebase.c b/src/sbbs3/js_filebase.c
index a093f139b955591b1668178fc072a8f87576b55b..c43123f31a81a871e48d7cf27abe1932d2942ecd 100644
--- a/src/sbbs3/js_filebase.c
+++ b/src/sbbs3/js_filebase.c
@@ -164,6 +164,51 @@ js_dump_file(JSContext *cx, uintN argc, jsval *arglist)
 	return JS_TRUE;
 }
 
+static bool
+set_content_properties(JSContext *cx, JSObject* obj, const char* fname, str_list_t ini)
+{
+	const char*	val;
+	jsval		js_val;
+	JSString*	js_str;
+	const char* key;
+	const uintN flags = JSPROP_ENUMERATE | JSPROP_READONLY;
+
+	if(fname == NULL
+		|| (js_str = JS_NewStringCopyZ(cx, fname)) == NULL
+		|| !JS_DefineProperty(cx, obj, "name", STRING_TO_JSVAL(js_str), NULL, NULL, flags))
+		return false;
+
+	const char* key_list[] = { "type", "time", "format", "compression", "md5", "sha1", NULL };
+	for(size_t i = 0; key_list[i] != NULL; i++) {
+		key = key_list[i];
+		if((val = iniGetString(ini, NULL, key, NULL, NULL)) != NULL
+			&& ((js_str = JS_NewStringCopyZ(cx, val)) == NULL
+				|| !JS_DefineProperty(cx, obj, key, STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
+			return false;
+	}
+
+	key = "size";
+	js_val = DOUBLE_TO_JSVAL((jsdouble)iniGetBytes(ini, NULL, key, 1, -1));
+	if(!JS_DefineProperty(cx, obj, key, js_val, NULL, NULL, flags))
+		return false;
+
+	key = "mode";
+	if(iniKeyExists(ini, NULL, key)) {
+		js_val = INT_TO_JSVAL(iniGetInteger(ini, NULL, key, 0));
+		if(!JS_DefineProperty(cx, obj, key, js_val, NULL, NULL, flags))
+			return false;
+	}
+
+	key = "crc32";
+	if(iniKeyExists(ini, NULL, key)) {
+		js_val = UINT_TO_JSVAL(iniGetLongInt(ini, NULL, key, 0));
+		if(!JS_DefineProperty(cx, obj, key, js_val, NULL, NULL, flags))
+			return false;
+	}
+
+	return true;
+}
+
 static bool
 set_file_properties(JSContext *cx, JSObject* obj, file_t* f, enum file_detail detail)
 {
@@ -181,67 +226,67 @@ set_file_properties(JSContext *cx, JSObject* obj, file_t* f, enum file_detail de
 		|| !JS_DefineProperty(cx, obj, "name", STRING_TO_JSVAL(js_str), NULL, NULL, flags))
 		return false;
 
-	if(((f->from != NULL && *f->from != '\0') || detail > file_detail_extdesc)
+	if(((f->from != NULL && *f->from != '\0') || detail > file_detail_content)
 		&& ((js_str = JS_NewStringCopyZ(cx, f->from)) == NULL
 			|| !JS_DefineProperty(cx, obj, "from", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
 		return false;
 
-	if(((f->from_ip != NULL && *f->from_ip != '\0') || detail > file_detail_extdesc)
+	if(((f->from_ip != NULL && *f->from_ip != '\0') || detail > file_detail_content)
 		&& ((js_str = JS_NewStringCopyZ(cx, f->from_ip)) == NULL
 			|| !JS_DefineProperty(cx, obj, "from_ip_addr", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
 		return false;
 
-	if(((f->from_host != NULL && *f->from_host != '\0') || detail > file_detail_extdesc)
+	if(((f->from_host != NULL && *f->from_host != '\0') || detail > file_detail_content)
 		&& ((js_str = JS_NewStringCopyZ(cx, f->from_host)) == NULL
 			|| !JS_DefineProperty(cx, obj, "from_host_name", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
 		return false;
 
-	if(((f->from_prot != NULL && *f->from_prot != '\0') || detail > file_detail_extdesc)
+	if(((f->from_prot != NULL && *f->from_prot != '\0') || detail > file_detail_content)
 		&& ((js_str = JS_NewStringCopyZ(cx, f->from_prot)) == NULL
 			|| !JS_DefineProperty(cx, obj, "from_protocol", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
 		return false;
 
-	if(((f->from_port != NULL && *f->from_port != '\0') || detail > file_detail_extdesc)
+	if(((f->from_port != NULL && *f->from_port != '\0') || detail > file_detail_content)
 		&& ((js_str = JS_NewStringCopyZ(cx, f->from_port)) == NULL
 			|| !JS_DefineProperty(cx, obj, "from_port", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
 		return false;
 
-	if(((f->author != NULL && *f->author != '\0') || detail > file_detail_extdesc)
+	if(((f->author != NULL && *f->author != '\0') || detail > file_detail_content)
 		&& ((js_str = JS_NewStringCopyZ(cx, f->author)) == NULL
 			|| !JS_DefineProperty(cx, obj, "author", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
 		return false;
 
-	if(((f->author_org != NULL && *f->author_org != '\0') || detail > file_detail_extdesc)
+	if(((f->author_org != NULL && *f->author_org != '\0') || detail > file_detail_content)
 		&& ((js_str = JS_NewStringCopyZ(cx, f->author_org)) == NULL
 			|| !JS_DefineProperty(cx, obj, "author_org", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
 		return false;
 
-	if(((f->to_list != NULL && *f->to_list != '\0') || detail > file_detail_extdesc)
+	if(((f->to_list != NULL && *f->to_list != '\0') || detail > file_detail_content)
 		&& ((js_str = JS_NewStringCopyZ(cx, f->to_list)) == NULL
 			|| !JS_DefineProperty(cx, obj, "to_list", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
 		return false;
 
 	val = BOOLEAN_TO_JSVAL(f->idx.attr & FILE_ANONYMOUS);
-	if((val == JSVAL_TRUE || detail > file_detail_extdesc)
+	if((val == JSVAL_TRUE || detail > file_detail_content)
 		&& !JS_DefineProperty(cx, obj, "anon", val, NULL, NULL, flags))
 		return false;
 
-	if(((f->tags != NULL && *f->tags != '\0') || detail > file_detail_extdesc)
+	if(((f->tags != NULL && *f->tags != '\0') || detail > file_detail_content)
 		&& ((js_str = JS_NewStringCopyZ(cx, f->tags)) == NULL
 			|| !JS_DefineProperty(cx, obj, "tags", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
 		return false;
 
-	if(((f->desc != NULL && *f->desc != '\0') || detail > file_detail_extdesc)
+	if(((f->desc != NULL && *f->desc != '\0') || detail > file_detail_content)
 		&& ((js_str = JS_NewStringCopyZ(cx, f->desc)) == NULL
 			|| !JS_DefineProperty(cx, obj, "desc", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
 		return false;
 
-	if(((f->extdesc != NULL && *f->extdesc != '\0') || detail > file_detail_extdesc)
+	if(((f->extdesc != NULL && *f->extdesc != '\0') || detail > file_detail_content)
 		&& ((js_str = JS_NewStringCopyZ(cx, f->extdesc)) == NULL
 			|| !JS_DefineProperty(cx, obj, "extdesc", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
 		return false;
 
-	if(f->cost > 0 || detail > file_detail_extdesc) {
+	if(f->cost > 0 || detail > file_detail_content) {
 		val = UINT_TO_JSVAL(f->cost);
 		if(!JS_DefineProperty(cx, obj, "cost", val, NULL, NULL, flags))
 			return false;
@@ -253,17 +298,17 @@ set_file_properties(JSContext *cx, JSObject* obj, file_t* f, enum file_detail de
 	val = UINT_TO_JSVAL(f->hdr.when_written.time);
 	if(!JS_DefineProperty(cx, obj, "time", val, NULL, NULL, flags))
 		return false;
-	if(f->hdr.when_imported.time > 0 || detail > file_detail_extdesc) {
+	if(f->hdr.when_imported.time > 0 || detail > file_detail_content) {
 		val = UINT_TO_JSVAL(f->hdr.when_imported.time);
 		if(!JS_DefineProperty(cx, obj, "added", val, NULL, NULL, flags))
 			return false;
 	}
-	if(f->hdr.last_downloaded > 0 || detail > file_detail_extdesc) {
+	if(f->hdr.last_downloaded > 0 || detail > file_detail_content) {
 		val = UINT_TO_JSVAL(f->hdr.last_downloaded);
 		if(!JS_DefineProperty(cx, obj, "last_downloaded", val, NULL, NULL, flags))
 			return false;
 	}
-	if(f->hdr.times_downloaded > 0 || detail > file_detail_extdesc) {
+	if(f->hdr.times_downloaded > 0 || detail > file_detail_content) {
 		val = UINT_TO_JSVAL(f->hdr.times_downloaded);
 		if(!JS_DefineProperty(cx, obj, "times_downloaded", val, NULL, NULL, flags))
 			return false;
@@ -291,6 +336,32 @@ set_file_properties(JSContext *cx, JSObject* obj, file_t* f, enum file_detail de
 			return false;
 	}
 
+	if(detail >= file_detail_content) {
+		JSObject* array;
+		if((array = JS_NewArrayObject(cx, 0, NULL)) == NULL) {
+			JS_ReportError(cx, "array allocation failure, line %d", __LINE__);
+			return false;
+		}
+		str_list_t ini = strListSplit(NULL, f->content, "\r\n");
+		str_list_t file_list = iniGetSectionList(ini, NULL);
+		if(file_list != NULL) {
+			for(size_t i = 0; file_list[i] != NULL; i++) {
+				JSObject* fobj;
+				if((fobj = JS_NewObject(cx, NULL, NULL, array)) == NULL) {
+					JS_ReportError(cx, "object allocation failure, line %d", __LINE__);
+					return false;
+				}
+				str_list_t section = iniGetSection(ini, file_list[i]);
+				set_content_properties(cx, fobj, file_list[i], section);
+				JS_DefineElement(cx, array, i, OBJECT_TO_JSVAL(fobj), NULL, NULL, JSPROP_ENUMERATE);
+				iniFreeStringList(section);
+			}
+			strListFree(&file_list);
+		}
+		strListFree(&ini);
+		JS_DefineProperty(cx, obj, "content", OBJECT_TO_JSVAL(array), NULL, NULL, JSPROP_ENUMERATE);
+	}
+
 	return true;
 }
 
@@ -1164,7 +1235,10 @@ js_add_file(JSContext *cx, uintN argc, jsval *arglist)
 		char fpath[MAX_PATH + 1];
 		getfilepath(scfg, &file, fpath);
 		file_client_hfields(&file, client);
-		p->smb_result = smb_addfile(&p->smb, &file, SMB_SELFPACK, extdesc, fpath);
+		str_list_t list = list_archive_contents(fpath, /* pattern: */NULL
+			,(scfg->dir[file.dir]->misc & DIR_NOHASH) == 0, /* error: */NULL, /* size: */0);
+		p->smb_result = smb_addfile_withlist(&p->smb, &file, SMB_SELFPACK, extdesc, list, fpath);
+		strListFree(&list);
 		JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(p->smb_result == SMB_SUCCESS));
 	}
 	JS_RESUMEREQUEST(cx, rc);
@@ -1246,8 +1320,13 @@ js_update_file(JSContext *cx, uintN argc, jsval *arglist)
 				if(strcmp(extdesc ? extdesc : "", file.extdesc ? file.extdesc : "") == 0)
 					p->smb_result = smb_putfile(&p->smb, &file);
 				else {
-					if((p->smb_result = smb_removefile(&p->smb, &file)) == SMB_SUCCESS)
-						p->smb_result = smb_addfile(&p->smb, &file, SMB_SELFPACK, extdesc, newfname);
+					if((p->smb_result = smb_removefile(&p->smb, &file)) == SMB_SUCCESS) {
+						str_list_t list = list_archive_contents(newfname, /* pattern: */NULL
+							,file.dir < scfg->total_dirs && (scfg->dir[file.dir]->misc & DIR_NOHASH) == 0
+							,/* error: */NULL, /* size: */0);
+						p->smb_result = smb_addfile_withlist(&p->smb, &file, SMB_SELFPACK, extdesc, list, newfname);
+						strListFree(&list);
+					}
 				}
 			}
 		}
@@ -1749,6 +1828,7 @@ static char* filebase_detail_prop_desc[] = {
 	 "Include indexed-filenames only",
 	 "Normal level of file detail (e.g. full filenames, minimal meta data)",
 	 "Normal level of file detail plus extended descriptions",
+	 "Normal level of file detail plus extended descriptions and archived contents",
 	 "Maximum file detail, include undefined/null property values",
 	 NULL
 };
@@ -1789,10 +1869,12 @@ JSObject* js_CreateFileBaseClass(JSContext* cx, JSObject* parent, scfg_t* cfg)
 				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
 			JS_DefineProperty(cx, detail, "EXTENDED", INT_TO_JSVAL(file_detail_extdesc), NULL, NULL
 				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
-			JS_DefineProperty(cx, detail, "MAX", INT_TO_JSVAL(file_detail_extdesc + 1), NULL, NULL
+			JS_DefineProperty(cx, detail, "CONTENTS", INT_TO_JSVAL(file_detail_content), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, detail, "MAX", INT_TO_JSVAL(file_detail_content + 1), NULL, NULL
 				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
 #ifdef BUILD_JSDOCS
-			js_DescribeSyncObject(cx, detail, "Detail level numeric constants", 0);
+			js_DescribeSyncObject(cx, detail, "Detail level numeric constants (in increasing verbosity)", 0);
 			js_CreateArrayOfStrings(cx, detail, "_property_desc_list", filebase_detail_prop_desc, JSPROP_READONLY);
 #endif
 		}
diff --git a/src/sbbs3/upgrade_to_v319.c b/src/sbbs3/upgrade_to_v319.c
index a4f8cc9f98c08453729583e813afd09c34d5b46a..8c0d668fa3b89375329d82aabce96b538f2fd016 100644
--- a/src/sbbs3/upgrade_to_v319.c
+++ b/src/sbbs3/upgrade_to_v319.c
@@ -660,7 +660,10 @@ bool upgrade_file_bases(bool hash)
 					if(*extdesc)
 						body = extdesc;
 				}
-				result = smb_addfile(&smb, &file, SMB_FASTALLOC, body, fpath);
+				str_list_t list = list_archive_contents(fpath, /* pattern: */NULL
+					,(scfg.dir[i]->misc & DIR_NOHASH) == 0, /* error: */NULL, /* size: */0);
+				result = smb_addfile_withlist(&smb, &file, SMB_FASTALLOC, body, list, fpath);
+				strListFree(&list);
 			}
 			if(result != SMB_SUCCESS) {
 				fprintf(stderr, "\n!Error %d (%s) adding file to %s\n", result, smb.last_error, smb.file);
diff --git a/src/smblib/smbdefs.h b/src/smblib/smbdefs.h
index 5f692adf18ecd154d36b320da603b1be7199f9ed..a91eea7dfc0e031d3f51005f97dfe76b3951b7dd 100644
--- a/src/smblib/smbdefs.h
+++ b/src/smblib/smbdefs.h
@@ -632,6 +632,10 @@ typedef struct {				/* Message or File */
 		uchar*	text;			/* Message body text (optional) */
 		char*	extdesc;		/* File extended description */
 	};
+	union {
+		uchar*	tail;			/* Message body tail (optional) */
+		char*	content;		/* Archive content list */
+	};
 	char*		tags;			/* Message tags (space-delimited) */
 	char*		editor;			/* Message editor (if known) */
 	char*		mime_version;	/* MIME Version (if applicable) */
diff --git a/src/smblib/smbfile.c b/src/smblib/smbfile.c
index f96426376b6296018020188651aa42589ebeb5fc..da6cca6a1d3652db27e8b6bfde46c8e7b3241986 100644
--- a/src/smblib/smbfile.c
+++ b/src/smblib/smbfile.c
@@ -295,7 +295,9 @@ int smb_getfile(smb_t* smb, smbfile_t* file, enum file_detail detail)
 		if((result = smb_getmsghdr(smb, file)) != SMB_SUCCESS)
 			return result;
 		if(detail >= file_detail_extdesc)
-			file->extdesc = smb_getmsgtxt(smb, file, GETMSGTXT_ALL);
+			file->extdesc = smb_getmsgtxt(smb, file, GETMSGTXT_BODY_ONLY);
+		if(detail >= file_detail_content)
+			file->content = smb_getmsgtxt(smb, file, GETMSGTXT_TAIL_ONLY);
 	}
 	file->dir = smb->dirnum;
 
@@ -325,7 +327,7 @@ void smb_freefilemem(smbfile_t* file)
 
 /****************************************************************************/
 /****************************************************************************/
-int smb_addfile(smb_t* smb, smbfile_t* file, int storage, const char* extdesc, const char* path)
+int smb_addfile(smb_t* smb, smbfile_t* file, int storage, const char* extdesc, const char* content, const char* path)
 {
 	if(file->name == NULL || *file->name == '\0') {
 		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s missing name", __FUNCTION__);
@@ -343,7 +345,28 @@ int smb_addfile(smb_t* smb, smbfile_t* file, int storage, const char* extdesc, c
 	}
 	file->hdr.attr |= MSG_FILE;
 	file->hdr.type = SMB_MSG_TYPE_FILE;
-	return smb_addmsg(smb, file, storage, SMB_HASH_SOURCE_NONE, XLAT_NONE, /* body: */(const uchar*)extdesc, /* tail: */NULL);
+	return smb_addmsg(smb, file, storage, SMB_HASH_SOURCE_NONE, XLAT_NONE
+		,/* body: */(const uchar*)extdesc, /* tail: */(const uchar*)content);
+}
+
+/****************************************************************************/
+/* Like smb_addfile(), except 'content' is a str_list_t: 'list'				*/
+/****************************************************************************/
+int smb_addfile_withlist(smb_t* smb, smbfile_t* file, int storage, const char* extdesc, str_list_t list, const char* path)
+{
+	char* content = NULL;
+	int result;
+
+	if(list != NULL) {
+		size_t size = strListCount(list) * 1024;
+		content = calloc(1, size);
+		if(content == NULL)
+			return SMB_ERR_MEM;
+		strListCombine(list, content, size - 1, "\r\n");
+	}
+	result = smb_addfile(smb, file, storage, extdesc, content, path);
+	free(content);
+	return result;
 }
 
 /****************************************************************************/
@@ -353,7 +376,7 @@ int smb_renewfile(smb_t* smb, smbfile_t* file, int storage, const char* path)
 	int result;
 	if((result = smb_removefile(smb, file)) != SMB_SUCCESS)
 		return result;
-	return smb_addfile(smb, file, storage, file->extdesc, path);
+	return smb_addfile(smb, file, storage, file->extdesc, file->content, path);
 }
 
 /****************************************************************************/
diff --git a/src/smblib/smblib.c b/src/smblib/smblib.c
index 0ddccff1edcc52a5c6e2b9b144fedbdd2660ae92..ab102772231e502b464d3c591e2b5b1dc6956ea9 100644
--- a/src/smblib/smblib.c
+++ b/src/smblib/smblib.c
@@ -1141,6 +1141,10 @@ void smb_freemsgmem(smbmsg_t* msg)
 		free(msg->text);
 		msg->text = NULL;
 	}
+	if(msg->tail != NULL) {
+		free(msg->tail);
+		msg->tail = NULL;
+	}
 }
 
 /****************************************************************************/
diff --git a/src/smblib/smblib.h b/src/smblib/smblib.h
index 660db76ff3cf01b313ed898af573cc99d2b85ca6..cb28c223c357a1be1cb5b7feadddc97dc94bdd5b 100644
--- a/src/smblib/smblib.h
+++ b/src/smblib/smblib.h
@@ -283,8 +283,9 @@ SMBEXPORT int 		smb_open_fp(smb_t*, FILE**, int share);
 SMBEXPORT void		smb_close_fp(FILE**);
 
 /* New FileBase API: */
-enum file_detail { file_detail_index, file_detail_normal, file_detail_extdesc };
-SMBEXPORT int		smb_addfile(smb_t*, smbfile_t*, int storage, const char* extdesc, const char* path);
+enum file_detail { file_detail_index, file_detail_normal, file_detail_extdesc, file_detail_content };
+SMBEXPORT int		smb_addfile(smb_t*, smbfile_t*, int storage, const char* extdesc, const char* content, const char* path);
+SMBEXPORT int		smb_addfile_withlist(smb_t*, smbfile_t*, int storage, const char* extdesc, str_list_t, const char* path);
 SMBEXPORT int		smb_renewfile(smb_t*, smbfile_t*, int storage, const char* path);
 SMBEXPORT int		smb_getfile(smb_t*, smbfile_t*, enum file_detail);
 SMBEXPORT int		smb_putfile(smb_t*, smbfile_t*);