* test/runtest, test/tests/inc/test-deb359167,
authorEric Gillespie <epg@pretzelnet.org>
Sat, 17 Jan 2009 16:47:29 +0000 (16:47 +0000)
committerEric Gillespie <epg@pretzelnet.org>
Sat, 17 Jan 2009 16:47:29 +0000 (16:47 +0000)
test/tests/inc/test-eom-align, test/tests/manpages/test-manpages:
Load common.sh via absolute path, otherwise some /bin/sh
(e.g. dash) can't load it.

* uip/Makefile.in, uip/new.c, test/tests/new/test-basic: Add new
program, and fn/fp/unseen symlinks.

* test/{runtest,setup-test}: Move MH profile under Mail directory
so each test script will have its own to muck with, if needed.

* h/Makefile.in, h/crawl_folders.h, sbr/Makefile.in,
sbr/crawl_folders.c, uip/folder.c: Extract the folder crawling
code from folder.c into new crawl_folders function, using a
callback to assemble the folder info in folder.c.  Drop compare
function and use strcmp instead.  Rename addfold and addir to
add_folder and add_children (add dir vs. add folder?
confusing names).

20 files changed:
ChangeLog
h/Makefile.in
h/crawl_folders.h [new file with mode: 0644]
man/.cvsignore
man/Makefile.in
man/fnext.man [new file with mode: 0644]
man/fprev.man [new file with mode: 0644]
man/new.man [new file with mode: 0644]
man/unseen.man [new file with mode: 0644]
sbr/Makefile.in
sbr/crawl_folders.c [new file with mode: 0644]
test/runtest
test/setup-test
test/tests/inc/test-deb359167
test/tests/inc/test-eom-align
test/tests/manpages/test-manpages
test/tests/new/test-basic [new file with mode: 0644]
uip/Makefile.in
uip/folder.c
uip/new.c [new file with mode: 0644]

index 5817d9a..b2ea1aa 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,24 @@
+2009-01-16  Eric Gillespie  <epg@pretzelnet.org>
+
+       * test/runtest, test/tests/inc/test-deb359167,
+       test/tests/inc/test-eom-align, test/tests/manpages/test-manpages:
+       Load common.sh via absolute path, otherwise some /bin/sh
+       (e.g. dash) can't load it.
+
+       * uip/Makefile.in, uip/new.c, test/tests/new/test-basic: Add new
+       program, and fn/fp/unseen symlinks.
+
+       * test/{runtest,setup-test}: Move MH profile under Mail directory
+       so each test script will have its own to muck with, if needed.
+
+       * h/Makefile.in, h/crawl_folders.h, sbr/Makefile.in,
+       sbr/crawl_folders.c, uip/folder.c: Extract the folder crawling
+       code from folder.c into new crawl_folders function, using a
+       callback to assemble the folder info in folder.c.  Drop compare
+       function and use strcmp instead.  Rename addfold and addir to
+       add_folder and add_children (add dir vs. add folder?
+       confusing names).
+
 2008-12-26  Peter Maydell  <pmaydell@chiark.greenend.org.uk>
 
        * sbr/m_getfld.c: fix two bugs which could cause us to walk off
index 492d364..7b4a276 100644 (file)
@@ -10,7 +10,7 @@ srcdir = @srcdir@
 VPATH  = @srcdir@
 
 # header files included in distribution
-HDRS =         addrsbr.h aliasbr.h dropsbr.h fmt_compile.h fmt_scan.h          \
+HDRS =         addrsbr.h aliasbr.h crawl_folders.h dropsbr.h fmt_compile.h fmt_scan.h \
        md5.h mf.h mh.h mhcachesbr.h mhparse.h mime.h msh.h mts.h       \
        netdb.h nmh.h nntp.h picksbr.h popsbr.h prototypes.h rcvmail.h  \
        scansbr.h signals.h tws.h vmhsbr.h utils.h
diff --git a/h/crawl_folders.h b/h/crawl_folders.h
new file mode 100644 (file)
index 0000000..d04287a
--- /dev/null
@@ -0,0 +1,18 @@
+
+/*
+ * crawl_folders.h -- crawl folder hierarchy
+ *
+ * $Id$
+ */
+
+#define CRAWL_NUMFOLDERS 100
+
+/* Callbacks return TRUE crawl_folders should crawl the children of `folder'.
+ * Callbacks need not duplicate folder, as crawl_folders does not free it. */
+typedef boolean (crawl_callback_t)(char *folder, void *baton);
+
+/* Crawl the folder hierarchy rooted at the relative path `dir'.  For each
+ * folder, pass `callback' the folder name (as a path relative to the current
+ * directory) and `baton'; the callback may direct crawl_folders not to crawl
+ * its children; see above. */
+void crawl_folders (char *dir, crawl_callback_t *callback, void *baton);
index 1cd0b60..cacddcd 100644 (file)
@@ -10,9 +10,11 @@ dp.8
 flist.1
 flists.1
 fmtdump.8
+fnext.1
 folder.1
 folders.1
 forw.1
+fprev.1
 inc.1
 install-mh.1
 man.sed
@@ -37,6 +39,7 @@ mhstore.1
 msgchk.1
 msh.1
 mts.conf.5
+new.1
 next.1
 nmh.1
 packf.1
@@ -58,5 +61,6 @@ sendfiles.1
 show.1
 slocal.1
 sortm.1
+unseen.1
 whatnow.1
 whom.1
index fe70265..ab93318 100644 (file)
@@ -57,6 +57,7 @@ MAN1SRC = ali. anno. burst. comp.             \
        nmh. mhbuild. mhl. mhlist. mhmail.      \
        mhn. mhparam. mhpath. mhshow.           \
        mhstore. msgchk. msh.                   \
+       new. fnext. fprev. unseen.              \
        next. packf. pick. prev.                        \
        prompter. rcvdist. rcvpack.             \
        rcvstore. rcvtty. refile.               \
