#! /usr/bin/perl

    #   Read or search a mail folder and dispatch
    #   messages to folders based on action keys.
    
    #	by John Walker  http://www.fourmilab.ch/
    #	    	   September 2002

    #   The assignments to the %action hash define the keys
    #   which are accepted by the "Disposition" prompt.  You
    #   can add additional folders by simply
    #   specifying the key and path name.  The upper case
    #   keywords denote actions built into the program.
    #	If you add any built-in actions, be sure to document
    #	them in the printHelpText subroutine.

    $action{'d'} = "";	    # Discard and advance to next message
    $action{'j'} = "junk";
    $action{'m'} = "mail";

    $action{'b'} = 'BACK';
    $action{'f'} = 'FILE';
    $action{'q'} = "QUIT";
    $action{'v'} = 'VIEW';
    $action{'/'} = 'SEARCH';
    $action{'?'} = 'HELP';

    if ($#ARGV < 0) {
    	print("Usage: perl splitmail.pl mail_folder_file\n");
	exit(2);
    }
    open(IN, "<$ARGV[0]") || die "Cannot open input file $ARGV[0]";

    while (1) {
    	$whence = tell(IN);
    	if ((!($l = <IN>)) || ($l =~ m/^From /)) {
    	    last;
	}
    }
    
    if (!$l) {
    	print("No messages in mail folder!\n");
	exit(0);
    }
    
    $eof = 0;
    $pattern = '';  	    	# Initialise search pattern
    
    push(@backStack, $whence);
    
    while (!$eof) {
    
    	#   Read next message from mail folder.  At
	#   this point $l contains the first ("From ")
	#   line of the message.
	
	$nlines = 0;
	undef @message;
	$msize = length($l);
	&trim_end_of_line;
	$message[$nlines++] = $l;
	
	#   Read the balance of the message into the
	#   @message array.  Quit when the "From " line
	#   of the next message is encountered or the
	#   end of the folder is encountered.
	
	while (1) {
	    $whence = tell(IN);
	    if ((!($l = <IN>)) || ($l =~ m/^From /)) {
		last;
	    } else {
	    	&trim_end_of_line;
	    	$message[$nlines++] = $l;
	    	$msize += length($l);
    	    }
	}
	push(@backStack, $whence);
	
	if ($nlines > 0) {
	    &dispose_of_message;
	}
	$eof = !($l);
    }

#   Dispose of the message in the @message
#   array.
    
