Source Code
gen_mail_report
  Prev   Next
#!/usr/bin/perl
# vi:ts=4
#
# gen_mail_report   - generate a report from the summarization statistics
#                     gathered by "extract_mail_stats"
#                   - run by the 'report_mail_stats' script
#
#
# See "Usage" part below for list of flags.
#
#
#   Matt Covey, MetaWire, Inc., June, July 1997
#       initially from ideas by Tom Christiansen <tchrist@convex.com> ("ssl"),
#       and Paul O'Neill, Coastal Imaging Lab, Oregon State University,
#       18 jun 90
#
#
# Typical output:
#    
#   
#    Tue, May 17  8438 msgs arrived, delivered to 7108 addresses [50.147 Mbytes]
#                11949 errors
#                    6 msgs / minute (ave) [min 4, max 7]
#    
#       From    To    Domain                                   %     Bytes 
#    ------- -------- -------------------------------------- --- --------- 
#    <-  158  4910 -> test.com.............................. 32%  36.629 M
#    <-  504     4 -> yahoo.com.............................  3%     578 K
#    <-   58   446 -> genius.com............................  3%   2.293 M
#              389 -> cyberagenz.com........................  2%   2.373 M
#              321 -> [ignored].............................  2%   1.249 M
#    <-  314          <blank 'from'>........................  2%   1.890 M
#    <-    4   281 -> transware.com.........................       1.406 M
#    <-  221     6 -> hotmail.com...........................         288 K
#    <-  126          msn.com...............................         181 K
#               90 -> mac.com...............................       1.462 M
#    <-   74     4 -> comcast.net...........................       3.834 M
#    <-   70          funtimedate.com.......................          80 K      [list shortened
#    <-    1          cinemateka.ru.........................         388 K       after this line]
#    <- 5655    69 -> 3423 domains with < 15 msgs, 2%, 300K. 36%  20.564 M
#    ------- -------- -------------------------------------- --- --------- 
#    
#    
#    ------------------------------------------------------------------------------|
#     411|                                                           *             |
#     390|                       *                                   *             |
#     369|                 *     *           *     *           *  *  *             |
#     349|     *     *  *  *  *  *        *  *     *  *        *  *  *  *          |
#     328|  *  *  *  *  *  *  *  *  *     *  *     *  *     *  *  *  *  *  *       |
#     308|  *  *  *  *  *  *  *  *  *  *  *  *     *  *  *  *  *  *  *  *  *       |
#     287|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *     * |
#     267|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *     * |
#     246|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *     * |
#     226|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * |
#     205|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * |
#     184|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * |
#     164|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * |
#     143|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * |
#     123|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * |
#     102|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * |
#      82|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * |
#      61|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * |
#      41|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * |
#      20|  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * |
#    ------------------------------------------------------------------------------|
#    hour: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#    ------------------------------------------------------------------------------|
#
require '/usr/local/etc/syswatch/bin/sw_common.pl';
do set_common_vars();


#
# Defaults:
#
#                                 report if...
$TheThresholdMsgs       = 0;    #      has this many msgs, else goes in "others" category
$UseNMsgThreshold       = 0;    # off

$TheThresholdPercent    = 0;    #   or has this much percent of total to/from msgs
$UsePercentThreshold    = 0;    # off

$TheThresholdKBytes     = 0;    #   or has this many Kbytes
$UseKBytesThreshold     = 0;    # off

$ReportStrangeLines     = 1;    # on
$ReportDeferrals        = 1;    # on
$ReportTotals           = 1;    # on
$ReportBytes            = 1;    # on
$ReportDetails          = 1;    # on
$ReportHourly           = 0;    # off
$ReportAveragePer       = 1;    # off
$IsSeparateDefersTable  = 0;    # off
$ChangeUserToDomain     = 1;    # on
$TopLevelDomainsOnly    = 0;    # off


#
# User/domain aliases, ones to ignore, etc
#
$TheIgnoreFile          = "$SW_CONFIG/mailusers.ignore";
$TheUserAliasesFile     = "$SW_CONFIG/mailusers.aliases";
$TheDomainAliasesFile   = "$SW_CONFIG/maildomains.aliases";


#
# Display related details:
#
$MIN_PERCENT_TO_PRINT_PERCENT_NUMBER = 2;       # otherwise don't print anything
$MAX_USER_NAME_COL_WIDTH    = 38;
$N_LINES_OF_GRAPH_DATA      = 20;

#
# Some log entries can be: "... from=<>, size=..."
# Use this string as a placeholder.
#
$NO_FROM_USER   = '<blank \'from\'>';
$NO_TO_USER     = '<blank \'to\'>';