diff --git a/man/fnext.man b/man/fnext.man
new file mode 100644 (file)
index 0000000..7529017
--- /dev/null
@@ -0,0 +1 @@
+.so man1/new.1
diff --git a/man/fprev.man b/man/fprev.man
new file mode 100644 (file)
index 0000000..7529017
--- /dev/null
@@ -0,0 +1 @@
+.so man1/new.1
diff --git a/man/new.man b/man/new.man
new file mode 100644 (file)
index 0000000..c724d3e
--- /dev/null
@@ -0,0 +1,110 @@
+.\"
+.\" %nmhwarning%
+.\" $Id$
+.\"
+.TH NEW %manext1% "%nmhdate%" MH.6.8 [%nmhversion%]
+
+.SH NAME
+new \- report on folders with new messages
+.PP
+fnext \- set current folder to next folder with new messages
+.PP
+fprev \- set current folder to previous folder with new messages
+.PP
+unseen \- scan new messages in all folders with new messages
+
+.SH SYNOPSIS
+.HP 5
+.na
+.B new
+.RI [ sequences ]
+.RB [ \-mode
+.IR mode ]
+.RB [ \-folders
+.IR foldersfile ]
+.RB [ \-version ]
+.RB [ \-help ]
+.PP
+.HP 5
+.B fnext
+is equivalent to
+.B new \-mode fnext
+.PP
+.HP 5
+.B fprev
+is equivalent to
+.B new \-mode fprev
+.PP
+.HP 5
+.B unseen
+is equivalent to
+.B new \-mode unseen
+.ad
+
+.SH DESCRIPTION
+.B New
+in its default mode produces a one\-line\-per\-folder listing of all
+folders containing messages in the listed
+.IR sequences
+or in the sequences listed in the profile entry
+.RI \*(lq Unseen-Sequence \*(rq.
+Each line contains the folder, the number of messages in the desired
+sequences, and the message lists from the .mh_sequences file.  For example:
+.PP
+.RS 5
+.nf
+foo     11.* 40\-50
+bar    380.  760\-772 824\-828
+ total    391.
+.fi
+.RE
+.PP
+The `*' on foo indicates that it is the current folder.  The last line shows
+the total number of messages in the desired sequences.
+.PP
+.B New
+crawls the folder hierarchy recursively to find all folders, and prints them
+in lexicographic order.  Override this behavior by providing
+.IR foldersfile
+containing the pre-sorted list of folders
+.B new
+should check, one per line.
+.PP
+In
+.B fnext
+and
+.B fprev
+modes,
+.B new
+instead changes to the next or previous matching folder, respectively.
+.PP
+In
+.B unseen
+mode,
+.B new
+executes
+.B scan sequences
+for each matching folder.
+
+.SH FILES
+.fc ^ ~
+.nf
+.ta \w'%etcdir%/ExtraBigFileName  'u
+^$HOME/\&.mh\(ruprofile~^The user profile
+.fi
+
+.SH "PROFILE COMPONENTS"
+.fc ^ ~
+.nf
+.ta 2.4i
+.ta \w'ExtraBigProfileName  'u
+^Path:~^To determine the user's nmh directory
+^Current\-Folder:~^To find the default current folder
+^Unseen-Sequence:~^The name of the unseen message sequence
+.fi
+
+.SH "SEE ALSO"
+scan(1), mh\-format(5)
+
+.SH HISTORY
+Based on Luke Mewburn's new (http://www.mewburn.net/luke/src/new).
diff --git a/man/unseen.man b/man/unseen.man
new file mode 100644 (file)
index 0000000..7529017
--- /dev/null
@@ -0,0 +1 @@
+.so man1/new.1
index a920958..b8787d1 100644 (file)
@@ -58,7 +58,8 @@ SRCS = addrsbr.c ambigsw.c atooi.c brkstring.c                        \
        check_charset.c client.c closefds.c concat.c context_del.c      \
        context_find.c context_foil.c context_read.c                    \
        context_replace.c context_save.c copy.c                         \
-       copyip.c cpydata.c cpydgst.c discard.c done.c dtime.c dtimep.c  \
+       copyip.c cpydata.c cpydgst.c crawl_folders.c                    \
+       discard.c done.c dtime.c dtimep.c                               \
        error.c ext_hook.c fdcompare.c folder_addmsg.c folder_delmsgs.c \
        folder_free.c folder_pack.c folder_read.c                       \
        folder_realloc.c gans.c getans.c getanswer.c                    \
diff --git a/sbr/crawl_folders.c b/sbr/crawl_folders.c
new file mode 100644 (file)
index 0000000..4889bf1
--- /dev/null
@@ -0,0 +1,151 @@
+
+/*
+ * crawl_folders.c -- crawl folder hierarchy
+ *
+ * $Id$
+ *
+ * This code is Copyright (c) 2008, by the authors of nmh.  See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+#include <h/mh.h>
+#include <h/crawl_folders.h>
+#include <h/utils.h>
+
+struct crawl_context {
+    int max;                   /* how many folders we currently can hold in
+                                * the array `folders', increased by
+                                * CRAWL_NUMFOLDERS at a time */
+    int total;                 /* how many `folders' actually has */
+    char **folders;            /* the array of folders */
+    int start;
+    int foldp;
+};
+
+/*
+ * Add the folder name into the
+ * list in a sorted fashion.
+ */
+
+static void
+add_folder (char *fold, struct crawl_context *crawl)
+{
+    register int i, j;
+
+    /* if necessary, reallocate the space for folder names */
+    if (crawl->foldp >= crawl->max) {
+       crawl->max += CRAWL_NUMFOLDERS;
+       crawl->folders = mh_xrealloc (crawl->folders,
+                                     crawl->max * sizeof(char *));
+    }
+
+    for (i = crawl->start; i < crawl->foldp; i++)
+       if (strcmp (fold, crawl->folders[i]) < 0) {
+           for (j = crawl->foldp - 1; j >= i; j--)
+               crawl->folders[j + 1] = crawl->folders[j];
+           crawl->foldp++;
+           crawl->folders[i] = fold;
+           return;
+       }
+
+    crawl->total++;
+    crawl->folders[crawl->foldp++] = fold;
+}
+
+static void
+add_children (char *name, struct crawl_context *crawl)
+{
+    char *prefix, *child;
+    struct stat st;
+    struct dirent *dp;
+    DIR * dd;
+    int child_is_folder;
+
+    if (!(dd = opendir (name))) {
+       admonish (name, "unable to read directory ");
+       return;
+    }
+
+    if (strcmp (name, ".") == 0) {
+       prefix = getcpy ("");
+    } else {
+       prefix = concat (name, "/", (void *)NULL);
+    }
+
+    while ((dp = readdir (dd))) {
+       /* If the system supports it, try to skip processing of children we
+        * know are not directories or symlinks. */
+       child_is_folder = -1;
+#if defined(HAVE_STRUCT_DIRENT_D_TYPE)
+       if (dp->d_type == DT_DIR) {
+           child_is_folder = 1;
+       } else if (dp->d_type != DT_LNK && dp->d_type != DT_UNKNOWN) {
+           continue;
+       }
+#endif
+       if (!strcmp (dp->d_name, ".") || !strcmp (dp->d_name, "..")) {
+           continue;
+       }
+       child = concat (prefix, dp->d_name, (void *)NULL);
+       /* If we have no d_type or d_type is DT_LNK or DT_UNKNOWN, stat the
+        * child to see what it is. */
+       if (child_is_folder == -1) {
+           child_is_folder = (stat (child, &st) != -1 && S_ISDIR(st.st_mode));
+       }
+       if (child_is_folder) {
+           /* add_folder saves child in the list, don't free it */
+           add_folder (child, crawl);
+       } else {
+           free (child);
+       }
+    }
+
+    closedir (dd);
+    free(prefix);
+}
+
+static void
+crawl_folders_body (struct crawl_context *crawl,
+                   char *dir, crawl_callback_t *callback, void *baton)
+{
+    int i;
+    int os = crawl->start;
+    int of = crawl->foldp;
+
+    crawl->start = crawl->foldp;
+
+    add_children (dir, crawl);
+
+    for (i = crawl->start; i < crawl->foldp; i++) {
+       char *fold = crawl->folders[i];
+       int crawl_children = 1;
+
+       if (callback != NULL) {
+           crawl_children = callback (fold, baton);
+       }
+
+       if (crawl_children) {
+           crawl_folders_body (crawl, fold, callback, baton);
+       }
+    }
+
+    crawl->start = os;
+    crawl->foldp = of;
+}
+
+void
+crawl_folders (char *dir, crawl_callback_t *callback, void *baton)
+{
+    struct crawl_context *crawl = mh_xmalloc (sizeof(*crawl));
+    crawl->max = CRAWL_NUMFOLDERS;
+    crawl->total = crawl->start = crawl->foldp = 0;
+    crawl->folders = mh_xmalloc (crawl->max * sizeof(*crawl->folders));
+
+    crawl_folders_body (crawl, dir, callback, baton);
+
+    /* Note that we "leak" the folder names, on the assumption that the caller
+     * is using them. */
+    free (crawl->folders);
+    free (crawl);
+}
index 4ec4a03..0b18653 100755 (executable)
@@ -13,15 +13,18 @@ if [ ! -e "$MH_TEST_DIR/bld/Makefile" ]; then
    echo "temporary directory missing or broken: running setup-test"
    ./setup-test
    export MH_TEST_DIR=`cat test-temp-dir`
-fi   
+fi
 
-export MH=$MH_TEST_DIR/mh_profile
+export MH=$MH_TEST_DIR/Mail/.mh_profile
 export PATH=$MH_TEST_DIR/bin:$PATH
 
+export MH_TEST_COMMON=$PWD/common.sh
+
 # clean old test data
 rm -rf $MH_TEST_DIR/Mail
 # setup test data
 mkdir $MH_TEST_DIR/Mail
+echo "Path: $MH_TEST_DIR/Mail" > $MH
 folder -create +inbox > /dev/null
 # create 10 basic messages
 for i in `seq 1 10`;
index a99c6c7..fab26a0 100755 (executable)
@@ -20,5 +20,3 @@ mkdir $TEMPDIR/bld
 cd $TEMPDIR/bld
 $srcdir/configure --prefix=$TEMPDIR --with-locking=fcntl --enable-debug
 make install
-
-echo "Path: $TEMPDIR/Mail" > $TEMPDIR/mh_profile
index 61d3283..36cd1c2 100644 (file)
@@ -2,7 +2,7 @@
 # Test a variant of a mailbox which caused debian bug 359167.
 set -e
 
-. common.sh
+. $MH_TEST_COMMON
 
 THISDIR="tests/inc"
 
index 5d197ff..3b6c677 100644 (file)
@@ -4,7 +4,7 @@
 
 set -e
 
-. common.sh
+. $MH_TEST_COMMON
 
 THISDIR="tests/inc"
 
index 2ad77d8..f7acc69 100644 (file)
@@ -6,7 +6,7 @@
 #
 ######################################################
 
-. common.sh
+. $MH_TEST_COMMON
 
 require_prog groff
 
diff --git a/test/tests/new/test-basic b/test/tests/new/test-basic
new file mode 100644 (file)
index 0000000..772b72f
--- /dev/null
@@ -0,0 +1,164 @@
+#!/bin/sh
+
+# TODO: Move to a common file tests can source; need more framework...
+failed=0
+check() {
+    diff -u $expected $actual
+    if [ $? -ne 0 ]; then
+        failed=$((failed + 1))
+    fi
+}
+
+folders=$MH_TEST_DIR/Mail/.folders
+
+expected=$MH_TEST_DIR/$$.expected
+actual=$MH_TEST_DIR/$$.actual
+
+# make second folder
+cp -r $MH_TEST_DIR/Mail/inbox $MH_TEST_DIR/Mail/foo1
+cp -r $MH_TEST_DIR/Mail/inbox $MH_TEST_DIR/Mail/foo2
+# but only list inbox and foo2 in .folders, and sorted differently
+cat > $folders <<EOF
+inbox
+foo2
+EOF
+
+# test with no sequence
+cat > $expected <<EOF
+new: must specify sequences or set Unseen-Sequence
+EOF
+new > $actual 2>&1
+check
+
+# test with no desired messages
+cat > $expected <<EOF
+ total      0.
+EOF
+new aseq > $actual 2>&1
+check
+new -folders $folders aseq > $actual 2>&1
+check
+
+# test fnext/fprev with no desired messages
+cat /dev/null > $expected
+fnext aseq > $actual 2>&1
+check
+fprev aseq > $actual 2>&1
+check
+
+# add 1 desired message in each folder
+echo 'aseq: 1' > $MH_TEST_DIR/Mail/inbox/.mh_sequences
+echo 'aseq: 1' > $MH_TEST_DIR/Mail/foo1/.mh_sequences
+echo 'aseq: 1' > $MH_TEST_DIR/Mail/foo2/.mh_sequences
+
+# test with all folders
+cat > $expected <<EOF
+foo1       1.  1
+foo2       1.  1
+inbox      1.* 1
+ total      3.
+EOF
+new aseq > $actual 2>&1
+check
+
+# test with .folders
+cat > $expected <<EOF
+inbox      1.* 1
+foo2       1.  1
+ total      2.
+EOF
+new -folders $folders aseq > $actual 2>&1
+check
+
+# add 2 desired messages to another sequence in each folder
+echo 'bseq: 3-4' >> $MH_TEST_DIR/Mail/inbox/.mh_sequences
+echo 'bseq: 3-4' >> $MH_TEST_DIR/Mail/foo1/.mh_sequences
+echo 'bseq: 3-4' >> $MH_TEST_DIR/Mail/foo2/.mh_sequences
+
+# test listing aseq and bseq
+cat > $expected <<EOF
+foo1       3.  1 3-4
+foo2       3.  1 3-4
+inbox      3.* 1 3-4
+ total      9.
+EOF
+new aseq bseq > $actual 2>&1
+check
+
+# set aseq bseq as unseen
+echo 'Unseen-Sequence: aseq bseq' >> $MH
+new > $actual 2>&1
+check
+
+# test unseen
+cat > $expected <<EOF
+
+3 aseq bseq messages in foo1
+   1  09/29 Test1              Testing message 1<<This is message number 1 >>
+   3  09/29 Test3              Testing message 3<<This is message number 3 >>
+   4  09/29 Test4              Testing message 4<<This is message number 4 >>
+
+3 aseq bseq messages in foo2
+   1  09/29 Test1              Testing message 1<<This is message number 1 >>
+   3  09/29 Test3              Testing message 3<<This is message number 3 >>
+   4  09/29 Test4              Testing message 4<<This is message number 4 >>
+
+3 aseq bseq messages in inbox (*: current folder)
+   1  09/29 Test1              Testing message 1<<This is message number 1 >>
+   3  09/29 Test3              Testing message 3<<This is message number 3 >>
+   4  09/29 Test4              Testing message 4<<This is message number 4 >>
+EOF
+unseen > $actual 2>&1
+check
+
+# test fnext with the current folder not in the list
+echo 'Current-Folder: foo1' > $MH_TEST_DIR/Mail/context
+echo 'inbox  1 3-4' > $expected
+fnext -folders $folders > $actual 2>&1
+check
+
+# test fprev with the current folder not in the list
+echo 'Current-Folder: foo1' > $MH_TEST_DIR/Mail/context
+echo 'inbox  1 3-4' > $expected
+fprev -folders $folders > $actual 2>&1
+check
+
+# test fnext with current folder in the middle of the list
+echo 'Current-Folder: foo2' > $MH_TEST_DIR/Mail/context
+echo 'inbox  1 3-4' > $expected
+fnext > $actual 2>&1
+check
+
+# test fprev with current folder in the middle of the list
+echo 'Current-Folder: foo2' > $MH_TEST_DIR/Mail/context
+echo 'foo1  1 3-4' > $expected
+fprev > $actual 2>&1
+check
+
+# test fprev with current folder at the beginning of the list
+echo 'Current-Folder: foo1' > $MH_TEST_DIR/Mail/context
+echo 'inbox  1 3-4' > $expected
+fprev > $actual 2>&1
+check
+
+# test fnext with current folder at the end of the list
+echo 'Current-Folder: inbox' > $MH_TEST_DIR/Mail/context
+echo 'foo1  1 3-4' > $expected
+fnext > $actual 2>&1
+check
+
+# test fnext with no current folder
+rm $MH_TEST_DIR/Mail/context
+echo 'foo1  1 3-4' > $expected
+fnext > $actual 2>&1
+check
+
+# test fnext with only one folder in the list
+cat > $folders <<EOF
+inbox
+EOF
+echo 'inbox  1 3-4' > $expected
+fnext -folders $folders > $actual 2>&1
+check
+
+exit $failed
index f101a28..1e1ae9b 100644 (file)
@@ -60,7 +60,7 @@ SETGID_MAIL    = @SETGID_MAIL@
 # commands to build
 CMDS = ali anno burst comp dist flist folder forw install-mh mark mhbuild \
        mhlist mhmail mhn mhparam mhpath mhshow mhstore msgchk \
-       msh packf pick prompter refile repl rmf rmm scan send show \
+       msh new packf pick prompter refile repl rmf rmm scan send show \
        sortm whatnow whom
 
 ## removed this from CMDS until I can fix it
@@ -83,7 +83,7 @@ SRCS = ali.c aliasbr.c anno.c annosbr.c ap.c burst.c comp.c             \
        mhbuildsbr.c mhcachesbr.c mhfree.c mhl.c mhlist.c mhlistsbr.c     \
        mhlsbr.c mhmail.c mhmisc.c mhn.c mhoutsbr.c mhparam.c mhparse.c   \
        mhpath.c mhshow.c mhshowsbr.c mhstore.c mhstoresbr.c mhtest.c     \
-       msgchk.c msh.c mshcmds.c packf.c pick.c picksbr.c popsbr.c \
+       msgchk.c msh.c mshcmds.c new.c packf.c pick.c picksbr.c popsbr.c \
        post.c prompter.c rcvdist.c rcvpack.c rcvstore.c rcvtty.c         \
        refile.c repl.c replsbr.c rmf.c rmm.c scan.c scansbr.c send.c     \
        sendsbr.c show.c slocal.c sortm.c spost.c termsbr.c viamail.c     \
@@ -183,6 +183,9 @@ msgchk: msgchk.o $(POPLIB) $(LOCALLIBS)
 msh: msh.o mshcmds.o vmhsbr.o picksbr.o scansbr.o dropsbr.o mhlsbr.o termsbr.o $(LOCALLIBS)
        $(LINK) msh.o mshcmds.o vmhsbr.o picksbr.o scansbr.o dropsbr.o mhlsbr.o termsbr.o $(LINKLIBS) $(TERMLIB)
 
+new: new.o $(LOCALLIBS)
+       $(LINK) new.o $(LINKLIBS)
+
 packf: packf.o dropsbr.o $(LOCALLIBS)
        $(LINK) packf.o dropsbr.o $(LINKLIBS)
 
@@ -265,11 +268,17 @@ install-cmds:
 install-lcmds:
        rm -f $(DESTDIR)$(bindir)/flists
        rm -f $(DESTDIR)$(bindir)/folders
+       rm -f $(DESTDIR)$(bindir)/fnext
+       rm -f $(DESTDIR)$(bindir)/fprev
+       rm -f $(DESTDIR)$(bindir)/unseen
        rm -f $(DESTDIR)$(bindir)/prev
        rm -f $(DESTDIR)$(bindir)/next
        rm -f $(DESTDIR)$(libdir)/install-mh
        $(LN) $(DESTDIR)$(bindir)/flist  $(DESTDIR)$(bindir)/flists
        $(LN) $(DESTDIR)$(bindir)/folder $(DESTDIR)$(bindir)/folders
+       $(LN) $(DESTDIR)$(bindir)/new    $(DESTDIR)$(bindir)/fnext
+       $(LN) $(DESTDIR)$(bindir)/new    $(DESTDIR)$(bindir)/fprev
+       $(LN) $(DESTDIR)$(bindir)/new    $(DESTDIR)$(bindir)/unseen
        $(LN) $(DESTDIR)$(bindir)/show   $(DESTDIR)$(bindir)/prev
        $(LN) $(DESTDIR)$(bindir)/show   $(DESTDIR)$(bindir)/next
 
index a94f131..f83bedc 100644 (file)
@@ -6,12 +6,13 @@
  *
  * $Id$
  *
- * This code is Copyright (c) 2002, by the authors of nmh.  See the
+ * This code is Copyright (c) 2002, 2008, by the authors of nmh.  See the
  * COPYRIGHT file in the root directory of the nmh distribution for
  * complete copyright information.
  */
 
 #include <h/mh.h>
+#include <h/crawl_folders.h>
 #include <h/utils.h>
 #include <errno.h>
 
@@ -78,22 +79,10 @@ static int all      = 0;    /* should we output all folders             */
 
 static int total_folders = 0;  /* total number of folders                  */
 
-static int start = 0;
-static int foldp = 0;
-
 static char *nmhdir;
 static char *stack = "Folder-Stack";
 static char folder[BUFSIZ];
 
-#define NUMFOLDERS 100
-
-/*
- * This is how many folders we currently can hold in the array
- * `folds'.  We increase this amount by NUMFOLDERS at a time.
- */
-static int maxfolders;
-static char **folds;
-
 /*
  * Structure to hold information about
  * folders as we scan them.
@@ -118,13 +107,10 @@ static int maxFolderInfo;
 /*
  * static prototypes
  */
-static void dodir (char *);
 static int get_folder_info (char *, char *);
+static crawl_callback_t get_folder_info_callback;
 static void print_folders (void);
 static int sfold (struct msgs *, char *);
-static void addir (char *);
-static void addfold (char *);
-static int compare (char *, char *);
 static void readonly_folders (void);
 
 
@@ -350,12 +336,8 @@ main (int argc, char **argv)
            done (0);
     }
 
