From f1f9c20bd8d9d175f8db73b062a60a9f79e95a7e Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Fri, 11 Jun 2021 17:32:18 -0400 Subject: 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. --- archive.c | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++------- bubger.1 | 50 ++++++++++++++++------- concat.c | 11 +++--- 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 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); -- cgit 1.4.1