/*
allmailscan.c   --crb3 02jan02/19may03

based on a perl script of mine with the same name and purpose,
rewritten to C for better speed and smaller memory footprint.

scan through /home/user/Maildir/new, reporting the contents by user
in from/subj summary lines. options for subdir other than /new;
Maildir-base other than /home; explicitly listing all checked users,
not just those with mail waiting.

this program was written, compiled and put to use on a redhat linux 6.2
system. it's probably highly dependent on GNU tooling, particularly
readdir() behavior.

Copyright (C) 2002,2003 C. R. Bryan III (crb3), All Rights Reserved.
This program is released under the terms of the GNU
General Public License, version 2.

History:

v0.01:
    --crb3 02jan02
        initial version. completed under fire.
v0.02:
    --crb3 02jan02
        added malloc'd storage of usernames.
        much faster due to the fewer head-seeks.
        usernames not sorted.
v0.03:
    --crb3 03jan02
        switched to using scandir. faster, sorted, but
        not thread-safe: piping the output to less means a blank result.
v0.04:
    --crb3 03jan02
        reinstate the malloc'd name storage, see if that helps.
        ...it doesn't. we core instead. the core suggests that
        the program segfaulted down in malloc::chunk_free, which
        suggests a double-free. 'less' must use the scandir machinery,
        methinks, and the stuff must not be reentrant.
        i need to do my own sorting instead, in a version based on 0.02.
        0.02 doesn't segfault when piped into 'less'.
v0.05:
    --crb3 03jan02
        remove all mention of gnu's scandir stuff, reinstate
        readdir stuff from 0.02, add an aftermarket pointer array
        and qsort. the compiler warns about arg4 to qsort being
        an incompatible pointer type, but things work; i'm
        probably just not appeasing it properly by sprinkling
        constness around like pixie-dust.
        this version works when piped into less.
        finally put in my BARS for readability.
v0.06:
    --crb3 19jan02
        cobble up a get_swopt section to grab commandline
        switches (I'll study up on gnu's opts system later... Did.
        Don't wanna use it here).
        -m switch for which /Maildir/??? subdir to scan; I use
        mutt locally and keep a lot of list-type mail in /sav.
        compiler still warns about arg4 to qsort, and it still works.
        -a switch to list all names, with-mail or not.
        -h switch for some minimal explanations and immediate exit.
v0.07:
    --crb3 25jan02
        preparing for a templated output line; an immediate result
        is a further speedup, on moving from string-construction to
        pointer-manipulation (...why I love C...) for subject-line
        padding.
        the printfs pad the beginning of a string, not the end, so
        specifying precision can't help us here.
        the qsort complaint is still there.
        i'll probably move string-lengths into a struct, and pass
        that around by reference, to avoid my old DOS habit of
        lotsa-globals.

v0.08:
    --crb3 16may03
        add -b for basedir. default is /home, but /var/vpopmail/users
        is now a needed option.

v0.09:
    --crb3 17may03
        clean out // commenting. PATH_MAX system define now in use.
        applied a Benchmark script to test concatenations speed:
        concatenations are faster (36s vs. 39s over a 1000-loop), over
        replacing two concatenating sequences with snprintf(). so
        interpreted languages really _are_ slow...

v0.10:
    --crb3 19may03/20dec03
        sort filenames as well as usernames.
        clean up some concatenations (sprintf is measurably faster).
        now we have two qsort complaints. the answer to both is
        probably a matter of casting (couched the right way).

v0.11:
    --crb3 21dec03
        -u arg for sole user to scan.

todo:
- find the sweetening that shuts up the qsort warnings.
- investigate D_TYPE on Linux.
later options:
- which-subMaildir option 'all', for new+cur
- list all older/newer than a specified date
- HTML output by merging with a template
- more flexible output-line formatting (working on it)
when all these options are in, I'll call it v1.0.
what's here now works.
*/

#define VERSION "v0.11"
#define MAXPATH (PATH_MAX)  /* found an appropriate GNU define for this */
#define MAXLINE (1024)      /*  big enough in linux. YMMV in other OS   */
#define MALSIZE (4096)      /* a convenient malloc size                 */
#define MAXUSR  (128)       /* arbitrary limit on username size         */

