summary refs log tree commit diff
diff options
context:
space:
mode:
authorJune McEnroe <june@causal.agency>2021-06-11 17:32:18 -0400
committerJune McEnroe <june@causal.agency>2021-06-11 18:29:36 -0400
commitf1f9c20bd8d9d175f8db73b062a60a9f79e95a7e (patch)
treec0db35013b96f6c6fd5322c5f058b681a790df0e
parentGeneralize index.{atom,html} to search pages (diff)
downloadbubger-f1f9c20bd8d9d175f8db73b062a60a9f79e95a7e.tar.gz
bubger-f1f9c20bd8d9d175f8db73b062a60a9f79e95a7e.zip
Generate arbitrary search pages and feeds
First export ALL threads, then generate search pages. Skip search
threads that weren't exported by the ALL search, i.e. non-root
threads.
-rw-r--r--archive.c134
-rw-r--r--bubger.150
-rw-r--r--concat.c11
3 files changed, 159 insertions, 36 deletions
diff --git a/archive.c b/archive.c
index 0bee374..400e040 100644
--- a/archive.c
+++ b/archive.c
@@ -80,6 +80,107 @@ static void createDirs(void) {
 	createDir("thread");
 }
 
+static struct Search {
+	size_t cap;
+	size_t len;
+	char **names;
+	char **exprs;
+} search;
+
+static void searchAdd(const char *name, const char *expr) {
+	if (search.len == search.cap) {
+		search.cap = (search.cap ? search.cap * 2 : 8);
+		search.names = realloc(
+			search.names, sizeof(*search.names) * search.cap
+		);
+		search.exprs = realloc(
+			search.exprs, sizeof(*search.exprs) * search.cap
+		);
+		if (!search.names || !search.exprs) err(EX_OSERR, "realloc");
+	}
+	size_t i = search.len++;
+	search.names[i] = strdup(name);
+	search.exprs[i] = strdup(expr);
+	if (!search.names[i] || !search.exprs[i]) err(EX_OSERR, "strdup");
+}
+
+static int searchRead(const char *path) {
+	FILE *file = fopen(path, "r");
+	if (!file) return -1;
+
+	int line = 1;
+	size_t cap = 0;
+	char *buf = NULL;
+	for (ssize_t len; 0 < (len = getline(&buf, &cap, file)); ++line) {
+		if (buf[len - 1] == '\n') buf[len - 1] = '\0';
+		if (!buf[0] || buf[0] == '#') continue;
+
+		char *expr = buf;
+		char *name = strsep(&expr, " \t");
+		if (!expr) {
+			errx(EX_DATAERR, "%s:%d: missing search expression", path, line);
+		}
+		searchAdd(name, &expr[strspn(expr, " \t")]);
+	}
+	if (ferror(file)) err(EX_IOERR, "%s", path);
+	fclose(file);
+	return 0;
+}
+
+static void searchDefault(void) {
+	for (size_t i = 0; i < search.len; ++i) {
+		if (!strcmp(search.names[i], "index")) return;
+	}
+	searchAdd("index", "ALL");
+}
+
+static const char *algo = "REFERENCES";
+
+static void
+searchThreads(struct IMAP *imap, const char *name, const char *expr) {
+	struct Resp resp;
+	struct List threads = {0};
+	struct List envelopeItems = {0};
+	struct Envelope *envelopes = NULL;
+
+	enum Atom thread = atom("thread");
+	fprintf(
+		imap->w, "%s UID THREAD %s UTF-8 %s\r\n",
+		Atoms[thread], algo, expr
+	);
+	for (; resp = respOk(imapResp(imap)), resp.tag != thread; respFree(resp)) {
+		if (resp.resp != AtomThread) continue;
+		threads = resp.data;
+		resp.data = (struct List) {0}; // prevent freeing threads with resp
+	}
+	respFree(resp);
+	if (!threads.len) goto concat;
+
+	enum Atom concat = atom("concat");
+	envelopes = calloc(threads.len, sizeof(*envelopes));
+	if (!envelopes) err(EX_OSERR, "calloc");
+	concatFetch(imap->w, concat, threads);
+	for (; resp = respOk(imapResp(imap)), resp.tag != concat; respFree(resp)) {
+		if (resp.resp != AtomFetch) continue;
+		if (!resp.data.len) errx(EX_PROTOCOL, "missing FETCH data");
+		// Prevent freeing data in envelopes with resp:
+		struct Data items = dataTake(&resp.data.ptr[0]);
+		concatData(threads, envelopes, dataCheck(items, List).list);
+		listPush(&envelopeItems, items);
+	}
+	respFree(resp);
+
+concat:
+	concatSearch(name, threads, envelopes);
+
+	for (size_t i = 0; i < threads.len; ++i) {
+		envelopeFree(envelopes[i]);
+	}
+	free(envelopes);
+	listFree(envelopeItems);
+	listFree(threads);
+}
+
 int main(int argc, char *argv[]) {
 	int exitStatus = 0;
 
@@ -90,8 +191,7 @@ int main(int argc, char *argv[]) {
 
 	bool idle = false;
 	const char *mailbox = "Archive";
-	const char *algo = "REFERENCES";
-	const char *search = "ALL";
+	const char *searchPath = NULL;
 
 	for (
 		int opt;
@@ -104,7 +204,7 @@ int main(int argc, char *argv[]) {
 				if (error) err(EX_NOINPUT, "%s", optarg);
 			}
 			break; case 'H': concatHead = optarg;
-			break; case 'S': search = optarg;
+			break; case 'S': searchPath = optarg;
 			break; case 'T': baseTitle = optarg;
 			break; case 'a': algo = optarg;
 			break; case 'h': host = optarg;
@@ -131,6 +231,14 @@ int main(int argc, char *argv[]) {
 	}
 	if (!baseTitle) baseTitle = mailbox;
 
+	if (searchPath) {
+		int error = searchRead(searchPath);
+		if (error) err(EX_NOINPUT, "%s", searchPath);
+	} else {
+		searchRead("SEARCH");
+	}
+	searchDefault();
+
 	char *pass = NULL;
 	if (passPath) {
 		FILE *file = fopen(passPath, "r");
@@ -209,24 +317,18 @@ examine:;
 	} else {
 		goto logout;
 	}
+	createDirs();
 
 	struct List threads = {0};
 	enum Atom thread = atom("thread");
-	fprintf(
-		imap.w, "%s UID THREAD %s UTF-8 %s\r\n",
-		Atoms[thread], algo, search
-	);
+	fprintf(imap.w, "%s UID THREAD %s UTF-8 ALL\r\n", Atoms[thread], algo);
 	for (; resp = respOk(imapResp(&imap)), resp.tag != thread; respFree(resp)) {
 		if (resp.resp != AtomThread) continue;
-		if (!resp.data.len) {
-			errx(EX_TEMPFAIL, "no messages matching %s", search);
-		}
 		threads = resp.data;
 		resp.data = (struct List) {0}; // prevent freeing threads with resp
 	}
 	respFree(resp);
 
-	createDirs();
 	enum Atom export = atom("export");
 	if (!exportFetch(imap.w, export, threads)) {
 		goto concat;
@@ -260,10 +362,6 @@ concat:;
 	respFree(resp);
 
 	concatThreads(threads, envelopes);
-	concatSearch("index", threads, envelopes);
-	fflush(stdout);
-	uidWrite("UIDNEXT", uidNext);
-
 	for (size_t i = 0; i < threads.len; ++i) {
 		envelopeFree(envelopes[i]);
 	}
@@ -271,6 +369,12 @@ concat:;
 	listFree(envelopeItems);
 	listFree(threads);
 
+	for (size_t i = 0; i < search.len; ++i) {
+		searchThreads(&imap, search.names[i], search.exprs[i]);
+	}
+
+	fflush(stdout);
+	uidWrite("UIDNEXT", uidNext);
 	if (!idle) goto logout;
 
 idle:
diff --git a/bubger.1 b/bubger.1
index a456c19..c429721 100644
--- a/bubger.1
+++ b/bubger.1
@@ -12,7 +12,7 @@
 .Op Fl A Ar entries
 .Op Fl C Ar path
 .Op Fl H Ar head
-.Op Fl S Ar search
+.Op Fl S Ar file
 .Op Fl T Ar title
 .Op Fl a Ar algo
 .Op Fl h Ar host
@@ -43,7 +43,7 @@ The arguments are as follows:
 .Bl -tag -width Ds
 .It Fl A Ar entries
 Limit the number of entries
-in the index Atom feed.
+in search Atom feeds.
 The default limit is 20.
 Thread Atom feeds
 always contain all entries.
@@ -60,14 +60,14 @@ to the
 .Sy <head>
 element of HTML pages.
 .
-.It Fl S Ar search
-Limit threads to messages matching
-.Ar search .
-The default search is
-.Sy ALL .
+.It Fl S Ar file
+Read search definitions from
+.Ar file .
+Search definitions are documented in
+.Sx FILES .
 .
 .It Fl T Ar title
-Set the title for the index HTML page and Atom feed.
+Set the base title for search HTML pages and Atom feeds.
 The default title is the mailbox name.
 .
 .It Fl a Ar algo
@@ -100,7 +100,7 @@ Add a
 .Dq write
 mailto link of
 .Ar addr
-to the index page navigation.
+to search page navigation.
 .
 .It Fl p Ar port
 Connect to IMAP on
@@ -120,7 +120,7 @@ Add a
 .Dq subscribe
 link of
 .Ar url
-to the index page navigation.
+to search page navigation.
 .
 .It Fl u Ar base
 Set the base URL for links in Atom feeds.
@@ -159,10 +159,8 @@ The IMAP password.
 .
 .Sh FILES
 .Bl -tag -width Ds
-.It Pa index.atom
-Rendered Atom feed of recent messages.
-.It Pa index.html
-Rendered HTML index of all threads.
+.It Pa *.atom, *.html
+Rendered Atom feeds and HTML pages for each search.
 .It Pa thread/*.atom , Pa thread/*.html , Pa thread/*.mbox
 Rendered Atom, HTML and mboxrd files for each thread.
 .It Pa attachment/*/*/*.*
@@ -174,11 +172,33 @@ Cached Atom, HTML and mboxrd fragments for each message.
 .It Pa UIDNEXT
 Stores the next UID of the mailbox.
 Remove this file to force re-render
-the index page and feed.
+the search pages and feeds.
 .It Pa UIDVALIDITY
 Stores the mailbox UID validity.
+.It Pa SEARCH
+The default path to read search definitions from.
 .El
 .
+.Pp
+Each line of the
+.Pa SEARCH
+file defines a search
+for which an Atom feed
+and an HTML page will be generated.
+Blank lines and lines beginning with
+.Ql #
+are ignored.
+Each line consists of a search name
+and an IMAP search expression,
+separated by whitespace.
+If no
+.Pa index
+search is defined,
+the following default is used:
+.Bd -literal -offset indent
+index	ALL
+.Ed
+.
 .Sh EXAMPLES
 .Bd -literal
 bubger -C archive list@example.org |
diff --git a/concat.c b/concat.c
index 82f2c84..2b1c619 100644
--- a/concat.c
+++ b/concat.c
@@ -294,15 +294,13 @@ void concatSearch(
 	if (!order) err(EX_OSERR, "calloc");
 
 	for (size_t i = 0; i < threads.len; ++i) {
+		order[i].index = i;
+		order[i].created = envelopes[i].time;
+
 		struct stat status;
 		char *path = threadPath(envelopes[i].messageID, "html");
-		int error = stat(path, &status);
-		if (error) err(EX_DATAERR, "%s", path);
+		if (!stat(path, &status)) order[i].updated = status.st_mtime;
 		free(path);
-
-		order[i].index = i;
-		order[i].created = envelopes[i].time;
-		order[i].updated = status.st_mtime;
 	}
 	qsort(order, threads.len, sizeof(*order), sortCompare);
 
@@ -322,6 +320,7 @@ void concatSearch(
 	if (error) err(EX_IOERR, "%s", path);
 
 	for (size_t i = threads.len - 1; i < threads.len; --i) {
+		if (!order[i].updated) continue;
 		const struct Envelope *envelope = &envelopes[order[i].index];
 		struct List thread = dataCheck(threads.ptr[order[i].index], List).list;
 		error = htmlSearchThread(file, envelope, thread);