-    /* Allocate initial space to record folder names */
-    maxfolders = NUMFOLDERS;
-    folds = mh_xmalloc (maxfolders * sizeof(char *));
-
     /* Allocate initial space to record folder information */
-    maxFolderInfo = NUMFOLDERS;
+    maxFolderInfo = CRAWL_NUMFOLDERS;
     fi = mh_xmalloc (maxFolderInfo * sizeof(*fi));
 
     /*
@@ -365,7 +347,7 @@ main (int argc, char **argv)
        /*
         * If no folder is given, do them all
         */
-       /* change directory to base of nmh directory for dodir */
+       /* change directory to base of nmh directory for crawl_folders */
        if (chdir (nmhdir) == NOTOK)
            adios (nmhdir, "unable to change directory to");
        if (!argfolder) {
@@ -373,7 +355,7 @@ main (int argc, char **argv)
                admonish (NULL, "no folder given for message %s", msg);
            readonly_folders (); /* do any readonly folders */
            strncpy (folder, (cp = context_find (pfolder)) ? cp : "", sizeof(folder));
-           dodir (".");
+           crawl_folders (".", get_folder_info_callback, NULL);
        } else {
            strncpy (folder, argfolder, sizeof(folder));
            if (get_folder_info (argfolder, msg)) {
@@ -385,7 +367,7 @@ main (int argc, char **argv)
             * we still need to list all level-1 sub-folders.
             */
            if (!frecurse)
-               dodir (folder);
+               crawl_folders (folder, get_folder_info_callback, NULL);
        }
     } else {
        strncpy (folder, argfolder ? argfolder : getfolder (1), sizeof(folder));
@@ -412,32 +394,8 @@ main (int argc, char **argv)
     return 1;
 }
 