//#define USEDTYPE          /* do d_type encodings work on this system? */

//#define DEBUG

#include <unistd.h>
#include <dirent.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>

typedef enum {
  er_noerr=0,
  er_noperm,
  er_nodir,
  er_nomail,
  er_nomal,
  er_noopen,
  er_norealloc,
  er_nopmalloc,
  er_norpmalloc
} erexcode;

int isblank(char *s);
void chomp(char *s);
void prmsgline(char *infile);
char *uc(char *s);
char *lc(char *s);

/*
currently, we get two warnings from the compiler about passing
qsort a pointer to the function prototyped here...
*/

int strpcmp(const char **a,const char **b);

/*
...what qsort wants is:
  int (*compar)(const void *, const void *)
but the code works reliably. i'm still looking for some way to
pacify the compiler's prototype-checker on this. we need this
code to move fast, so sweetening by passing through autovars
is not an option.

*/

int per_dir(char *maildir,int allnames,char *uname);
void free_perdir(void);

/*
at-exit functions, uptop where i usually put such so they're
isolated and self-prototyping... and the pointers they need to see.
I *almost* got away with no globals...
*/
char *namp;     /* malloc'd pointer, must be visible to both    */
char **sortp;   /* malloc'd pointer farm, ditto                 */

void exit_freenamp(void){ if(namp!=NULL) free(namp); }
void exit_freesortp(void){ if(sortp!=NULL) free(sortp); }

/****************************************************************/

