|
/* 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==0) return(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 |