[alt.sources] Cmail - check to see who's read their mail - UNIX

jonathan@cs.keele.ac.uk (Jonathan Knight) (10/23/89)

Here's a neat little program which detects whether users on your
local machine have ready their mail recently.  It reports when they
last checked it and also whether any new mail has arrived since.

Very useful if your local machine uses userids which are based on
department or type of user or year of graduation.

You may copy this program as often as you like - just don't pretend
you wrote it.

You may rip off any useful bits of code you like - a "thankyou" would
be nice.


#!/bin/sh
# to extract, remove the header and type "sh filename"
if `test ! -s ./Makefile`
then
echo "Writing ./Makefile"
cat > ./Makefile << '\Rogue\Monster\'
# Makefile for cmail - a mail checking program
#
# This is the CFLAGS I had to use under Dynix so that the normal
# include files were found first, but regexp.h was found in the
# att universe.
#
#CFLAGS = -O -I/usr/include -I/usr/att/usr/include
#
CFLAGS = -O
LFLAGS =

# Destination:
# DEST	- The destination for the cmail executeable
# MDEST - The destination for the cmail.1 manual source

DEST = /usr/local/bin
MDEST = /usr/man/man1

#

all:		cmail

cmail:		cmail.c cmail.h
		cc $(CFLAGS) $(LFLAGS) -o cmail cmail.c

install:	cmail cmail.1
		install -c -m 755 -s cmail $(DEST)
		install -c -m 644 cmail.1 $(MDEST)

clean:
		rm -f cmail.o cmail core a.out
\Rogue\Monster\
else
  echo "Will not over-write ./Makefile"
fi
if `test ! -s ./README`
then
echo "Writing ./README"
cat > ./README << '\Rogue\Monster\'
	cmail - check when people have read their mail - Version 1.1

Cmail is useful if you want to check to see if a group
of people have read their mail.  It allows regular expressions
as a list of usernames to check, so sites which have usernames
derived from the department or project a user is involved in
will benefit.

Check the compile options in the Makefile and then take a look at
cmail.h.  Once these are correct run make.

Cmail has been tested on the following systems:

	Ultrix 1.2A (BSD4.2)
	Dynix 3.0.15 (BSD4.2 ish)
	Microport 2.4 (SYSVR2) (R.I.P.)
	SunOS 3.5 (BSD4.2) (Yellow Pages)

Bugs, etc can be sent to:

  ______    JANET :jonathan@uk.ac.keele.cs     Jonathan Knight,
    /       BITNET:jonathan%cs.kl.ac.uk@ukacrl Department of Computer Science
   / _   __ other :jonathan@cs.keele.ac.uk     University of Keele, Keele,