int main(int argc,char* argv[])
{
  char whatmail[33]="new";
  char buf[16];
  char athome[MAXPATH+2]="/home";
  DIR *dhome;
  struct dirent *dentry;
  char name[MAXPATH+2];
  char soleusr[MAXUSR+2]="\0";
  char maildir[MAXPATH+2];
  int nsize, retcode;
  int namspc,mallocated,maldx;
  int namesct,nameslpct;
  char *npt,*npre;
  char **sortpa;
  int b,c;  /* oh, i come from CP/M with a djnz on my knee... o/~   */
  char *p;  /* and i spent some time in MCU's, don't you cry for me */
  int ostate=0,allnames=0;

    if(argc>1){
        for(b=1;b<argc;b++){
            p=argv[b];
            if(*p++=='-'){
                c=*p++;
                if(strchr("=:",*p) && *p)  p++;
                switch( tolower(c)){    /* decode and catch snugged arg */
         case 'm':                      /* which Maildir/subdir to scan */
                    ostate='m';
                    if(!*p)  continue;
                    break;
         case 'a':
                    if(!*p){
                        allnames ^= 1;
                        continue;
                    }else{
                        if(*p=='-')  allnames=0;
                        if(*p=='+')  allnames=1;
                    }
                    break;
         case 'b':
                    ostate='b';
                    if(!*p)  continue;
                    break;

         case 'u':
                    ostate='u';
                    if(!*p)  continue;
                    break;

         case 'h':
                    puts("allmailscan -a -bBDIR -mDIR -h\n"
                        VERSION
                        " --crb3 19jan02/17may03\n"
                        "list all qmail /Maildir/new contents\n"
                        "-a: list all usernames, with or without mail\n"
                        "-b: use basedir BDIR. default: /home\n"
                        "-m: list subdir DIR. default: new\n"
                        "-h: this help\n");
                    exit(0);
                    break;
         default:
                    printf("unknown option %c ignored.\n",ostate);
                    break;

                }
            }else{
                p--;
            }
            if(ostate){                 /* grab snugged or spaced arg   */
                switch(ostate){
         case 'm':
                    strncpy(whatmail,p,32);
                    whatmail[32]='\0';
                    break;

         case 'b':
                    strncpy(athome,p,MAXPATH);
                    athome[MAXPATH]='\0';
                    break;

         case 'u':
                    strncpy(soleusr,p,MAXUSR);
                    soleusr[MAXUSR]='\0';
                    break;

         default:
                    printf("unknown option-state %c ignored.\n",ostate);
                    break;
                }
            }
            /* put else{ ..process tail args } here */
            ostate=0;
        }
    }

/*
generate a showoff copy of the specific maildir(s) we'll scan.
*/
    strcpy(buf,whatmail);
    uc(buf);

    if(soleusr[0]==0){

/*
grab some bufferspace to hold a loose array of usernames gotten
from the /home directory.
*/
        if( (namp=malloc(MALSIZE))==NULL){
            puts("can't malloc array space.");
            exit(er_nomal);
        }
        npt=namp; namspc=0; mallocated=MALSIZE;
        atexit(exit_freenamp);

/*
now scoop up those names, in as-found order.
*/

        if( (dhome=opendir(athome))==NULL){
            printf("can't open DIR %s\n",athome);
            exit(er_noopen);
        }

/*
we scoop the names out of glib's secret struct now so we can
sort them, to minimize HD head-seeks, and to get the list safely
out of reach of a nonreentrant function before anybody else in
our neighborhood can use it and clobber our list.
gnu's scandir was tried, and it's neato-peachy-keen, but it
segfaulted in malloc's free on linux2.2 when allmailscan's
output was piped to gnu's less. in this case, no gnus was good
gnus.

POSIX/portability: the GNU docs warn that the dirent.d_type struct
element isn't universal among POSIX systems. check your system
for availability, comment off if not found.
(used here and in per_dir).
*/

        namesct=0;
        while( (dentry=readdir(dhome))!=NULL){  /* ignore dotnames  */
        if(!strcmp(dentry->d_name,".") || !strcmp(dentry->d_name,".."))
            continue;
#ifdef USEDTYPE
        if(dentry->d_type != DT_DIR)            /* ignore files     */
            continue;
#endif
        strcpy(name,dentry->d_name);
        if( (namspc+ (nsize=strlen(name))+1) >= mallocated){
            if( (npre=realloc(namp,mallocated+MALSIZE))==NULL){
                puts("can't realloc enough array space.");
                    exit(er_norealloc);
                }
                maldx=(int)(npt-namp);
                namp=npre; npt=namp+maldx;
                mallocated += MALSIZE;
            }
            strcpy(npt,name);
            npt += nsize+1;
            namspc += nsize+1;
            namesct++;
        }
        *npt=0;             /* an extra null for a cap...needed?    */

        closedir(dhome);    /* done with you; give back the struct. */


/*
now build an array of pointers to the name strings so we can
sort with them. no resize needed: we already know how many there
are.
*/

        if( (sortp=calloc(namesct,sizeof(char*)))==NULL){
            puts("can't malloc pointer-array space.");
            exit(er_nopmalloc);
        }
        atexit(exit_freesortp);
        for(sortpa=sortp,npt=namp,nameslpct=0;nameslpct<namesct;nameslpct++){
            sortp[nameslpct]=npt;
            npt+=(strlen(npt)+1);
        }

        qsort(sortp,namesct,sizeof(char*),strpcmp);

    }

/*
i'd rather not have this tool do silent-running. It's simple
enough to excise this header line from a capture, and I want the
explicit statement of the subdir being scanned... so, we always
emit this line.
*/

    printf("%s MAIL SUMMARY\n",buf);

/*
now we don't have to refer back to the /home dir, our live list
is sorted, and we're ready to roll, peeking into maildirs. if a
name doesn't have a maildir, we skip it. if you want to be
qmail-rigorous, you can stick in a test for homedir ownership.
i run a small system where that might conceal things I need to
notice, particularly in special-purpose maildirs owned by root.
*/
    if(soleusr[0]==0){
        for(nameslpct=0;nameslpct<namesct;nameslpct++){

            strcpy(name,sortp[nameslpct]);
            sprintf(maildir,"%s/%s/Maildir/%s",athome,name,whatmail);
            switch( (retcode=per_dir(maildir,allnames,name)) )
            {
       case er_noerr:
       case er_nomail:
       case er_noperm:
                continue;       /* no maildir here or no permission */

       case er_nomal:
       case er_norealloc:
       case er_noopen:
       case er_nopmalloc:
       case er_norpmalloc:
                exit(retcode);

            }
        }
    }else{
        sprintf(maildir,"%s/%s/Maildir/%s",athome,soleusr,whatmail);
        retcode=per_dir(maildir,allnames,soleusr);
    }
    exit(er_noerr);
}