-/*
- * Base routine for scanning a folder
- */
-
-static void
-dodir (char *dir)
-{
-    int i;
-    int os = start;
-    int of = foldp;
-
-    start = foldp;
-
-    addir (dir);
-
-    for (i = start; i < foldp; i++) {
-       get_folder_info (folds[i], NULL);
-       fflush (stdout);
-    }
-
-    start = os;
-    foldp = of;
-}
-
 static int
-get_folder_info (char *fold, char *msg)
+get_folder_info_body (char *fold, char *msg, boolean *crawl_children)
 {
     int        i, retval = 1;
     struct msgs *mp = NULL;
@@ -449,7 +407,7 @@ get_folder_info (char *fold, char *msg)
      * for folder information
      */
     if (total_folders >= maxFolderInfo) {
-       maxFolderInfo += NUMFOLDERS;
+       maxFolderInfo += CRAWL_NUMFOLDERS;
        fi = mh_xrealloc (fi, maxFolderInfo * sizeof(*fi));
     }
 
@@ -493,8 +451,32 @@ get_folder_info (char *fold, char *msg)
        folder_free (mp); /* free folder/message structure */
     }
 
-    if (frecurse && (fshort || fi[i].others) && (fi[i].error == 0))
-       dodir (fold);
+    *crawl_children = (frecurse && (fshort || fi[i].others)
+                      && (fi[i].error == 0));
+    return retval;
+}
+
+static boolean
+get_folder_info_callback (char *fold, void *baton)
+{
+    boolean crawl_children;
+    get_folder_info_body (fold, NULL, &crawl_children);
+    fflush (stdout);
+    return crawl_children;
+}
+
+static int
+get_folder_info (char *fold, char *msg)
+{
+    boolean crawl_children;
+    int retval;
+
+    retval = get_folder_info_body (fold, msg, &crawl_children);
+
+    if (crawl_children) {
+       crawl_folders (fold, get_folder_info_callback, NULL);
+    }
+
     return retval;
 }
 
