#!/usr/bin/env perl
#
# Copyright 1997-2024 Luke Mewburn <luke@mewburn.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#
# Install as the following programs:
#	new --
#		list all folders with unseen messages
#	fp --
#		move to previous folder with unseen messages
#	fn --
#		move to next folder with unseen messages
#	unseen --
#		list all unseen messages
#
#	The script uses `mhpath +`/.folders as a list of mh folders to
#	search for the 'unseen:' line in the .mh_sequences files of.
#
#	This is generated by the 'Find all folders' option in exmh,
#	or by invoking any of these commands with '-r' as the first argument.
# 
#	Thanks to Simon Burge for the concept, and Matt Green for fixing
#	some minor bugs.
#
#	http://www.mewburn.net/luke/src/new
#

#
# By default, the method used to determine the unseen sequence `cheats' and
# examines the .mh_sequences files directly. This is much faster than using
# `pick unseen`, but doesn't work for nmh's private (aka -nopublic) sequences.
# If you want the slower method, change $USE_PICK=0 to $USE_PICK=1.
#
$USE_PICK=0;


#	save invocation name of script, set output to flush after each line
#
($PROG=$0) =~ s!.*/!!;
$| = 1;

#	cd to ~/Mail
#
($mhplus) = mhpath("+");
chdir($mhplus) || die("$PROG: can't cd $mhplus - $!\n");

#	parse cmd line options
#
if ($ARGV[0] eq "-r") {
	rebuild_folders();
}

#	get list of unseen messages as well as maximum folder name length
#
($maxlen, %unseen) = get_unseen();
unless (keys %unseen) {
	print "No new messages...\n";
	exit(0);
}

#	get current folder
#
$cur = backtick("folder -fast -nocreate", 1);	
die("$PROG: no valid current folder defined\n") unless ($cur);

#	determine mode of operation
#
if ($PROG eq 'new') {
	new();		# if invoked as "new", summarise unseen folders
} elsif ($PROG eq 'unseen') {
	unseen();	# if invoked as "unseen", scan unseen in each folder
} elsif ($PROG eq 'fp') {
	fmove(-1);	# if invoked as "fp", move to previous folder
} elsif ($PROG eq 'fn') {
	fmove(1);	# if invoked as "fn", move to next folder
} else {
	die("$PROG: unknown invocation name!\n");
}
exit(0);


# ----
#


# backtick --
#	run a command in a backtick, and return the result.
#	barf on error unless ignoreerror is set
sub
backtick
{
	local($cmd, $ignoreerror) = @_;
	die("Usage ${PROG}::backtick(cmd [,ignoreerror])\n") unless ($cmd);
	local($result, $retval);

	chop($result = `$cmd 2>/dev/null`);
	$retval = $? >> 8;
	unless ($ignoreerror || $retval == 0)
	{
		die ("$PROG: $cmd returned $retval, $result\n")
	}
	return $result;
}


# mhpath --
#	determine path to given folder
#
sub
mhpath
{
	local($path) = $_[0] || die("Usage ${PROG}::mhpath(dir)\n");

	return backtick("mhpath $path");
}


# mhparam --
#	get the value of a .mh_profile parameter
#
sub
mhparam
{
	local($what) = $_[0] || die("Usage ${PROG}::mhparam(what)\n");

	return backtick("mhparam $what");
}


# weightinbox --
#	sort routine to weight inbox to front of list
#
sub
weightinbox
{
	if ($a eq "inbox" || $a eq "Inbox") {
		return -1;
	} elsif ($b eq "inbox" || $b eq "Inbox") {
		return 1;
	} else {
		return $a cmp $b;
	}
}


# rebuild_folders --
#	rebuild +/.folders
#
sub
rebuild_folders
{
	local(*FCMD, *FOLD);
	local($line, %list);

	open(FCMD, "folder -fast -all -recurse 2>/dev/null |") ||
	    die("$PROG: can't run folder to rebuild .folders - $!\n");
	print "Rebuilding folders list. Found:";
	while (defined($line = <FCMD>)) {
		chop($line);
		if ($line =~ /^\./) {	# skip folders with a leading "."
			next;
		}
		$list{$line}++;
		print " $line";
		print FOLD $line, "\n";
	}
	close(FCMD) || warn("$PROG: can't close folder pipe - $!\n");
	print "\n";

	open(FOLD, "> .folders") || die("$PROG: can't write .folders - $!\n");
	foreach $line (sort weightinbox keys %list) {
		print FOLD "$line\n";
	}
	close(FOLD) || warn("$PROG: can't close .folders - $!\n");
}