/***************************************************************>>
per_dir.
common-sense breakout of per-Maildir loop functionality into a
separate function, for clarity.
takes a Maildir-path string, plus a pointers to a "first-time"
flag and a mode switch.

opens that /dir, sorts the contents, shows the username if
appropriate, then invokes prmsgline on each file in the list.

the malloc'd storage for the sorting is obtained once, the first
time in, and reused, realloc'd as required. matching stump routine
free_perdir() must be called to free that memory after the last
call into per_dir. (GNU/Linux does not respond gracefully when
we malloc-and-release on a per-dir basis, so we have to use the
statics.) we discard the internal structure and rebuild on every
invocation, though, keeping just the allocations.
<<***************************************************************/

static char *fnmalpt=NULL;  /* ptr to malloc'd filename space   */
static char *fnampt=NULL;   /* working ptr within that space    */
static char **dfsortp=NULL; /* ptr to malloc'd sort-ptr space   */
static int dfmallocated=0;  /* current size of fnmalpt alloc    */
static int dpmallocated=0;  /* current size of dfsortp alloc    */

int per_dir(char *maildir,int allnames,char *uname)
{
  char fname[MAXPATH+2]="\0";
  char name[MAXPATH+2]="\0";
  char *dfnpre;
  char **dfsortpa;
  DIR *dfhome;
  struct dirent *dentry;
  int fnsiz,fnamesct,dfnameslpct,dfmaldx;
  int dpneeded;
  int dfnamsiz=0;
  int mfirst=1;
#ifdef DEBUG
  char *dbgpt;
  int dbgb;
#endif

    if( (dfhome=opendir(maildir))==NULL)
        return(er_noerr);   /* no maildir here or no permission */

    if(allnames){   /* show it here in case we come up empty    */
        printf("%s:\n",uname);
        mfirst=0;           /* already shown, don't do it again */
    }

/*
more tests on /home/???/maildir/? not in my system. we're read-only,
running at root if we're seeing all.
*/

/*
grab some bufferspace to hold a loose array of filenames gotten
from the /maildir/whatever directory.
*/
    if(fnmalpt==NULL){      /* first time in? grab some DRAM.   */
        if( (fnmalpt=malloc(MALSIZE))==NULL){           // nampt
            puts("can't malloc file array space.");
            return(er_nomal);
        }
        dfmallocated=MALSIZE;       /* how much is allocated?   */

        atexit(free_perdir);
    }

/*
we scoop the names out of glib's secret struct now so we can
sort them, to minimize HD head-seeks, and to get the list safely
out of reach of a nonreentrant function before anybody else in
our neighborhood can use it and clobber our list.

again, the dirent.d_type struct-element isn't guaranteed across
all of POSIX. This code was written, compiled and used on RHL6.2.
*/

    fnamesct=0;
    fnampt=fnmalpt;
    dfnamsiz=0;
    while( (dentry=readdir(dfhome))!=NULL){
        strncpy(fname,dentry->d_name,MAXPATH);
        fname[MAXPATH]=0;
        if((!strcmp(fname,".")) || (!strcmp(fname,"..")))
            continue;                       /* ignore dotnames  */

#ifdef USEDTYPE
        if(dentry->d_type != DT_REG)        /* ignore subdirs   */
            continue;
#endif
        fnsiz=strlen(fname);

/* need some more space? ask for it, in MALSIZE chunks. */

        if( dfnamsiz+(fnsiz+1) >= dfmallocated){
            if( (dfnpre=realloc(fnmalpt,dfmallocated+MALSIZE))==NULL){
                puts("can't realloc enough file array space.");
                return(er_norealloc);
            }
#ifdef DEBUG
            printf("reallocated: %d bytes\n",dfmallocated+MALSIZE);
#endif
/* regen pointers after a realloc, based on new val in dfnpre   */

            dfmaldx=(int)(fnampt-fnmalpt);
            fnmalpt=dfnpre;
            fnampt=fnmalpt+dfmaldx;
            dfmallocated += MALSIZE;
        }
        strcpy(fnampt,fname);
        dfnamsiz += (fnsiz+1);
        fnampt += (fnsiz+1);
        fnamesct++;
    }
    if(!fnamesct)           /* nothing found    */
        return(er_nomail);
    *fnampt=0;             /* an extra null for a cap...needed? */

#ifdef DEBUG
    printf("--fname array built: %d names, %d bytes\n",fnamesct,dfnamsiz);
    for(dbgb=0,dbgpt=fnmalpt;dbgb<fnamesct;dbgb++){
        printf("- %s\n",dbgpt);
        dbgpt += (strlen(dbgpt)+1);
    }
#endif

/*
now build an array of pointers to the name strings so we can
sort with them. resizing might be needed, once per_dir, after
which we freshly populate the array and run the sort.
*/

    dpneeded= ( (fnamesct * sizeof(char *) / MALSIZE) +
         (  ( (fnamesct * sizeof(char *)) % MALSIZE) ? MALSIZE : 0 ));

    if(dfsortp==NULL){
        if( (dfsortp=malloc(dpneeded))==NULL){
            puts("can't malloc file pointer-array space.");
            return(er_nopmalloc);
        }
        dpmallocated=dpneeded;
    }else{
        if( dpmallocated < dpneeded ){
            if( (dfsortp=realloc(dfsortp,dpneeded))==NULL){
                puts("can't realloc file pointer-array space.");
                return(er_norpmalloc);
            }
            dpmallocated=dpneeded;
        }
    }
    for(dfsortpa=dfsortp,fnampt=fnmalpt,dfnameslpct=0;
            dfnameslpct<fnamesct;dfnameslpct++){
        *dfsortpa=fnampt;
        dfsortpa++;
        fnampt+=(strlen(fnampt)+1);
    }

#ifdef DEBUG
    puts("--fname sort array built");
#endif

    qsort(dfsortp,fnamesct,sizeof(char*),strpcmp);

#ifdef DEBUG
    puts("--fname array qsorted");
#endif

/*
now spit out pretty lines
*/

    for(dfnameslpct=0;dfnameslpct<fnamesct;dfnameslpct++){

        strcpy(name,dfsortp[dfnameslpct]);
        if(mfirst){
            printf("%s:\n",uname); mfirst=0;
        }
        sprintf(fname,"%s/%s",maildir,name);
        prmsgline(fname);
    }

    return(er_noerr);



}