@@ -654,99 +636,6 @@ sfold (struct msgs *mp, char *msg)
 }
 
 
-static void
-addir (char *name)
-{
-    char *prefix, *child;
-    struct stat st;
-    struct dirent *dp;
-    DIR * dd;
-    int child_is_folder;
-
-    if (!(dd = opendir (name))) {
-       admonish (name, "unable to read directory ");
-       return;
-    }
-
-    if (strcmp (name, ".") == 0) {
-       prefix = getcpy ("");
-    } else {
-       prefix = concat (name, "/", (void *)NULL);
-    }
-
-    while ((dp = readdir (dd))) {
-       /* If the system supports it, try to skip processing of children we
-        * know are not directories or symlinks. */
-       child_is_folder = -1;
-#if defined(HAVE_STRUCT_DIRENT_D_TYPE)
-       if (dp->d_type == DT_DIR) {
-           child_is_folder = 1;
-       } else if (dp->d_type != DT_LNK && dp->d_type != DT_UNKNOWN) {
-           continue;
-       }
-#endif
-       if (!strcmp (dp->d_name, ".") || !strcmp (dp->d_name, "..")) {
-           continue;
-       }
-       child = concat (prefix, dp->d_name, (void *)NULL);
-       /* If we have no d_type or d_type is DT_LNK or DT_UNKNOWN, stat the
-        * child to see what it is. */
-       if (child_is_folder == -1) {
-           child_is_folder = (stat (child, &st) != -1 && S_ISDIR(st.st_mode));
-       }
-       if (child_is_folder) {
-           /* addfold saves child in the list, don't free it */
-           addfold (child);
-       } else {
-           free (child);
-       }
-    }
-
-    closedir (dd);
-    free(prefix);
-}
-
-/*
- * Add the folder name into the
- * list in a sorted fashion.
- */
-
-static void
-addfold (char *fold)
-{
-    register int i, j;
-
-    /* if necessary, reallocate the space for folder names */
-    if (foldp >= maxfolders) {
-       maxfolders += NUMFOLDERS;
-       folds = mh_xrealloc (folds, maxfolders * sizeof(char *));
-    }
-
-    for (i = start; i < foldp; i++)
-       if (compare (fold, folds[i]) < 0) {
-           for (j = foldp - 1; j >= i; j--)
-               folds[j + 1] = folds[j];
-           foldp++;
-           folds[i] = fold;
-           return;
-       }
-
-    folds[foldp++] = fold;
-}
-
-
-static int
-compare (char *s1, char *s2)
-{
-    register int i;
-
-    while (*s1 || *s2)
-       if ((i = *s1++ - *s2++))
-           return i;
-
-    return 0;
-}
-
 /*
  * Do the read only folders
  */