#
# Main part of program:
#
    do handle_args ();
    do hash_passwd () if $ReduceToLocalMailbox;
    
    do read_ignore_users  ( $TheIgnoreFile        ) if( -e $TheIgnoreFile );
    do read_user_aliases  ( $TheUserAliasesFile   ) if( -e $TheUserAliasesFile );
    do read_domain_aliases( $TheDomainAliasesFile ) if( -e $TheDomainAliasesFile );
    
    while( $ARGV[0] )
    {
        $NDays++;
        do read_stats( $ARGV[ 0 ] );
        shift;
    }
    
    do output_stats();
    
    exit( 0 );
#
# -- end --
#

#
# handle_args
#
# Parse the args and set out internal flags
#
sub handle_args
{
    while( $ARGV[ 0 ] =~ /^-/ )
    {
        if( &is_arg( '-threshold' ) || &is_arg( '-t' ) )
        {
            $n = int( $ARGV[ 0 ] );
            $last_chr = substr( $ARGV[ 0 ], length( $ARGV[ 0 ] - 1 ) );
            if( $last_chr eq '%' )
            {
                $TheThresholdPercent = $n;
                $UsePercentThreshold = 1;
            }
            elsif( $last_chr eq 'K' )
            {   
                $TheThresholdKBytes  = $n;
                $UseKBytesThreshold  = 1;
            }
            elsif( $last_chr eq 'M' )
            {
                $TheThresholdKBytes  = $n * 1024;
                $UseKBytesThreshold  = 1;
            }
            else
            {
                $TheThresholdMsgs    = $n;
                $UseNMsgThreshold    = 1;
            }
            shift ARGV;
            next;
        }
        
        if( &is_arg( '-all' ) )
        {
            $TheThresholdKBytes  = 0;
            $UseKBytesThreshold  = 0;
            $TheThresholdPercent = 0;
            $UsePercentThreshold = 0;
            $TheThresholdMsgs    = 0;
            $UseNMsgThreshold    = 0;
            next;
        }
        
        $ChangeUserToDomain  = 0, next  if( &is_arg( '-Users'           ) );
        $ChangeUserToDomain  = 1, next  if( &is_arg( '-Domains'         ) );
        $TopLevelDomainsOnly = 1, next  if( &is_arg( '-topleveldomains' ) );
        $ReportDetails       = 0, next  if( &is_arg( '-nodetails'       ) );
        $ReportTotals        = 0, next  if( &is_arg( '-nototals'        ) );
        $ReportBytes         = 0, next  if( &is_arg( '-nobytes'         ) );
        $ReportDeferrals     = 0, next  if( &is_arg( '-nodefers'        ) );
        $ReportHourly        = 1, next  if( &is_arg( '-hourly'          ) );
        $ReportAveragePer    = 1, next  if( &is_arg( '-avePer'          ) );
        $ReportStrangeLines  = 1, next  if( &is_arg( '-e'               ) );
        $ReduceToLocalMailbox= 1, next  if( &is_arg( '-m'               ) );
        $Debug               = 1, next  if( &is_arg( '-d'               ) );
        $Debug               = 2, next  if( &is_arg( '-d2'              ) );
        
        $TheTimeSpanLabel    = $ARGV[ 0 ], shift ARGV, next if( &is_arg( '-periodLabel' ) );
        
        print stderr "unknown arg: $ARGV[ 0 ]\n";
        print "Usage: $program [flags] counts_file+
  flags=
    -Domains          reduce addresses to domains           (jj\@A.B.co.jp -> B.co.jp)
    -topleveldomains  reduce addresses to top level domains (jj\@A.B.co.jp -> co.jp)
    -Users            use full address                      (jj\@A.B.co.jp)
    
    -threshold <num>  min number  of msgs needed to report addr
                xx%   min percent of msgs needed to report addr
                xxK   min KBytes  of msgs needed to report addr
                xxM   min MBytes  of msgs needed to report addr
    -t                same as -threshold
    -all              show all addresses (don't use thresholds)
    
    -hourly           print a graph of hourly received times
    -nototals         suppress printing of totals
    -nodetails        suppress printing of details
    -nodefers         suppress printing of deferral counts
    -nobytes          suppress printing of byte counts
    -avePer           print average msgs per day, etc
    
    -periodLabel      use this as the time period label (ex: 'Tue, Jul 1')
    -d                turn on debug mode
    -d2               turn even more debugging on
    -m                reduce to local mbox is possible
    -e                print strange lines to stderr
";
        exit -1;
    }
    
#   print "Debug mode\n" if( $Debug );
}


sub mk_threshold_label
{
    local ( $n_lbl ) = @_;
    
    local ( $type_lbl ) = 'domain';
    $type_lbl = 'user'          if( ! $ChangeUserToDomain );
    $type_lbl = $type_lbl . 's' if( int( $n_lbl ) != 1 );
    local ( $lbl ) = "$n_lbl $type_lbl with <";
    local ( $sep ) = ' ';
    
    if( $TheThresholdMsgs    >  1 )
    {
        $lbl = $lbl . "$sep$TheThresholdMsgs msgs";
        $sep = ', ';
    }
    
    if( $TheThresholdPercent >= 1 )
    {
        $lbl = $lbl . "$sep$TheThresholdPercent%";
        $sep = ', ';
    }
    
    if( $TheThresholdKBytes  >= 1 )
    {
        local ( $unit ) = 'K';
        local ( $n )    = $TheThresholdKBytes;
        
        if( $TheThresholdKBytes >= 1000 )
        {
            $n    = int( $TheThresholdKBytes / 1000 );
            $unit = 'M';
        }
        $lbl = $lbl . "$sep${n}$unit";
        $sep = ', ';
    }
    
#   $lbl = $lbl . " <<";
    
    return $lbl;
}


#
# read_ignore_users
#
# Read the file containing users to not print info about
#
sub read_ignore_users
{
    local ( $fn ) = @_;
    
    open( IUF, $fn ) || die "Can't open ignore file $fn";
    
    while( <IUF> )
    {
        next    if( /^#/ );
        next    if( /^$/ );
        chop( $_ );
        $TheIgnoreUsers{ $_ } = 1;
        print "Ignoring user $_\n"  if( $Debug > 1 );
    }
    print "\n" if( $Debug > 1 );
    
    close( IUF );
}


#
# read_user_aliases
#
# Read the file containing aliases for users
#
sub read_user_aliases
{
    local ( $fn ) = @_;
    local ( $contains, $called );
    
    open( IUF, $fn ) || die "Can't open alias file $fn";
    while( <IUF> )
    {
        next    if( /^#/ );
        next    if( /^$/ );
        chop( $_ );
        ($contains, $called) = split;
        $TheUserAliases{ $contains } = $called;
        print "Aliasing: users containing '$contains' -> $called\n" if( $Debug > 1 );
    }
    print "\n" if( $Debug > 1 );
    
    close( IUF );
    
    @TheUserAliasKeys   = keys( TheUserAliases );
}


#
# read_domain_aliases
#
sub read_domain_aliases
{
    local ( $fn ) = @_;
    local ( $contains, $called );
    
    open( IUF, $fn ) || die "Can't open alias file $fn";
    
    while( <IUF> )
    {
        next    if( /^#/ );
        next    if( /^$/ );
        chop( $_ );
        ($contains, $called) = split;
        $TheDomainAliases{ $contains } = $called;
        print "Aliasing: users containing '$contains' -> $called\n" if( $Debug > 1 );
    }
    print "\n" if( $Debug > 1 );
    
    close( IUF );
    
    @TheDomainAliasKeys = keys( TheDomainAliases );
}


#
# read_stats
#
sub read_stats
{
    local ( $fn ) = @_;
    local ( $user, $n_to, $n_from, $bytes_to, $bytes_from, $n, $ndeferrals );
    
    open( LD, $fn ) || die "Can't open $fn";
    
    while( <LD> )
    {
        next    if( /^#/ );
        next    if( /^$/ );
        
        $NMsgsFrom       += $1, next    if( /^MsgsFrom  (\d+)/ );
        $NMsgsTo         += $1, next    if( /^MsgsTo    (\d+)/ );
        $NErrors         += $1, next    if( /^Error lines   (\d+)/ );
        $NDeferrals      += $1, next    if( /^Deferred  (\d+)/ );
        $NBlocked        += $1, next    if( /^Blocked   (\d+)/ );
        $NBlockedExpired += $1, next    if( /^BlockedExpired    (\d+)/ );
        $NUnblocked      += $1, next    if( /^Unblocked (\d+)/ );
        
        #
        # Hourly    0       5       2       10      1   ...
        #
        if( /^Hourly    (.*)/ )
        {
            local ( @hrs ) = split( /\t/, $1 );
            local ( $hr );
            foreach $hr (00..23)
            {
                $Hourly{ $hr } += $hrs[ int( $hr ) ];
            }
            next;
        }
        
        #
        # A regular "user" line:
        #
        ($user, $n_to, $n_from, $bytes_to, $bytes_from, $ndeferrals) = split( /\t/ );
        
        #
        # Check for replacing this user with an alias
        #
        $user = &chk_for_alias( $user );
        
        #
        # Only want to deal with domains?
        #
        #   matt@kamson.com     -> kamson.com
        #
        $user = &extract_domain( $user )    if( $ChangeUserToDomain );
        
        $to_user_count  { $user } += $n_to;
        $from_user_count{ $user } += $n_from;
        $to_user_size   { $user } += $bytes_to;
        $from_user_size { $user } += $bytes_from;
        $deferrals      { $user } += $ndeferrals;
        
        $total_to            += $n_to;
        $total_from          += $n_from;
        $total_to_size       += $bytes_to;
        $total_from_size     += $bytes_from;
        $total_msgs_deferred += $ndeferrals;
    }
    
    close( LD );
}


#
# extract_domain
#
# Given a mail address      matt@mail.metawire.com
# return the domain part:             metawire.com
#
# NOTE that if a name has several parts, we just
#       remove the top-most:
# 
#                           matt@mail.northca.metawire.com
#       return                        northca.metawire.com
#
sub extract_domain
{
    local ( $addr ) = @_;
    
    local ( $usr, $host_or_domain ) = split( '@', $addr, 2 );
    
    #
    # Check for invalid addresses, like:
    #
    #       @angstrom.metawire.com:anjyo@hrl.hitachi.co.jp
    #
    # Which would have split into:
    #
    #       $usr            = ''        -- nothing
    #       $host_or_domain = angstrom.metawire.com:anjyo@hrl.hitachi.co.jp
    #
    local ( $n ) = 0;
    while( $host_or_domain =~ /.*\@(.*)/ )
    {
        #
        # Split into:   angstrom.metawire.com:anjyo  hrl.hitachi.co.jp
        #               
        print "domain: bad addr: $addr\n"   if( $Debug && $n++ == 0 );
        
        $host_or_domain = $1;
    }   
    
    #
    # No domain part ? Then rtn addr
    #
    return $addr    if( ! $host_or_domain );
    
    local ( $mach, $domain ) = split( '\.', $host_or_domain, 2 );
    
    #
    # Must be "apple.com", not "lists.apple.com" ?
    #
    if( $TopLevelDomainsOnly && $domain =~ /.*\.(.*\..*)$/ )
    {
        return $1           if( $1 );
    }
    
    #
    # $domain is at least "abc.com"? Then can rtn that.
    #
    return $domain          if( $domain && $domain =~ /.*\..*/ );
    
    #
    # $host_or_domain is a good looking "abc.com"?
    #
    return $host_or_domain  if( $host_or_domain =~ /.*\..*/ );
    
    #
    # Doesn't fit the normal pattern, so just rtn the whole thing
    #
    return $addr;
}


#
# chk_for_alias
#
sub chk_for_alias
{
    local ( $usr ) = @_;
    local ( $a );
    
    foreach $a (@TheUserAliasKeys)
    {
        return $TheUserAliases{ $a }    if( $usr =~ /$a/ );
    }
    
    if( $ChangeUserToDomain )
    {
        foreach $a (@TheDomainAliasKeys)
        {
            return $TheDomainAliases{ $a }  if( $usr =~ /$a/ );
        }
    }
    
    return $usr;
}


#
# output_stats
#
sub output_stats
{
    local ( $user, $n, $max_user_name );
    
    $max_user_name = 10;
    
    #
    # Merge both together
    #
    @loop = keys( to_user_count );
    foreach $user (@loop)
    {
        $both_count{ $user } = $to_user_count{ $user };
        $n = length( $user );
        $max_user_name = $n if( $n > $max_user_name );
    }
    @loop = keys( from_user_count );
    foreach $user (@loop)
    {
        $both_count{ $user } += $from_user_count{ $user };
        $n = length( $user );
        $max_user_name = $n if( $n > $max_user_name );
    }
    
    local ( $below_thresh ) = &mk_threshold_label( "5####" );
    $n = length( $below_thresh );
    $max_user_name = $n if( $n > $max_user_name );
    
    $max_user_name = $MAX_USER_NAME_COL_WIDTH if( $max_user_name > $MAX_USER_NAME_COL_WIDTH );
    
    do print_totals()                   if( $ReportTotals );
    do print_details( $max_user_name )  if( $ReportDetails );
#   do print_defers()                   if( $IsSeparateDefersTable );
    do print_hourly_graph()             if( $ReportHourly );
}


#
# Mon, Jul  7  116 msgs arrived, delivered to 117 addresses [379 Kbytes]
#              167 errors
#               12 msgs deferred a total of 174 times
#               33 blocked
#                5 msgs / hour (ave) [min 0, max 30]
#
sub print_totals
{
    local ( $tspan ) = $TheTimeSpanLabel;
    local ( $indent ) = &pad_col( '', length( $TheTimeSpanLabel ), ' ' );
    
    #
    # Is "Jun 30..Jul  6, 1997" ? Since this makes the whole
    # first line too long, just split into:
    #
    #   Jun 30 ..
    #   Jul  6, 1997   1034 msgs arrived, delivered to 1255 addresses
    #
    if( $tspan =~ /(.*)\.\.(.*)/ )
    {
        $tspan  = "$1 ..\n$2 ";
        $indent = length( $2 ) + 1;
        $indent = substr( '                            ', 0, $indent );
    }
    
    printf "$tspan %5d msgs arrived", $NMsgsFrom;
    printf ", delivered to %d addresses", $NMsgsTo  if( $NMsgsFrom != $NMsgsTo );
#   print  " ($total_to w/ to, $total_from w/ from)" if( $total_to != $total_from );
    
    if( $ReportBytes )
    {
        local ( $n ) = $total_to_size;
        $n = $total_from_size if( $total_from_size > $n );
        local ( $kb ) = &KMbytes( $n, 'no spaces' );
        print " [${kb}bytes]";
    }
    print "\n";
    
    printf "$indent %5d errors\n",    $NErrors      if( $NErrors && $ReportStrangeLines );
    if( $NDeferrals )
    {
        printf "$indent %5d msgs deferred", $total_msgs_deferred;
        printf " a total of %d times", $NDeferrals if( $NDeferrals != $total_msgs_deferred );
        print  "\n";
    }
    if( $NBlocked )
    {
        printf "$indent %5d blocked", $NBlocked;
        printf ", %d expired", $NBlockedExpired     if( $NBlockedExpired );
        print  "\n"
    }
    elsif( $NBlockedExpired )
    {
        printf "$indent %5d blocked msgs expired\n", $NBlockedExpired   if( $NBlockedExpired );
    }
    printf "$indent %5d unblocked\n", $NUnblocked   if( $NUnblocked );
    
    if( $ReportAveragePer )
    {
    #   local ( $indent2 ) = substr( $indent, 0, length( $indent) - 5 );    # len( "(ave)" )
        local ( $nmsgs )   = $NMsgsFrom;
        $nmsgs = $NMsgsTo   if( $NMsgsTo > $nmsgs );
        
        local ( $n, $ave_msgs_unit ) = &calc_msgs_per_min_hr( $nmsgs, 24 );
        local ( $plural ) = '';
        $plural = 's'   if( $n > 1 );
        printf "$indent %5d msg$plural / $ave_msgs_unit (ave)", $n;
        
        local ( $min_val ) = 999999;
        local ( $max_val ) = 0;
        local ( $hr );
        foreach $hr (00..23)
        {
            $min_val = $Hourly{ $hr }   if( $Hourly{ $hr } < $min_val );
            $max_val = $Hourly{ $hr }   if( $Hourly{ $hr } > $max_val );
        }
        ($n, $unit) = &calc_msgs_per_min_hr( $min_val, 1 );
        print " [min $n, ";
        
        ($n, $unit) = &calc_msgs_per_min_hr( $max_val, 1, $unit );
        print "max $n";
        print ", per $unit"             if( $unit ne $ave_msgs_unit );
        print "]";
        
        printf ";  %d msgs / day", int( $nmsgs /  $NDays )  if( $NDays > 1 );
        
        print "\n";
    } 
    
    print "\n";
}


sub calc_msgs_per_min_hr
{
    local ( $nmsgs, $n_hours, $use_unit ) = @_;
    local ( $n, $unit );
    
    if( $use_unit eq '' || $use_unit eq 'minute' )
    {
        $n    = int( ($nmsgs / ($NDays * $n_hours * 60)) + 0.5 );
        $unit = 'minute';
    }
    
    if( $n == 0 || $use_unit eq 'hour' )
    {
        $n    = int( ($nmsgs / ($NDays * $n_hours)) + 0.5 );
        $unit = 'hour';
    }
    
    return ($n, $unit);
}


sub print_details
{
    local ( $max_user_name ) = @_;
    local ( $below_threshold, $n, $user );
    
    #
    #  From   To  Domain                                         %   Bytes   Defers
    # ----- ----- -------------------------------------------- --- --------- ------
    #
    &print_table_header();
    &print_table_header( '-' );

    #
    # Total up the "ignored" user count/sizes
    #
    local ( $n_ignored_to, $n_ignored_from, $sz_ignored_to, $sz_ignored_from, $n_defers_ignored );
    @loop = keys( both_count );
    foreach $user (sort bothsort @loop)
    {
        if( $TheIgnoreUsers{ $user } )
        {
            $n_ignored_to     += $to_user_count  { $user };
            $n_ignored_from   += $from_user_count{ $user };
            $sz_ignored_to    += $to_user_size   { $user };
            $sz_ignored_from  += $from_user_size { $user };
            $n_defers_ignored += $deferrals      { $user };
        }
    }
    if( $n_ignored_to || $n_ignored_from )
    {
        $user = "[ignored]";
        $to_user_count  { $user } = $n_ignored_to;
        $from_user_count{ $user } = $n_ignored_from;
        $to_user_size   { $user } = $sz_ignored_to;
        $from_user_size { $user } = $sz_ignored_from;
        $deferrals      { $user } = $n_defers_ignored;
        $both_count     { $user } = $n_ignored_to + $n_ignored_from;
    }
    
    #
    # print all the lines
    #
    @loop = keys( both_count );
    foreach $user (sort bothsort @loop)
    {
        &pr_user_line( $user, $max_user_name ) if( ! $TheIgnoreUsers{ $user } );
    }
    local ( $thres ) = &mk_threshold_label( "$NBelowThresholdUsers" );
    &pr_user_line( $thres, $max_user_name, $NBelowThresholdTo,  $NBelowThresholdFrom,
                                           $SzBelowThresholdTo, $SzBelowThresholdFrom,
                                           $NBelowThresholdDefers )
            if( $NBelowThresholdTo || $NBelowThresholdFrom || $NBelowThresholdDefers );
    
    #
    # And the bottom "------  ----- -----------------" line
    #
    &print_table_header( '-' )  if( $ReportTotals );
    
    print "\n";                 # a blank separator line
}


#
#    From    To    Domain                          %   Bytes   Defers 
# ------- -------- ----------------------------- --- --------- ------ 
#
sub print_table_header
{
    local ( $underline ) = @_;
    
    &pr_tbl_hdr_text( "   From",    $underline, ' ' );
    &pr_tbl_hdr_text( "   To   ",   $underline );
    
    local ( $who_lbl ) = 'Email address';
    $who_lbl = 'Domain' if( $ChangeUserToDomain );
    &pr_tbl_hdr_text( $who_lbl . &pad_col( $who_lbl, $max_user_name, ' ' ), $underline );
    
    &pr_tbl_hdr_text( "  %",        $underline );
    &pr_tbl_hdr_text( "    Bytes",  $underline )    if( $ReportBytes );
    &pr_tbl_hdr_text( "Defer",      $underline  )   if( $ReportDeferrals && $NDeferrals
                                                     && ! $IsSeparateDefersTable );
    print "\n";
}


sub pr_tbl_hdr_text
{
    local ( $lbl, $underline, $ending_sep ) = @_;
    
    $ending_sep = ' ' if( ! $ending_sep );
    
    print &pad_col( '', length( $lbl ), '-' )   if(   $underline );
    print $lbl                                  if( ! $underline );
    print $ending_sep;
}



#    From    To    Domain                          %   Bytes   Defers 
# ------- -------- ----------------------------- --- --------- ------ 
# <-   82   145 -> cyberstudios.com............. 13%     546 K     
# <-   34   103 -> metawire.com.................  8%     499 K     1
# <-    1   131 -> ffcnet.com...................  7%     445 K     
# <-   71          bungi.com....................  4%     300 K     
# <-   25    39 -> endeavorla.com...............  3%     185 K     
# <-   31    30 -> aol.com......................  3%     375 K     
# <-   50          watchdroid 6100..............  2%      10 K     
#            11 -> sierracanyon-pvt-k12-ca-us...                  12
# <-    3     6 -> edgela.com...................       2.119 M     
# <-    4     2 -> uu.net.......................          13 K     1
# <-    2     1 -> rotterdam.nl.................           2 K     1
# <-    1     1 -> msn.com......................           4 K     1
# <-  457   332 -> 223 domains with < 2%, 300K.. 46%   3.768 M     
# ------- -------- ----------------------------- --- --------- ------
#
sub pr_user_line
{
    local ( $user,
            $max_user_name,
            $n_to,
            $n_from,
            $sz_to,
            $sz_from,
            $n_defers ) = @_;
    
    if( $n_to == 0 && $n_from == 0 && $n_defers == 0 )
    {
        $n_to     = $to_user_count  { $user };
        $n_from   = $from_user_count{ $user };
        $sz_to    = $to_user_size   { $user };
        $sz_from  = $from_user_size { $user };
        $n_defers = $deferrals      { $user };
    }
    
    local ( $perc ) = do percent_of( $n_to + $n_from, $NMsgsFrom + $NMsgsTo );
    
    #
    # Check against thresholds - are there enough msgs/
    # % of msgs/bytes to report this individual, or so
    # few we'll just lump them in with the "others" group?
    #
    if( ($UseNMsgThreshold    && $n_to  + $n_from  >=  $TheThresholdMsgs)
     || ($UseKBytesThreshold  && $sz_to + $sz_from >= ($TheThresholdKBytes * 1024))
     || ($UsePercentThreshold && $perc             >=  $TheThresholdPercent)
     || $n_defers                                  !=  0
     || (! $UseNMsgThreshold && ! $UseKBytesThreshold && ! $UsePercentThreshold) )
    {
        # is above threshold
    }
    else    # is below threshold, so lump into the "others" category
    {
        $NBelowThresholdTo     += $n_to;
        $NBelowThresholdFrom   += $n_from;
        $SzBelowThresholdTo    += $sz_to;
        $SzBelowThresholdFrom  += $sz_from;
        $NBelowThresholdDefers += $n_defers;
        $NBelowThresholdUsers++;
        return;
    }
    
    #
    # From
    #
    print  "       "                if( $n_from == 0 );
    printf "<-%5d", $n_from         if( $n_from != 0 );
    
    #
    # To
    #
    print  "         "              if( $n_to == 0 );
    printf " %5d ->", $n_to         if( $n_to != 0 );
    
    #
    # Domain/Email addr
    #
    if( length( $user ) > $max_user_name )
    {
        $user = substr( $user, 0, $max_user_name - 1 ) . '+';
    }
    print " $user" . &pad_col( $user, $max_user_name, '.' );
    
    #
    # %
    #
    print  "     "              if( $perc <  $MIN_PERCENT_TO_PRINT_PERCENT_NUMBER );
    printf " %2d%% ", $perc     if( $perc >= $MIN_PERCENT_TO_PRINT_PERCENT_NUMBER );
    
    #
    # bytes
    #
    if( $ReportBytes )
    {
    #   print  "  ";
        
        $n = $sz_to + $sz_from;
        print "         "       if( $n == 0 );
        print &KMbytes( $n )    if( $n != 0 );
        
    #   print  "    "           if( $sz_to == 0 );
    #   printf "%4d", $sz_to    if( $sz_to != 0 );
        
    #   print  "    "           if( $sz_from == 0 );
    #   printf "%4d", $sz_from  if( $sz_from != 0 );
    }
    
    #
    # Defers
    #
    if( $ReportDeferrals && $NDeferrals && ! $IsSeparateDefersTable )
    {
        print  "      "             if( $n_defers == 0 );
        printf "  %4d", $n_defers   if( $n_defers != 0 );
    }
    
    print "\n";
}


#
# print_hourly_graph()
#
# Print a nice bar chart of the hourly info
#
sub print_hourly_graph
{
    local ( $hr, $max_val, $line );
    
    $max_val = 0;
    foreach $hr (00..23)
    {
        $max_val = $Hourly{ $hr }   if( $Hourly{ $hr } > $max_val );
    }

    $max_val = $N_LINES_OF_GRAPH_DATA if( $max_val < $N_LINES_OF_GRAPH_DATA );
    
    local ( $rs_scaling      ) = $max_val / $N_LINES_OF_GRAPH_DATA;
    local ( $percent_scaling ) =      100 / $N_LINES_OF_GRAPH_DATA;
    
    print "\n\n";
    print "-"x78;
    print "|\n";
    for($line=$N_LINES_OF_GRAPH_DATA; $line > -3; $line--)
    {
        next if( $line == 0 );
        if( $line == -1 )
        {
            print "-"x78;
            print "|\n";
            next;
        }
        
        local ( $value_for_this_line   ) = $line * $rs_scaling;
        local ( $percent_for_this_line ) = $line * $percent_scaling;
        printf ("%4d| ", int( $value_for_this_line ) ) if( $line > -1 );
        
        print "hour: " if ( $line == -2 );
        foreach $hr (0..23)
        {
            if( $line == -2 )
            {
                local ( $start ) = $hr;
                $start = "0$hr" if( $hr < 10 );
                print "$start ";
                next;
            }
            
            $ind = "$host $hr";
            
            if( ($line == 1 && $Hourly{ $hr } > 0)
             || ($Hourly{ $hr } >= $value_for_this_line) )
            {
                print " * ";
            }
            else
            {
                print "   ";
            }
        }
        print  "|\n";
    }
    print "-"x78;
    print "|\n";
}


#
# percent_of()
#
# Return how much $part is a percent of the $total
#
sub percent_of
{
    local ( $part, $total ) = @_;
    local ( $percent );
    
    if( $part == 0 || $total == 0 )
    {
        $percent = 0;
    }
    elsif( $part == $total )
    {
        $percent = 100;
    }
    elsif( $part > $total )
    {
        $percent = 100;
    }
    else
    {
        $percent = int( ($part * 100) / $total );
        $percent = 1    if( $percent == 0 );
    }
    return $percent;
}


sub tosort
{
    ($to_user_count  { $b } - $to_user_count{ $a })   * 10000000  + $to_user_size { $b } - $to_user_size  { $a };
}

sub fromsort
{
    ($from_user_count{ $b } - $from_user_count{ $a }) * 10000000 + $from_user_size{ $b } - $from_user_size{ $a };
}

sub bothsort
{
#   ($both_count{ $b } - $both_count{ $a });
#   (($both_count  { $b } - $both_count  { $a }) * 10000000) + ($a - $b);

    if( $both_count{ $b } == $both_count{ $a } )
    {
        ($b le $a);
    }
    else
    {
        (($both_count{ $b } - $both_count{ $a }) * 100000);
    }
}


#
# pr_pad()
#
# Pad out this many chrs
# //print( "-"x78 );
#
sub pr_pad
{
    local ( $wid,
            $lbl,
            $chr ) = @_;
    
    local ( $len ) = 0;
    if( $lbl )
    {
        $len = length( $lbl );
        print $lbl;
    }
    $chr = ' ' if( ! $chr );
    foreach ($len..$wid)
    {
        print $chr;
    }
}


#
# KMbytes()
#
# return a string for this many bytes:
#
#       10 K
#        3 K
#      854 K
#    4.1   M
#   13.5   M
#  384.2   M
#
sub KMbytes
{
    local ( $n, $suppress_spaces ) = @_;
    local ( $i );
    
    $n = int( ( int( $n ) + 1023) / 1024);
    
    #      10 K
    #       3 K
    #     854 K
    #
    if( $n < 1000 )
    {
        if( $suppress_spaces )
        {
            $i = "$n";
        }
        else
        {
            $i = "    $n";
            $i = " $i"  if( $n < 100 );
            $i = " $i"  if( $n <  10 );
        }
        return "$i K";
    }
    
    #
    #    4.123 M
    #   13.123 M
    #  384.123 M
    #
    local ( $xx, $dd, $sd );
    $xx = int( $n / 1000 );
    $dd = int( ($n - ($xx * 1000)) );
    $sd = "$dd";
    $sd = "${sd}0" if( $dd < 100 );
    $sd = "${sd}0" if( $dd <  10 );
    $i = "$xx.$sd";
#   $i = "$i.000" if( int( $n ) == $n );
    if( ! $suppress_spaces )
    {
        $i = " $i"  if( $xx < 100 );
        $i = " $i"  if( $xx <  10 );
    }
    return "$i M";
}


sub strip
{
    local($foo) = shift(@_);
#print "$foo\n";
    
    $foo =~ s/@.*//;
    $foo =~ s/.*!//;
    $foo =~ s/\s*\(.*\)//;
    $foo =~ tr/A-Z/a-z/;

    return $foo;
} 


sub hash_passwd
{
    chop( $yp = `/bin/domainname` ) if -x '/bin/domainname';
    $passwd = $yp ? 'ypcat passwd |' : '/etc/passwd';
    open( PASSWD, $passwd ) || die "$program: can't open $passwd: $!\n";
    while( <PASSWD> )
    {
        /^(\w+):[^:]+:(\d+):.*/;
        ($who,$uid) = ($1, $2);
    #   $uid = 'zero' if ! $uid;  # kludge for uid 0
        $uid = 'zero' if( $uid == 0 && $who );
    #   $uid = 'zero' if defined($uid);
        $known{$who} = $uid;
    #print "$who $uid       $known{$who}\n";
    } 
    close PASSWD;
#print "SPECIEALLLL -- $known{''}\n";
}