#!/usr/bin/perl # # dej This script restores messages from an MH/NMH junk folder to # the inbox folder, preferably using the same message number it # originally had. If the original message number is already in # use, then the restored message will be put at the end of the # inbox's messages. # # usage: # dejunk [-verbose | -help | -Version] ... # # Revision History # 1.0 Initial revision. 140112 # 1.1 Added license info. 180616 # # Written by Wayne Morrison, 140112. # # Copyright 2014 Wayne Morrison # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # use strict; use Getopt::Long qw(:config no_ignore_case_always); # # Version information. # my $NAME = "dej"; my $VERS = "$NAME version: 1.1"; ############################################################################ # # Options fields. # my %opts = (); # Options. # # Command line arguments. # my @opts = ( 'verbose', # Give verbose output. 'help', # Give a help message. 'Version', # Display the program version. ); my $verbose = 0; # Verbose flag. ############################################################################ my %commas = (); # Checksums of all the comma files. my $summer = 'cksum'; # Checksum program. my $INBOX = 'inbox'; # Default inbox folder name. my $JUNK = 'junk'; # Default junk folder name. my $inbox; # Path to MH inbox folder. my $junk; # Path to MH junk folder. my $errs = 0; # Error count. ############################################################################ main(); exit(0); #----------------------------------------------------------------------------- # Routine: main() # sub main { $| = 1; # # Munch on the options and arguments. # optsandargs(); # # Get paths to a set of MH folders. # getdirs(); # # Get checksums for all the comma files in the inbox. # getcommas($inbox); # # Move the specified files to an appropriate place in the inbox. # foreach my $msg (@ARGV) { saver($msg); } } #---------------------------------------------------------------------- # Routine: optsandargs() # # Purpose: Parse the command line for options and arguments. # sub optsandargs { # # Parse the options. # GetOptions(\%opts,@opts) || usage(); # # Check for some immediate-action options. # usage() if(defined($opts{'help'})); version() if(defined($opts{'Version'})); $verbose = $opts{'verbose'}; usage() if(@ARGV == 0); } #---------------------------------------------------------------------- # Routine: getdirs() # # Purpose: Get paths to a set of MH folders. # sub getdirs { # # Get the path to the MH inbox. # $inbox = `mhpath +$INBOX`; chomp $inbox; if($inbox eq '') { print STDERR "unable to find the inbox path\n"; exit(1); } # # Get the path to the MH junk folder. # $junk = `mhpath +$JUNK`; chomp $junk; if($junk eq '') { print STDERR "unable to find the junk folder path\n"; exit(1); } } #---------------------------------------------------------------------- # Routine: stuff_to_do() # # Purpose: Get checksums for all the comma files in the inbox. # sub getcommas { my $dir = shift; # Inbox directory. my @cfiles; # Comma files in inbox. # # Get the paths to all the comma files in the inbox. # vprint("looking for comma files in $dir"); @cfiles = glob("$dir/,*"); return if(@cfiles == 0); vprint("comma files count: " . @cfiles); # # Get checksums for all the comma files in the inbox. # # print "commas:\n"; foreach my $cf (@cfiles) { $commas{$cf} = checksum($cf); # print "\t$cf\t$commas{$cf}\n"; } # print "\n\n"; } #---------------------------------------------------------------------- # Routine: checksum() # # Purpose: Return a checksum of the given file. # -1 will be returned if the checksum program fails. # sub checksum { my $fn = shift; # Remote file to check. my $cmd; # Command to execute. my $csout; # Checksum output of the file. my $chksum; # Checksum of the file. if(! -f $fn) { print STDERR "\"$fn\" is not a regular file\n"; return(-2); } # # Build the checksum command. # $cmd = "$summer $fn"; # # Get info for the file. # $csout = `$cmd`; $csout =~ /(\d+)/; $chksum = $1; # # Return the checksum or -1 on failure. # return(-1) if(($? >> 8) != 0); return($chksum); } #---------------------------------------------------------------------- # Routine: saver() # # Purpose: This routine figures out the inbox message number for the # restored file, and then moves the junked message to that # inbox file. # sub saver { my $fn = shift; # File to save. my $fullfn; # Full filename. my $fnsum; # Checksum of file. my $newfn = ''; my $scstr; # Message scan output. my $lastmsg; # Last message number in inbox. my $msgnum; # Message number of new file. # # Get the full path to the file. # $fullfn = "$junk/$fn"; if(! -e $fullfn) { print "message $fn not in junk folder\n"; return; } # # Get the message number for the last message in the inbox and # increment it. # $scstr = `scan +$INBOX last`; $scstr =~ /^ ?(\d+)[ \+]/; $lastmsg = $1 + 1; # # Get the checksum for this message. # $fnsum = checksum($fullfn); # # Look in our commas hash for a deleted inbox file # with the same checksum. We'll hit on the first # we find. # foreach my $cfn (sort(keys(%commas))) { if($fnsum == $commas{$cfn}) { $newfn = $cfn; delete $commas{$cfn}; last; } } # # Get the message path for the restored file. # if($newfn ne '') { my $nocomma = $newfn; # # We found a matching file, so we'll use that number -- # unless there's already a file sitting there. # $nocomma =~ s/\/,(\d+)$/\/$1/; # print "match: $fullfn\n\t\t<$newfn>\n\t\t<$nocomma>\n"; # # If the inbox has a file whose name matches the comma file, # except without the comma, we'll restore the file to the end # of the inbox. # (E.g., 130 and ,130 both exist.) # # If the inbox does not have a file whose name matches the # comma file without the comma, we'll restore the file to # that position in the inbox. # (E.g., 130 doesn't exist and ,130 does exist.) # if(-e $nocomma) { $nocomma =~ s/\/(\d+)$/\/$lastmsg/; $newfn = $nocomma; # print "match -e: mv $fullfn $newfn\n"; } else { $newfn =~ s/\/,(\d+)$/\/$1/; # print "match ! -e: mv $fullfn $newfn\n"; } } else { # # We didn't find a matching file in the commas list, so we'll # we'll use a message number just past the end of the inbox set. # $newfn = "$inbox/$lastmsg"; # print "no-match: mv $fullfn $newfn\n"; } # print "$fullfn -> $newfn\n"; system("scan +junk $fn | cat -v"); system("mv $fullfn $newfn"); } #---------------------------------------------------------------------- # Routine: vprint() # # Purpose: Print a string if -verbose was given. # All trailing whitespace will be stripped. # sub vprint { my $str = shift; return if(! $verbose); $str =~ s/\s+$//; print "$str\n"; } #---------------------------------------------------------------------- # Routine: version() # # Purpose: Print the version number(s) and exit. # sub version { print STDERR "$VERS\n"; exit(0); } #---------------------------------------------------------------------- # Routine: usage() # # Purpose: Give usage message and exit. # sub usage { print STDERR "usage: dejunk [options] ... \n"; print STDERR "\n"; print STDERR "\twhere [options] are:\n"; print STDERR "\t\t-verbose\n"; print STDERR "\t\t-help\n"; print STDERR "\t\t-Version\n"; exit(0); } 1; ############################################################################## =pod =head1 NAME B - moves an MH/NMH mail message from a junk folder to the inbox =head1 SYNOPSIS dejunk [options] ... =head1 DESCRIPTION B restores messages from an MH/NMH junk folder to the inbox folder, preferably using the same message number it originally had. If the original message number is already in use, then the restored message will be put at the end of the inbox's messages. MH and NMH use directories and files for mail folders and mail messages, with everything for a user being stored in either I<~/mail> or I<~/Mail>. (Other directories are possible, but these are the most common.) Each mail folder is its own subdirectory. Each individual mail message is stored in its own file whose name is the message's number in its mailbox. In most cases, deleting a message (with the B command) doesn't actually delete the message, but instead prepends a comma to the message's file name. The comma informs MH not to consider that an "active" mail message. Similarly, when a message is refiled from one folder to another, a copy remains in the original folder but it has a comma prepended to its name. In restoring a message to the inbox, the junk message will be compared to the "deleted" inbox files in an attempt to find the original message number. File checksums are compared in order to find the original "deleted" file. When specifying messages to restore, B distinguishes between "deleted" junk messages (those with a comma prepended to the message number) and junk messages that haven't been deleted. In practical terms, this means that the actual file name in the junk folder must be given on the command line -- "dejunk ,12" and "dejunk 12" will act on different messages. B tries to restore a junk message to the same message number it had when it was junked. If that message number is already in use in the inbox, then the message will be moved to the end of the inbox's message list. There are three cases for restored files: =over 4 =item * no message in the inbox matches the specified junk message The junk message will be moved to the end of the inbox. =item * "deleted" message in the inbox matches the specified junk message, and no undeleted inbox message has that message number The junk message will be moved to the inbox with the message number (without leading comma) of the matching message. =item * "deleted" message in the inbox matches the specified junk message, and an undeleted inbox message has that message number The junk message will be moved to the end of the inbox, so as not to overwrite the active message having that message number. The junk folder is the folder named B in the MH/NMH mail directory. =back =head1 OPTIONS B takes the following options: =over 4 =item I<-verbose> This option provides verbose information about the operation of B. =item I<-Version> Display the version information for B. =item I<-help> Display a help message. =back =head1 LICENSE Copyright 2014 Wayne Morrison Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. =head1 AUTHOR Wayne Morrison, wayne@waynemorrison.com =head1 SEE ALSO B, B, B, B, B =cut