# get_unseen --
#	build map of unseen folders.
#	returns ($maxlen, %unseenlist), where $maxlen is length() of
#	longest name in list, and %unseenlist is map of folder=>unseenlist
#
sub
get_unseen
{
	local(*FLIST, *SEQF);
	local($folder, $file);
	local($max, %result);
	local($line, $unseen);

	open(FLIST, ".folders") || die("$PROG: can't open .folders - $!\n");
	$unseen = mhparam("unseen-sequence") || "unseen";
	while (defined($folder = <FLIST>)) {
		chop($folder);
				# fast way; doesn't support private folders
				#
		if (! $USE_PICK) {
			$file = "$folder/.mh_sequences";
			unless (-f $file && -r $file) {
				next;
			}
			open(SEQF, $file) || do {
				warn("$PROG: can't open $file - $!\n");
				next;
			};
			while (defined($line = <SEQF>)) {
				chop($line);
				unless ($line =~ /^${unseen}:\s+(\d.*)/o) {
					next;
				}
				$result{$folder} = $1;
				if (length($folder) > $max) {
					$max = length($folder);
				}
				last;
			}
			close(SEQF) || warn("can't close $file - $!\n");
		} else {	
				# mrg's way; slow, but always works
				#
			$unseen = backtick("pick +$folder unseen", 1);
			$unseen =~ s/\s+/ /g;
			$unseen =~ s/^\s+//;
			$unseen =~ s/\s+$//;
			unless (defined($unseen) && $unseen ne "") {
				next;
			}
			$result{$folder} = $unseen;
			if (length($folder) > $max) {
				$max = length($folder);
			}
		}
	}
	close(FLIST) || warn("can't close .folders - $!\n");
	return ($max, %result);
}


# sequence_len --
#	return the number of items in the sequence.
#
sub
sequence_len
{
	local($seq) = $_[0] || die("Usage ${PROG}::sequence_len(sequence)\n");

	local($count, $elem);

	$count = 0;
	foreach $elem (split(/ /, $seq)) {
		if ($elem =~ /^(\d+)-(\d+)$/) {
			$count += $2 - $1 + 1;
		} elsif ($elem =~ /^(\d+)$/) {
			$count ++;
		} else {
			die("$PROG: unknown sequence element $elem\n");
		}
	}
	return $count;
}

# bracket --
#	return the given value wrapped in your favourite brackets
#
sub
bracket
{
	local($val) = $_[0] || die("Usage ${PROG}::bracket(val)\n");

	return $val . ".";
#	return "<" . $val . ">";
#	return "{" . $val . "}";
#	return "[" . $val . "]";
#	return "(" . $val . ")";
}


# new --
#	list all folders with unseen messages
#
sub
new
{
	local($elem, $count, $total);

	$total = 0;
	if ($maxlen < 5) {	# length of "total"
		$maxlen = 5;
	}
	foreach $elem (sort weightinbox keys %unseen) {
		$count = sequence_len($unseen{$elem});
		$total += $count;
		printf("+%-${maxlen}s  %6s%s %s\n",
		    $elem,
		    bracket($count),
		    $cur eq $elem ? "*" : " ",
		    $unseen{$elem});
	}
	printf(" %-${maxlen}s %7s\n",
		    "total",
		    bracket($total));
}


# unseen --
#	list all unseen messages
#
sub
unseen
{
	local($elem, $count);

	foreach $elem (sort weightinbox keys %unseen) {
		$count = sequence_len($unseen{$elem});
		printf("\n%d unseen message%s in $elem%s\n",
		    $count,
		    $count == 1 ? "" : "s",
		    $cur eq $elem ? " (*: current folder)" : "");
		system("scan +$elem unseen 2>/dev/null");
	}
	backtick("folder -fast +$cur");
}


# fmove --
#	move to next folder with unseen messages
#	(or previous if $direction < 0)
#
sub
fmove
{
	local($direction) = $_[0];
	local($elem, $first, @ukeys);

	@ukeys = sort weightinbox (keys %unseen, ($unseen{$cur}) ? () :  $cur);
	if ($direction < 0) {
		@ukeys = reverse @ukeys;
	}
	$first = $ukeys[0];
	while ($elem = shift(@ukeys)) {
		if ($elem eq $cur) {
			last;
		}
	}
	$elem = shift(@ukeys) || $first;
	backtick("folder -fast +$elem");
	printf("+%-${maxlen}s   %s\n", $elem, $unseen{$elem});
}