(_/ (_) / / UUCP  :...!ukc!kl-cs!jonathan      Staffordshire.  ST5 5BG.  U.K.
\Rogue\Monster\
else
  echo "Will not over-write ./README"
fi
if `test ! -s ./cmail.1`
then
echo "Writing ./cmail.1"
cat > ./cmail.1 << '\Rogue\Monster\'
.TH CMAIL 1 "8 July 1989" "Local Commands"
.SH NAME
cmail \- check when users last checked their mail
.SH SYNOPSIS
cmail [-v] userlist...
.SH DESCRIPTION
.LP
.I Cmail
is a program for showing when users last checked their mail.
It produces output given the user name, their full name (as
given in the password file) and when they last checked their
mail.  If no system mailbox exists then no date is shown.
.LP
If mail has been added to the mailbox since it was last checked
then
.I cmail
will print
.B "New Mail"
beside the date the user last read their mail.
.LP
The
.I userlist
is a list of restricted regular expressions.  The regular expressions
are compared to the usernames in the password file and any match is
then used.  The restriction on the regular expressions is that the
comparison is only performed at the start of the user name (an
implicit circumflex at the start of the regular expression).
.LP
Note that any command that scans the mailbox of a user will cause
.I cmail
to believe the user has checked their mailbox.  So
.IR from (1)
and
.IR mail (1)
when run will cause cmail to believe all the mail has been
checked.  Programs like the auto-reply daemon of the elm
mailing system will also make cmail believe the mail has been
checked.
.SH OPTIONS
.IP "-v"
Print the version and release date of cmail.
.SH EXAMPLES
.LP
Check users who's usernames start with 'biy'.
.IP
cmail biy
.LP
Check users who's name has a 'd' as the third character.
.IP
cmail ..d
.LP
Check users who's username begins with csa or cca and check
the username advisory.
.IP
cmail -v "c[sc]a" advisory$
.SH AUTHOR
Jonathan@cs.keele.ac.uk
\Rogue\Monster\
else
  echo "Will not over-write ./cmail.1"
fi
if `test ! -s ./cmail.c`
then
echo "Writing ./cmail.c"
cat > ./cmail.c << '\Rogue\Monster\'
	/* Reports on who's got mail to read.

	   Version 1.0
		Initial release

	   Version 1.1
		Fixed manual page

	   Usage:  mailfor [-v] user1 user2 user3 ....

	   Users can be given as regular expressions.
	*/

	/* Options you shouldn't change
	*/

#define REGSIZE		100
#define UNAMESIZE	10
#define FNAMESIZE	25
#define PRINT1		"%-10.10s %-25.25s "

#define VERSION 	"1.1"
#define DATE		"8 July 1989"
#define YEAR		"1989"

	/* Include files
	*/

#include "cmail.h"
#include <pwd.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>

#ifdef TIME
#include <time.h>
#else
#include <sys/time.h>
#endif

#ifdef STRING
#include <string.h>
#else
#include <strings.h>
#endif

#ifdef INDEX
#define strchr(a, b)  index(a, b)
#define strrchr(a, b) rindex(a, b)
#endif

struct passwd *getpwent();	/* These are not declared for SYSVR2 */
void endpwent();		/* so we'll declare them here.       */

	/* Global data declarations
	*/

char *myname;

	/* Now comes the initialisation of the regular expression
	   package.  We use this rather than the regexp(3) routines
	   because then we can hold all the arguments in a compiled
	   form and just do one pass of the password file.
	*/

void regerr(err)
int err;
{	fprintf(stderr, "%s: Regular expresion failure %d\n", myname, err);
	exit(255);
}

#define INIT		register char *sp = instring;
#define GETC()		(*sp++)
#define PEEKC()		(*sp)
#define UNGETC(c)	(--sp)
#define RETURN(c)	return;
#define ERROR(c)	regerr(c)

#include <regexp.h>

	/* Structures to hold the list of users and their
	   full names after a scan of the password file.
	*/

typedef struct userlist
{	char username[UNAMESIZE];	/* Username */
	char fullname[FNAMESIZE];	/* Full name */
	struct userlist *next;		/* Next user in list */
}	USERLIST;

typedef struct userroot
{	char reguser[REGSIZE];		/* User name in regexp format */
	char *argument;			/* Original argument */
	USERLIST *last;			/* Last username in list */
	USERLIST *first;		/* First username in list */
}	USERROOT;

	/* Functions we may want to use later
	*/

USERROOT *build();
USERROOT *buildarray();
void buildlist();
void addtolist();
char *getusername();
void printarray();
void printstat();
void fatal();

char *malloc();
char *calloc();

	/* Now the program
	*/

main(argc, argv)
char *argv[];
int argc;
{	USERROOT *array;

	/* Find out who I am
	*/

	myname=strrchr(*argv, '/');
	if (myname == NULL)
		myname=(*argv);
	else
		++myname;
	++argv;
	--argc;

	/* If they've asked for a version then now is a good
	   time to print it out.
	*/

	if (strcmp(*argv, "-v") == 0)
	{   printf("\n%s Version %s, %s\n(C) Copyright %s Jonathan Knight\n\n",
			myname, VERSION, DATE, YEAR);
	    ++argv;
	    --argc;
	}

	/* Check for the obvious
	*/

	if (argc == 0) exit(0);

	/* Go to the spool directory.
	*/

	if (chdir(MSPOOL))
	    fatal("Can't access mail spool directory", errno);


	/* Create an array of users which match the arguments
	*/

	array=build(argc, argv);

	/* Print out the result
	*/

	printarray(argc, array);

	exit(0);
}

	/* Build a list of users and their full names
	*/

USERROOT *build(argc, argv)
int argc;
char *argv[];
{	USERROOT *array;

	array=buildarray(argc, argv);
	if (array != NULL)
		buildlist(argc, array);
	return(array);
}

	/* This function builds an array of the arguments passed
	   ready for a scan of the password file
	*/

USERROOT *buildarray(argc, argv)
int argc;
char *argv[];
{	USERROOT *array;
	int i;

	array=(USERROOT *)calloc(argc, sizeof(USERROOT));

	if (array == NULL)
		return(NULL);

	/* Array already initialised to 0 by the calloc call
	*/

	for (i=0; i<argc; ++i)
	{   array[i].argument=argv[i];
	    (void)compile(argv[i], array[i].reguser,
				&array[i].reguser[REGSIZE], '\0');
	}

	return(array);
}

	/* This function scans the password file looking for
	   users which match the list of users given.  If they
	   do, then a structure containing the username and their
	   full name is stored.
	*/

void buildlist(argc, array)
int argc;
USERROOT array[];
{	struct passwd *pwent;
	int i;

	while ((pwent=getpwent()) != NULL)
	    for (i=0; i < argc; ++i)
		if (advance(pwent->pw_name, array[i].reguser))
			addtolist(&array[i], pwent);

	endpwent();

	/* Report on any regexp's that didn't match anything
	*/

	for (i=0; i<argc; ++i)
	    if (array[i].first == NULL)
		fprintf(stderr, "No users matched %s\n", array[i].argument);
}

	/* Function to get a username from a GECOS field
	*/

#ifdef FINGG
char *getusername(gecos)
char *gecos;
{	char *c;

	for (c=gecos; (*c != '\0') && (*c != ','); ++c);
	*c='\0';
	return(gecos);
}
#else
#ifdef SYSVG
char *getusername(gecos)
char *gecos;
{	char *s, *e;

	s=strchr(gecos, '-');
	if (s==NULL)
		s=gecos;
	else
		++s;

	e=strrchr(gecos, '(');
	if (e!=NULL)
		*e='\0';

	return(s);
}
#else
#define getusername(gecos) gecos
#endif
#endif

void addtolist(el, pwent)
USERROOT *el;
struct passwd *pwent;
{	USERLIST *new;

	if ((new=(USERLIST *)malloc(sizeof(USERLIST))) == NULL)
		fatal("No memory for user list", errno);

	if (el->first == NULL)
	    el->first = new;
	else
	    el->last->next=new;

	el->last=new;
	new->next = NULL;
	strncpy(new->username, pwent->pw_name, UNAMESIZE);
	strncpy(new->fullname, getusername(pwent->pw_gecos), FNAMESIZE);
}

	/* Now a function to print out all the users we have collected
	*/

void printarray(argc, array)
USERROOT array[];
{	int i;
	USERLIST *p;

	if (array == NULL) return;

	for (i=0; i<argc; ++i)
	if (array[i].first != NULL)
	    for (p=array[i].first; p != NULL; p=p->next)
		printstat(p);
}

	/* Function to stat the mail file and print out some
	   information.
	*/

void printstat(p)
USERLIST *p;
{	struct stat buf;

	printf(PRINT1, p->username, p->fullname);

	if (stat(p->username, &buf) == 0)
	{   printf("%.19s", ctime(&buf.st_atime));
	    if ((buf.st_atime < buf.st_mtime) && (buf.st_size))
		printf("  New Mail");
	}
	putchar('\n');
	fflush(stdout);
}

	/* Function to perform a fatal error
	*/

void fatal(mess, err)
char *mess;
int err;
{	fprintf(stderr, "%s: ", myname);
	perror(mess);
	exit(err);
}

\Rogue\Monster\
else
  echo "Will not over-write ./cmail.c"
fi
if `test ! -s ./cmail.h`
then
echo "Writing ./cmail.h"
cat > ./cmail.h << '\Rogue\Monster\'
/* Options:
*
*SYSVG	- You have a SYSV gecos field in your passwd file "(000)-Name(0000)"
*
*FINGG	- You have a BSD finger type GECOS field "name, office, ext, ..."
*	  (If neither of the above is defined then the GECOS field is assumed
*	   to only contain the user's name)
*
*STRING	- If the file /usr/include/string.h exists.  Otherwise
*	  /usr/include/strings.h is used instead.
*
*TIME	- time.h is in /usr/include and not /usr/include/sys
*
*INDEX	- You have index and rindex rather that strchr and strrchr.
*
*MSPOOL	- Where the system mailboxes are.
*/

#undef	SYSVG
#undef	FINGG
#undef	STRING
#undef	TIME
#define	INDEX
#define	MSPOOL	"/usr/spool/mail"

\Rogue\Monster\
else
  echo "Will not over-write ./cmail.h"
fi
echo "Finished archive 1 of 1"
exit
-- 
  ______    JANET :jonathan@uk.ac.keele.cs     Jonathan Knight,
    /       BITNET:jonathan%cs.kl.ac.uk@ukacrl Department of Computer Science
   / _   __ other :jonathan@cs.keele.ac.uk     University of Keele, Keele,
(_/ (_) / / UUCP  :...!ukc!kl-cs!jonathan      Staffordshire.  ST5 5BG.  U.K.

tchrist@convex.COM (Tom Christiansen) (10/24/89)

In article <1121@kl-cs.UUCP> jonathan@cs.keele.ac.uk (Jonathan Knight) writes:

>Here's a neat little program which detects whether users on your
>local machine have ready their mail recently.  It reports when they
>last checked it and also whether any new mail has arrived since.
>Very useful if your local machine uses userids which are based on
>department or type of user or year of graduation.

Sigh.  Yet another piece of system administrative hackery written in C.
This one took over 300 lines and contains various and sundry data structures
and subroutines.  I don't mean to impune the competence of the original
author.  It *is* a neat little program.  But it seems like terrific overkill
to do this in C.  So as an exercise, I rewrote the program in perl (version
3.0).   It took *substantially* less code: around 15%.  I'll bet it was
faster to write and faster to debug.  I'm sure it'll be faster to modify.
Even if you don't have perl, check out my script here.  I think you'll agree
that it's **MILES** clearer than the corresponding C code.

I wanted to do some timing comparisons, but found that the original program
used SysV's regexp() routines rather than BSD's rexex().  So if anyone else
wants to post timings to alt.sources.d, I'd be interested in seeing them.

Meanwhile, both to demonstrate the marvelousness of perl and also to chase
off the non-source-posting haranguers, here is the code.  I have retained the
basic structure, routine names, and variable names of the original code where
reasonable to do so, perhaps more so than was optimal.  Two comments on the
original C code: doing getpwent()s until you run out is terribly innefficient
on systems with long passwd files; mine is ~1300 lines long, and it would be
better to do readdir()s on SPOOL and then getpwnam()s on the results.
Secondly, the author has what is to me an odd style to his code (comment
placement, argument placement, curley placement), so I ran it through indent
to put it in KNF (kernel normal form) before conversion.  This was purely
aesthetic.

Recall that you will need version 3 of perl.  I include after the cmail.pl
script code for the ctime() function, which is not a perl intrinsic.  The
ctime.pl code was posted to the net some time ago, but I've unfortunately
lost the info on the original author.  I thank him for his code and apologize
for not having provided his proper credit.


--tom

#!/bin/sh
#    This is a shell archive.
#    Run the following text with /bin/sh to extract.

echo x cmail.pl
sed -e 's/^X//' << \EOFMARK > cmail.pl
X#!/usr/local/bin/perl3
X
X$PRINT1		= "%-10.10s %-25.25s ";
X$VERSION 	= "1.2";
X$DATE		= "23 October";
X$YEAR		= "1989";
X$SPOOL		= "/usr/spool/mail";
X
Xdo 'ctime.pl';
X($myname = $0) =~ s%.*/%%;
X
Xif ($ARGV[0] eq "-v") {
X   printf "%s Version %.1f, %s %d; Tom Christiansen <tchrist@convex.com>\n",
X	$myname, $VERSION, $DATE, $YEAR;
X   shift;
X} 
X
Xexit 0 unless $#ARGV > $[-1;
Xchdir $SPOOL || die "$myname: can't access mail spool directory: $!\n";
Xdo build(@ARGV);
Xdo printarray();
Xexit 0;
X
Xsub build {
X    setpwent;
Xpw: while (($name, $passwd, $uid, 
X	    $gid, $quota, $comment,
X	    $gcos, $dir, $shell) = getpwent)
X    {
Xrexpr:  foreach $rexpr ( @_ ) {
X	    next rexpr unless $name =~ /^$rexpr/;
X	    $matched_rexprs{$rexpr}++;
X	    ( $fullname = $gcos ) =~ s/,.*//;
X	    $fullname{$name} = $fullname;
X	    next pw;
X	} 
X    } 
X    endpwent;
X
X    foreach $rexpr ( @_ ) {
X	next if $matched_rexprs{$rexpr};
X	printf stderr "No users matched %s\n", $rexpr;
X    } 
X} 
X
Xsub printarray {
X    foreach $user ( keys %fullname ) {
X	printf $PRINT1, $user, $fullname{$user};
X	if ( -e $user ) {
X	    ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
X	       $atime,$mtime,$ctime,$blksize,$blocks) = stat(_);
X	    printf "%.19s", &ctime($atime);
X	    print "  New Mail" if $atime < $mtime && $size;
X	}
X	print "\n";
X    } 
X} 
EOFMARK
echo x ctime.pl
sed -e 's/^X//' << \EOFMARK > ctime.pl
X@DoW = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
X@MoY = ('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
X
Xsub ctime {
X    local($time) = @_;
X    local($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst);
X    local($date);
X
X    ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst)
X							  = localtime($time);
X    $year += ($year < 70)? 2000: 1900;
X    $date = sprintf("%s %s %2d %2d:%02d:%02d %s %4d\n",
X	      $DoW[$wday], $MoY[$mon], $mday, $hour, $min, $sec, 
X	      $ENV{'TZ'}, $year);
X    return $date;
X}
EOFMARK

    Tom Christiansen                       {uunet,uiucdcs,sun}!convex!tchrist 
    Convex Computer Corporation                            tchrist@convex.COM
		 "EMACS belongs in <sys/errno.h>: Editor too big!"