/***************************************************************>>
free_perdir.
at-exit function: free up mallocated memory used in per-dir
filename-sorting operations.
<<***************************************************************/

void free_perdir(void)
{
    if(fnmalpt != NULL)
        free(fnmalpt);
    if(dfsortp != NULL)
        free(dfsortp);
}

/***************************************************************>>
strpcmp.
wrapper for strcmp to work with an additional layer of indirection,
to work with qsort in working over a malloc'd array of pointers-
to-strings.
takes a pair of pointers-to-pointers, returns strcmp's integer
result.
<<***************************************************************/

int strpcmp(const char **a,const char **b)
{
    return(strcmp( *a, *b));
}


/***************************************************************>>
prmsgline.
given a (qmail Maildir message) filename, open it long enough
to read its headers and report a summary-line to stdout.
right now its parameters are all hardwired. there's much room
for commandline-driven format adjustment to be hacked in; this
is what works at my admin console.
--crb3 21dec03: Template line tmpt becomes a static to support it.
<<***************************************************************/

char tmpt[MAXLINE+2]=" %s%s%s%s%s\n";

void prmsgline(char *infile)
{
  int gotf=0,gots=0,gotd=0;
  int b,padlen;
  int fromlen=42;
  char inpline[MAXLINE+2];
  char *p,*pa,*pi;
  char inod[MAXLINE+2];
  char separ[]=": ";
  static char pad[MAXLINE+2]="\0";
  char from[MAXLINE+2]="\0",subj[MAXLINE+2]="\0",date[MAXLINE+2]="\0";

  FILE *IFIL;

    if(!pad[0]){            /* build a space-pad on first visit */
        for(b=0,p=pad;b<(MAXLINE);b++)
            *p++=' ';
        *p=0;
    }

    if( (IFIL=fopen(infile,"r"))==NULL){
      printf("can't open infile %s\n",infile);
      return;
    }

/*
now we can whack that name down to an identifying number.
can't modify the fname we're given, though, so gotta copy.
*/

    strncpy(inod,infile,MAXLINE);
    if( (pi=strchr(inod,'.'))!=NULL)
        *pi=0;

    if( (pi=strrchr(inod,'/'))==NULL)
        pi=inod;
    else
        pi++;

//  from[0]=subj[0]=date[0]=gotd=gotf=gots=0;
    while( (p=fgets(inpline,MAXLINE,IFIL))!=NULL
                    && !(gots & gotf & gotd)    /* quit when these are in   */
                    && !isblank(p) ){           /* or at end of headers     */
        chomp(inpline);

        if( (pa=strchr(inpline,':'))==NULL) continue;
        *pa++=0;
        lc(p);
        while(*p==' ' || *p=='\t') p++;
        if(strcmp(p,"from")==0){

            strncpy(from,pa,MAXLINE);
            from[MAXLINE]=0;
            from[fromlen]=0;
            gotf=1;

        }else if(strcmp(p,"subject")==0){
            strncpy(subj,pa,MAXLINE);
            subj[MAXLINE]=0;
            gots=1;
        }else if(strcmp(p,"date")==0){
            strncpy(date,pa,MAXLINE);
            date[MAXLINE]=0;
            gotd=1;
        }
    }
    fclose(IFIL);
    if( (padlen = fromlen-strlen(from)) <0)  padlen=0;
//  if(padlen<0)  padlen=0;
    printf(tmpt,pi,from,&pad[MAXLINE-padlen],separ,subj);
}