sub dispose_of_message {
    local($i, $from, $to, $subject, $disp, $afrom, $act);
    
    #	Parse message header for "interesting" items
    
    for ($i = 0; $i < $nlines; $i++) {
    	if ((length($message[$i]) == 0) ||
	    ($message[$i] =~ m/^\s*$/)) {
	    last;
	}
	
	if ($message[$i] =~ m/^From\s/) {
	    if (!($message[$i] =~ m/^From\s+-\s+/)) {
	    	$from = $message[$i];
	    } else {
	    	$afrom = $message[$i];
	    }
	}
	elsif ((!defined($from)) && ($message[$i] =~ m/^From:\s/)) {
	    $from = $message[$i];
	}
	elsif ($message[$i] =~ m/^To:\s/) {
	    $to = $message[$i];
	}
	elsif ($message[$i] =~ m/^Subject:\s/) {
	    $subject = $message[$i];
	}
	elsif ($message[$i] =~ m/^Date:\s/) {
	    $date = $message[$i];
	}
    }

    $disp = '';
    while (length($disp) == 0) {    
	print("\n");
	if (!defined($from)) {
	    print("$afrom\n");
	} else {
	    print("$from\n");
	}
	print("$to\n");
	print("$subject\n");
	print("$date\n");
	print("Size: $msize\n");
	
    	print("Disposition (" . join('', sort(keys(%action))) . "): ");
    	$disp = <STDIN>;
	chop($disp);
	if ((length($disp) > 0) && (!defined($action{substr($disp, 0, 1)}))) {
	    print("\nDisposition \"$disp\" undefined.  Enter ? for help.\n");
	    $disp = '';
	} else {
	    if (length($disp) > 0) {
	    	$act = $action{substr($disp, 0, 1)};
	    } else {
	    	$act = '';
		$disp = 'default';  	# Can be anything, so long as it isn't null
	    }
	    if (length($act) > 0) {
		if ($act eq 'QUIT') {
		    while (1) {
		    print("Really quit? (yn): ");
			$disp = <STDIN>;
			#   We accept 'q' as a synonym for 'y' Lynx-style, since it's
			#   much faster to type when you're furiously testing.
			if ((substr($disp, 0, 1) eq 'y') ||
		    	    (substr($disp, 0, 1) eq 'q')) {
		    	    undef $l;
			    last;
			} elsif (substr($disp, 0, 1) eq 'n') {
		    	    $disp = '';
			    last;
			}
		    }
		} elsif($act eq 'VIEW') {
		    &view_message;
		    $disp = '';
		} elsif ($act eq 'SEARCH') {
		    &search_messages;
		} elsif ($act eq 'BACK') {
		    if ($#backStack > 1) {
		    	pop(@backStack);
			pop(@backStack);
			seek(IN, $backStack[$#backStack], 0);
			$l = <IN>;
		    } else {
		    	print("At start of folder.\n");
			seek(IN, 0, 0);
			undef(@backStack);
			$l = <IN>;
			push(@backStack, 0);
		    	$disp = '';
		    }
		} elsif($act eq 'HELP') {
		    &printHelpText;
		    $disp = '';
		} else {
		    if ($act eq 'FILE') {
		    	print("File name: ");
		    	$act = <STDIN>;
			chop($act);
		    }
		    if (length($act) > 0) {
		    	if (open(OF, ">>$act")) {
			    for ($i = 0; $i < $nlines; $i++) {
		    		print(OF "$message[$i]\n");
			    }
			    close(OF);
			} else {
			    print("Cannot append message to file $act\n");
			    $disp = '';
			}
		    } else {
		    	$disp = '';
		    }
		}
	    }
	}
    }
}

#   Print help text for disposition prompt

sub printHelpText {
    local($k);
    
    print << "EOS";
    
b      Back to previous message
d      Display next message
f      Save in user-defined file
q      Quit
v      View message
/pat   Search for regular expression (+pat to find substring)
?      Print this message
EOS
    foreach $k (sort(keys(%action))) {
    	if ((length($action{$k}) > 0) &&
	    (!($action{$k} =~ m/^[A-Z]/))) {
	    print("$k      Save in folder \"$action{$k}\"\n");
	}
    }
}

#   Trim end of line sequences from line

sub trim_end_of_line {
    $l =~ s/(.*)([\r\n]+)$/\1/;
}

#   View text of message (like "more")

sub view_message {
    local($n, $page, $nx, $a, $ac);
    
    $page = 24;     	    # Page length
    $n = 0;
main:
    while (1) {
    	$nx = (($n + $page) < $nlines) ? ($n + $page) : $nlines;
    	for (; $n < $nx; $n++) {
	    print("$message[$n]\n");
	}
    	if ($nx < $nlines) {
	    while (1) {
		printf("--More--(%d%%) (bnq?): ", int(($nx * 100) / $nlines));
		$a = <STDIN>;
		chop($a);
		$ac = substr($a, 0, 1);
		if ((length($ac) == 0) || ($ac eq 'n')) {
		    last;
		}
		if ($ac eq 'q') {
	    	    last main;
		}
		if ($ac eq 'b') {
	    	    if ($n >= (2 * $page)) {
			$n -= 2 * $page;
		    } else {
			$n = 0;
		    }
	    	    next main;
		}
		if ($ac eq '?') {
		    print << "EOF";
		    
b     Back one page
n     Next page (blank line equivalent to n)
q     Quit viewing--back to disposition menu
?     Print this message
EOF
		}
	    }
	} else {
	    last;
	}
    }
}

#   Search messages for pattern.  You can abort a search
#   with ^C, which returns to the position in the folder
#   where the search began.  Note that the pattern is a
#   Perl regular expression, which is matched case-insensitive,
#   except if the first character is "+" (which is not valid
#   in a regular expression), in which case the following text
#   is matched explicitly, again ignoring case.

sub blooie {	    	    # Allow ^C to stop runaway search
    $searching = 0;
    print("^C terminated search.\n");
}

sub search_messages {
    local($startpos, $s, $msgpos, $lpos, @bsback, $lcp, $lcs);

    $SIG{'INT'} = 'blooie';
    chop($disp);
    if (length($disp) > 1) {
    	$pattern = substr($disp, 1);
    }
    if (length($pattern) == 0) {
    	print("Search pattern: ");
	$pattern = <STDIN>;
	chop($pattern);
    }

    $startpos = tell(IN);
    $searching = 1;
    @bsback = @backStack;
    $lcp = substr($pattern, 1);
    $lcp =~ tr/A-Z/a-z/;
    while ($searching) {
    	$lpos = tell(IN);
    	if (!($s = <IN>)) {
	    last;
	}
    	if ($s =~ m/^From /) {
	    $msgpos = $lpos;
	    push(@backStack, $lpos);
	}
	if ($pattern =~ m/^\+/) {
	    $lcs = $s;
	    $lcs =~ tr/A-Z/a-z/;
#    print("($lcp) ($lcs)\n");
	    if (index($lcs, $lcp) != -1) {
    		seek(IN, $msgpos, 0);
    		$l = <IN>;
    		$SIG{'INT'} = 'DEFAULT';
    		return;
	    }
	} elsif ($s =~ m/$pattern/i) {
    	    seek(IN, $msgpos, 0);
    	    $l = <IN>;
    	    $SIG{'INT'} = 'DEFAULT';
    	    return;
	}
    }
    print("No find.\n");
    seek(IN, $startpos, 0);
    @backStack = @bsback;
    $SIG{'INT'} = 'DEFAULT';
}


syntax highlighted by Code2HTML, v. 0.9.1