diff --git a/uip/new.c b/uip/new.c
new file mode 100644 (file)
index 0000000..4ff8c04
--- /dev/null
+++ b/uip/new.c
@@ -0,0 +1,511 @@
+
+/*
+ * new.c -- as new,    list all folders with unseen messages
+ *       -- as fnext,  move to next folder with unseen messages
+ *       -- as fprev,  move to previous folder with unseen messages
+ *       -- as unseen, scan all unseen messages
+ * $Id$
+ *
+ * This code is Copyright (c) 2008, by the authors of nmh.  See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ *
+ * Inspired by Luke Mewburn's new: http://www.mewburn.net/luke/src/new
+ */
+
+#include <sys/types.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <h/mh.h>
+#include <h/crawl_folders.h>
+#include <h/utils.h>
+
+static struct swit switches[] = {
+#define MODESW 0
+    { "mode", 1 },
+#define FOLDERSSW 1
+    { "folders", 1 },
+#define VERSIONSW 2
+    { "version", 1 },
+#define HELPSW 3
+    { "help", 1 },
+    { NULL, 0 }
+};
+
+static enum { NEW, FNEXT, FPREV, UNSEEN } run_mode = NEW;
+
+/* check_folders uses this to maintain state with both .folders list of
+ * folders and with crawl_folders. */
+struct list_state {
+    struct node **first, **cur_node;
+    size_t *maxlen;
+    char *cur;
+    char **sequences;
+    struct node *node;
+};
+
+/* Return the number of messages in a string list of message numbers. */
+static int
+count_messages(char *field)
+{
+    int total = 0;
+    int j, k;
+    char *cp, **ap;
+
+    field = getcpy(field);
+
+    /* copied from seq_read.c:seq_init */
+    for (ap = brkstring (field, " ", "\n"); *ap; ap++) {
+        if ((cp = strchr(*ap, '-')))
+            *cp++ = '\0';
+        if ((j = m_atoi (*ap)) > 0) {
+            k = cp ? m_atoi (cp) : j;
+
+            total += k - j + 1;
+        }
+    }
+
+    free(field);
+
+    return total;
+}
+
+/* Return TRUE if the sequence 'name' is in 'sequences'. */
+static boolean
+seq_in_list(char *name, char *sequences[])
+{
+    int i;
+
+    for (i = 0; sequences[i] != NULL; i++) {
+       if (strcmp(name, sequences[i]) == 0) {
+           return TRUE;
+       }
+    }
+
+    return FALSE;
+}
+
+/* Return the string list of message numbers from the sequences file, or NULL
+ * if none. */
+static char *
+get_msgnums(char *folder, char *sequences[])
+{
+    char *seqfile = concat(m_maildir(folder), "/", mh_seq, (void *)NULL);
+    FILE *fp = fopen(seqfile, "r");
+    int state;
+    char name[NAMESZ], field[BUFSIZ];
+    char *cp;
+    char *msgnums = NULL, *this_msgnums, *old_msgnums;
+
+    /* no sequences file -> no messages */
+    if (fp == NULL) {
+        return NULL;
+    }
+
+    /* copied from seq_read.c:seq_public */
+    for (state = FLD;;) {
+        switch (state = m_getfld (state, name, field, sizeof(field), fp)) {
+            case FLD:
+            case FLDPLUS:
+            case FLDEOF:
+                if (state == FLDPLUS) {
+                    cp = getcpy (field);
+                    while (state == FLDPLUS) {
+                        state = m_getfld (state, name, field,
+                                          sizeof(field), fp);
+                        cp = add (field, cp);
+                    }
+
+                    /* Here's where we differ from seq_public: if it's in a
+                    * sequence we want, save the list of messages. */
+                    if (seq_in_list(name, sequences)) {
+                       this_msgnums = trimcpy(cp);
+                       if (msgnums == NULL) {
+                           msgnums = this_msgnums;
+                       } else {
+                           old_msgnums = msgnums;
+                           msgnums = concat(old_msgnums, " ",
+                                            this_msgnums, (void *)NULL);
+                           free(old_msgnums);
+                           free(this_msgnums);
+                       }
+                    }
+                    free (cp);
+                } else {
+                   /* and here */
+                    if (seq_in_list(name, sequences)) {
+                       this_msgnums = trimcpy(field);
+                       if (msgnums == NULL) {
+                           msgnums = this_msgnums;
+                       } else {
+                           old_msgnums = msgnums;
+                           msgnums = concat(old_msgnums, " ",
+                                            this_msgnums, (void *)NULL);
+                           free(old_msgnums);
+                           free(this_msgnums);
+                       }
+                    }
+                }
+
+                if (state == FLDEOF)
+                    break;
+                continue;
+
+            case BODY:
+            case BODYEOF:
+                adios (NULL, "no blank lines are permitted in %s", seqfile);
+                /* fall */
+
+            case FILEEOF:
+                break;
+
+            default:
+                adios (NULL, "%s is poorly formatted", seqfile);
+        }
+        break;  /* break from for loop */
+    }
+
+    fclose(fp);
+
+    return msgnums;
+}
+
+/* Check `folder' (of length `len') for interesting messages, filling in the
+ * list in `b'. */
+static void
+check_folder(char *folder, size_t len, struct list_state *b)
+{
+    char *msgnums = get_msgnums(folder, b->sequences);
+    int is_cur = strcmp(folder, b->cur) == 0;
+
+    if (is_cur || msgnums != NULL) {
+       if (*b->first == NULL) {
+           *b->first = b->node = mh_xmalloc(sizeof(*b->node));
+       } else {
+           b->node->n_next = mh_xmalloc(sizeof(*b->node));
+           b->node = b->node->n_next;
+       }
+       b->node->n_name = folder;
+       b->node->n_field = msgnums;
+
+       if (*b->maxlen < len) {
+           *b->maxlen = len;
+       }
+    }
+
+    /* Save the node for the current folder, so we can fall back to it. */
+    if (is_cur) {
+       *b->cur_node = b->node;
+    }
+}
+
+static boolean
+crawl_callback(char *folder, void *baton)
+{
+    check_folder(folder, strlen(folder), baton);
+    return TRUE;
+}
+
+/* Scan folders, returning:
+ * first        -- list of nodes for all folders which have desired messages;
+ *                 if the current folder is listed in .folders, it is also in
+ *                 the list regardless of whether it has any desired messages
+ * last         -- last node in list
+ * cur_node     -- node of current folder, if listed in .folders
+ * maxlen       -- length of longest folder name
+ *
+ * `cur' points to the name of the current folder, `folders' points to the
+ * name of a .folder (if NULL, crawl all folders), and `sequences' points to
+ * the array of sequences for which to look.
+ */
+static void
+check_folders(struct node **first, struct node **last,
+             struct node **cur_node, size_t *maxlen,
+             char *cur, char *folders, char *sequences[])
+{
+    struct list_state b;
+    FILE *fp;
+    char *line;
+    size_t len;
+
+    *first = *cur_node = NULL;
+    *maxlen = 0;
+
+    b.first = first;
+    b.cur_node = cur_node;
+    b.maxlen = maxlen;
+    b.cur = cur;
+    b.sequences = sequences;
+
+    if (folders == NULL) {
+       chdir(m_maildir(""));
+       crawl_folders(".", crawl_callback, &b);
+    } else {
+       fp = fopen(folders, "r");
+       if (fp  == NULL) {
+           adios(NULL, "failed to read %s", folders);
+       }
+       while (vfgets(fp, &line) == OK) {
+           len = strlen(line) - 1;
+           line[len] = '\0';
+           check_folder(getcpy(line), len, &b);
+       }
+       fclose(fp);
+    }
+
+    if (*first != NULL) {
+       b.node->n_next = NULL;
+       *last = b.node;
+    }
+}
+
+/* Return a single string of the `sequences' joined by a space (' '). */
+static char *
+join_sequences(char *sequences[])
+{
+    int i;
+    size_t len = 0;
+    char *result, *cp;
+
+    for (i = 0; sequences[i] != NULL; i++) {
+       len += strlen(sequences[i]) + 1;
+    }
+    result = mh_xmalloc(len + 1);
+
+    for (i = 0, cp = result; sequences[i] != NULL; i++, cp += len + 1) {
+       len = strlen(sequences[i]);
+       memcpy(cp, sequences[i], len);
+       cp[len] = ' ';
+    }
+    /* -1 to overwrite the last delimiter */
+    *--cp = '\0';
+
+    return result;
+}
+
+/* Return a struct node for the folder to change to.  This is the next
+ * (previous, if FPREV mode) folder with desired messages, or the current
+ * folder if no folders have desired.  If NEW or UNSEEN mode, print the
+ * output but don't change folders.
+ *
+ * n_name is the folder to change to, and n_field is the string list of
+ * desired message numbers.
+ */
+static struct node *
+doit(char *cur, char *folders, char *sequences[])
+{
+    struct node *first, *cur_node, *node, *last, *prev;
+    size_t folder_len;
+    int count, total = 0;
+    char *command, *sequences_s;
+
+    if (cur == NULL || cur[0] == '\0') {
+        cur = "inbox";
+    }
+
+    check_folders(&first, &last, &cur_node, &folder_len, cur,
+                 folders, sequences);
+
+    if (run_mode == FNEXT || run_mode == FPREV) {
+       if (first->n_next == NULL) {
+           /* We have only one node; any desired messages in it? */
+           if (first->n_field == NULL) {
+               return NULL;
+           } else {
+               return first;
+           }
+       } else if (cur_node == NULL) {
+           /* Current folder is not listed in .folders, return first. */
+           return first;
+       }
+    } else if (run_mode == UNSEEN) {
+       sequences_s = join_sequences(sequences);
+    }
+
+    for (node = first, prev = NULL;
+        node != NULL;
+        prev = node, node = node->n_next) {
+        if (run_mode == FNEXT) {
+            /* If we have a previous node and it is the current
+             * folder, return this node. */
+            if (prev != NULL && strcmp(prev->n_name, cur) == 0) {
+                return node;
+            }
+        } else if (run_mode == FPREV) {
+            if (strcmp(node->n_name, cur) == 0) {
+                /* Found current folder in fprev mode; if we have a
+                 * previous node in the list, return it; else return
+                 * the last node. */
+                if (prev == NULL) {
+                    return last;
+                }
+                return prev;
+            }
+        } else if (run_mode == UNSEEN) {
+            if (node->n_field == NULL) {
+                continue;
+            }
+
+            printf("\n%d %s messages in %s",
+                   count_messages(node->n_field),
+                  sequences_s,
+                   node->n_name);
+            if (strcmp(node->n_name, cur) == 0) {
+                puts(" (*: current folder)");
+            } else {
+                puts("");
+            }
+            fflush(stdout);
+
+           /* TODO: Split enough of scan.c out so that we can call it here. */
+           command = concat("scan +", node->n_name, " ", sequences_s,
+                            (void *)NULL);
+           system(command);
+           free(command);
+        } else {
+            if (node->n_field == NULL) {
+                continue;
+            }
+
+            count = count_messages(node->n_field);
+            total += count;
+
+            printf("%-*s %6d.%c %s\n",
+                   folder_len, node->n_name,
+                   count,
+                   (strcmp(node->n_name, cur) == 0 ? '*' : ' '),
+                   node->n_field);
+        }
+    }
+
+    /* If we're fnext, we haven't checked the last node yet.  If it's the
+     * current folder, return the first node. */
+    if (run_mode == FNEXT && strcmp(last->n_name, cur) == 0) {
+        return first;
+    }
+
+    if (run_mode == NEW) {
+        printf("%-*s %6d.\n", folder_len, " total", total);
+    }
+
+    return cur_node;
+}
+
+int
+main(int argc, char **argv)
+{
+    char **ap, *cp, **argp, **arguments;
+    char help[BUFSIZ];
+    char *folders = NULL;
+    char *sequences[NUMATTRS + 1];
+    int i = 0;
+    char *unseen;
+    struct node *folder;
+
+#ifdef LOCALE
+    setlocale(LC_ALL, "");
+#endif
+    invo_name = r1bindex(argv[0], '/');
+
+    /* read user profile/context */
+    context_read();
+
+    arguments = getarguments (invo_name, argc, argv, 1);
+    argp = arguments;
+
+    /*
+     * Parse arguments
+     */
+    while ((cp = *argp++)) {
+       if (*cp == '-') {
+           switch (smatch (++cp, switches)) {
+           case AMBIGSW:
+               ambigsw (cp, switches);
+               done (1);
+           case UNKWNSW:
+               adios (NULL, "-%s unknown", cp);
+
+           case HELPSW:
+               snprintf (help, sizeof(help), "%s [switches] [sequences]",
+                         invo_name);
+               print_help (help, switches, 1);
+               done (1);
+           case VERSIONSW:
+               print_version(invo_name);
+               done (1);
+
+           case FOLDERSSW:
+               if (!(folders = *argp++) || *folders == '-')
+                   adios(NULL, "missing argument to %s", argp[-2]);
+               continue;
+           case MODESW:
+               if (!(invo_name = *argp++) || *invo_name == '-')
+                   adios(NULL, "missing argument to %s", argp[-2]);
+               invo_name = r1bindex(invo_name, '/');
+               continue;
+           }
+       }
+       /* have a sequence argument */
+       if (!seq_in_list(cp, sequences)) {
+           sequences[i++] = cp;
+       }
+    }
+
+    if (strcmp(invo_name, "fnext") == 0) {
+        run_mode = FNEXT;
+    } else if (strcmp(invo_name, "fprev") == 0) {
+        run_mode = FPREV;
+    } else if (strcmp(invo_name, "unseen") == 0) {
+        run_mode = UNSEEN;
+    }
+
+    if (folders == NULL) {
+       /* will flists */
+    } else {
+       if (folders[0] != '/') {
+           folders = m_maildir(folders);
+       }
+    }
+
+    if (i == 0) {
+       /* no sequence arguments; use unseen */
+       unseen = context_find(usequence);
+       if (unseen == NULL || unseen[0] == '\0') {
+           adios(NULL, "must specify sequences or set %s", usequence);
+       }
+       for (ap = brkstring(unseen, " ", "\n"); *ap; ap++) {
+           sequences[i++] = *ap;
+       }
+    }
+    sequences[i] = NULL;
+
+    folder = doit(context_find(pfolder), folders, sequences);
+    if (folder == NULL) {
+        done(0);
+        return 1;
+    }
+
+    if (run_mode == UNSEEN) {
+        /* All the scan(1)s it runs change the current folder, so we
+         * need to put it back.  Unfortunately, context_replace lamely
+         * ignores the new value you give it if it is the same one it
+         * has in memory.  So, we'll be lame, too.  I'm not sure if i
+         * should just change context_replace... */
+        context_replace(pfolder, "defeat_context_replace_optimization");
+    }
+
+    /* update current folder */
+    context_replace(pfolder, folder->n_name);
+
+    if (run_mode == FNEXT || run_mode == FPREV) {
+        printf("%s  %s\n", folder->n_name, folder->n_field);
+    }
+
+    context_save();
+
+    done (0);
+    return 1;
+}