/***************************************************************>>
chomp.
replication of a perl function.
get rid of EOL characters at the end of a string by nulling out
the bytes. handles any quantity and combination of \r,\n.
this function modifies the target of its argument.
void return, so don't try to assign it to an lvalue.
<<***************************************************************/

void chomp(char *s)
{
  char *pp;

    pp=strchr(s,'\0')-1;
    while((*pp=='\n' || *pp=='\r') && pp >= s )
        *(pp--)=0;
}

/***************************************************************>>
isblank.
C expression of a favorite Perl regex test, =~ /^\s*$/.
returns true if the pointed string has no printable chars in it.
<<***************************************************************/

int isblank(char *s)
{
  char *pp=s;

    while(*pp==' ' || *pp=='\t') pp++;
    if(*pp==0return(1);
    else return(0);
}

/***************************************************************>>
uc.
NOT a pure replication of the perl function. this one modifies its
target, uppercasing the pointed string, returning the pointer it
was issued.
<<***************************************************************/

char *uc(char *s)
{
  char *p;

    for(p=s;*p;p++){
        *p = toupper(*p);
    }
    return(s);
}

/***************************************************************>>
lc.
NOT a pure replication of the perl function. this one modifies its
target, lowercasing the pointed string, returning the pointer it
was issued.
<<***************************************************************/

char *lc(char *s)
{
  char *p;

    for(p=s;*p;p++){
        *p = tolower(*p);
    }
    return(s);
}

/***************************************************************>>
later:
output template...
compose a printf template string from input args.
that template can have HTML tags in it too.

n = numeric prefix: timestamp.pid_delivery
f = from
s = subj
d = date
t = to
c = cc

For speed, those args must become statics so that a first-pass
block can build a static array of pointers to push from the
user-provided template. And yes, we are talking about a big
speed hit here, as well as additional field-catching code.


speed-checking:

#!/usr/bin/perl -w
#
# benchmarker
#
use Benchmark;

die "$0 loops command-to-run...\n" unless defined $ARGV[1];
$loopct = shift(@ARGV);
$cmd = join(' ',(@ARGV));

sub this {
`$cmd`;
}

timethis ( $loopct, "this" );


<<***************************************************************/
/***************************************************************>>
<<***************************************************************/

/*****************************[eof]******************************/

Grab the
tarball
here
 
Syntax highlighting using Syntax::Highlight::Engine::Kate