/// /// \file newmail.cc /// C++ version of the scripts: buildcache.sh, updatecache.sh, /// mailstats.sh, and most importantly, newmail.sh // // Copyright 2005-2006, Chris Frey. To God be the glory. // License: GPLv2 // #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; // FIXME - put these settings in .newmailrc? #define DEFAULT_MAIL_DIR "/var/spool/mail" // get the username from uid? #define MAIN_MAIL_DIR "Mail" // relative to home dir typedef map grace_map_type; typedef vector mbox_container_type; enum ModeType { Display, Update }; typedef unsigned int FlagType; const unsigned int Normal = 0x00; const unsigned int Plain = 0x01; const unsigned int Verbose = 0x02; const unsigned int PrintZero = 0x04; const unsigned int PrintTotal = 0x08; const unsigned int PrintLatestNew = 0x10; const unsigned int ReadOnly = 0x20; const unsigned int PrintDetails = 0x40; const unsigned int HideBulk = 0x80; const unsigned int BuildCache = 0x100; bool NewMessagesFlag = false; /////////////////////////////////////////////////////////////////////////////// // LoginInfo // // LoginInfo // /// Represents a single entry in /etc/passwd. This class uses all thread- /// safe functions to access passwd data. /// /// Todo: this class is similar to the getpwent_r() functions, where you /// loop through all entries in /etc/passwd. Ponder how to do that someday. /// class LoginInfo { struct passwd p; char *buf; int size; void load_uid(uid_t u); // no copying for now LoginInfo(const LoginInfo &li); const LoginInfo& operator= (const LoginInfo &li); public: // exception class class load_error : public std::runtime_error { public: load_error() : std::runtime_error("unable to load pw entry") {} }; public: LoginInfo(); // loads info of current uid explicit LoginInfo(const char *name); // loads info of given username explicit LoginInfo(uid_t uid); // loads info of given uid ~LoginInfo(); // data access const char * name() const { return p.pw_name; } const char * password() const { return p.pw_passwd; } uid_t uid() const { return p.pw_uid; } gid_t gid() const { return p.pw_gid; } const char * real_name() const { return p.pw_gecos; } const char * home_dir() const { return p.pw_dir; } const char * shell() const { return p.pw_shell; } }; void LoginInfo::load_uid(uid_t uid) { struct passwd *pp; if( getpwuid_r(uid, &p, buf, size, &pp) != 0 ) { delete [] buf; throw std::runtime_error("error with getpwnam_r()"); } } #define LI_BUF_SIZE 1024 LoginInfo::LoginInfo() : buf(new char [LI_BUF_SIZE]), size(LI_BUF_SIZE) { load_uid(getuid()); } LoginInfo::LoginInfo(const char *name) : buf(new char [LI_BUF_SIZE]), size(LI_BUF_SIZE) { struct passwd *pp; if( getpwnam_r(name, &p, buf, size, &pp) != 0 ) { delete [] buf; throw std::runtime_error("error with getpwnam_r()"); } } LoginInfo::LoginInfo(uid_t uid) : buf(new char [LI_BUF_SIZE]), size(LI_BUF_SIZE) { load_uid(uid); } LoginInfo::~LoginInfo() { delete [] buf; } /////////////////////////////////////////////////////////////////////////////// // directory_iterator class directory_iterator { ::DIR *dir; ::dirent *current; void end_check(); public: directory_iterator(); // default ctor provides "end" directory_iterator(const std::string &directory); ~directory_iterator(); directory_iterator& operator++ (); // prefix directory_iterator operator++ (int); // postfix ::dirent& operator* (); ::dirent* operator-> (); // comparison bool operator== (const directory_iterator &other) const; bool operator!= (const directory_iterator &other) const { return !operator==(other); } }; directory_iterator::directory_iterator() : dir(0), current(0) { } directory_iterator::directory_iterator(const std::string &directory) { dir = opendir(directory.c_str()); if( !dir ) { // fixme - use an errno exception here! throw runtime_error("error with opendir()"); } current = readdir(dir); } directory_iterator::~directory_iterator() { if( dir ) closedir(dir); } // prefix directory_iterator& directory_iterator::operator++ () { end_check(); current = readdir(dir); return *this; } ::dirent& directory_iterator::operator* () { end_check(); return *current; } ::dirent* directory_iterator::operator-> () { end_check(); return current; } bool directory_iterator::operator== (const directory_iterator &other) const { return current == other.current; } void directory_iterator::end_check() { if( !dir || !current ) throw runtime_error("dereference or use of end diter"); } /////////////////////////////////////////////////////////////////////////////// // Utility functions void Usage(); bool changed(int fd1, int fd2, time_t grace_min = 0); bool changed(const char *fd1, const char *fd2, time_t grace_min = 0); void find_mboxes(const string &dir, vector &mboxes); void check_mbox(ostream &os, ostream &status, int grace_min, const string &dir, const string &mbox, FlagType flag, int &TotalTotal, int &NewTotal); void update_mbox_cache(const string &mboxfile, const string &cachefile, int &Total, int &New, FlagType flag); void Usage() { cerr << "newmail - mail status checker\n" "-----------------------------\n" "Usage: newmail [options] [mboxnames...]\n" " -b build cache: overwrites all cache files with new totals\n" " whether needed or not\n" " -c return command line value on status of new mail\n" " returns 0 on new mail, 1 on no new mail\n" " -d print From/Subject header details for all new messages\n" " since last run (the mboxes with asterisks)\n" " (not yet implemented)\n" " -h this help message\n" " -H hide all mailboxes with -2 in .newmailrc\n" " -S show all mailboxes with -2 in .newmailrc (default)\n" " -n new mode: print only mbox names with new mail since cache\n" //hmmmmm, add something here to sort in filetime order, like ls -ltr Mail/, //but display normally " -p plain mode: print only mbox names with new mail, no headings\n" " -r read-only: don't update cache when in display mode\n" " (has no effect in build or update modes)\n" " -t print grand totals\n" " -u update cache only: update as normal, but skip any output\n" " -v verbose: show all mailboxes, ignoring .newmailrc filters\n" " -z show mboxes with 0 new messages\n" "\n" "If an mboxname is specified, only that mbox is displayed and updated\n" "according to your command line options.\n" "\n" "You can set $NEWMAILOPTS to default combinations of these options\n" "to avoid typing them each time\n" << endl; } // // changed - special mbox and cache file timestamp comparison function // // Return true if file 1 exists and 2 doesn't. // Return false in all other cases // Return true if file 1 was changed more recently than file 2, // using mbox logic below. // bool changed(int fd1, int fd2, time_t grace_min) { struct stat s1, s2; // convert minutes to seconds grace_min *= 60; if( fstat(fd1, &s1) == -1 ) return false; if( fstat(fd2, &s2) == -1 ) return true; // check whether the mailbox has just been accessed by // mutt... if so, the mtime will be less than ctime if( s1.st_mtime < s1.st_ctime ) { // user just accessed the mailbox, so ignore grace_min // for just this round if( s1.st_ctime > s2.st_ctime ) return true; } else { // mailbox was last updated by incoming mail, use grace_min if( s1.st_ctime > (s2.st_ctime + grace_min) ) return true; } return false; } // // changed - special mbox and cache file timestamp comparison function // // Return true if file 1 exists and 2 doesn't. // Return false in all other cases // Return true if file 1 was changed more recently than file 2, // using mbox logic below. // bool changed(const char *fd1, const char *fd2, time_t grace_min) { struct stat s1, s2; // convert minutes to seconds grace_min *= 60; if( stat(fd1, &s1) == -1 ) return false; if( stat(fd2, &s2) == -1 ) return true; // check whether the mailbox has just been accessed by // mutt... if so, the mtime will be less than ctime if( s1.st_mtime < s1.st_ctime ) { // user just accessed the mailbox, so ignore grace_min // for just this round if( s1.st_ctime > s2.st_ctime ) return true; } else { // mailbox was last updated by incoming mail, use grace_min if( s1.st_ctime > (s2.st_ctime + grace_min) ) return true; } return false; } // // find_mboxes - finds all files in the given directory, appending // the filenames (not full paths) to the mboxes container // void find_mboxes(const string &dir, vector &mboxes) { directory_iterator end, di(dir); for( ; di != end; ++di ) { if( di->d_type & DT_REG ) { mboxes.push_back(string(di->d_name)); } } } void check_mbox(ostream &os, ostream &status, int grace_min, const string &dir, const string &mbox, FlagType flag, int &TotalTotal, int &NewTotal) { if( grace_min == -1 ) // -1 means skip entirely return; if( grace_min == -2 && (flag & HideBulk) ) // -2 means hide if requested return; string mboxfile = dir + "/" + mbox; string cachefile = dir + "/cache/" + mbox; int Total, New; int CachedTotal = 0, CachedNew = 0; ifstream cache(cachefile.c_str()); if( cache ) { cache >> CachedTotal >> CachedNew; } cache.close(); bool Changed = changed(mboxfile.c_str(), cachefile.c_str(), grace_min); if( Changed ) { // calculate new total and update if( !(flag & Plain) ) status << mbox << " \r" << flush; update_mbox_cache(mboxfile, cachefile, Total, New, flag); } else { Total = CachedTotal; New = CachedNew; } if( ((flag & PrintLatestNew) && Changed && New > CachedNew) || ((flag & PrintLatestNew) == 0 && New) || (flag & PrintZero) ) { TotalTotal += Total; NewTotal += New; if( flag & Plain ) { os << mbox << "\n"; } else { bool NewFlag = Changed && New > CachedNew; os << setw(6) << Total << setw(8) << New << " " << (NewFlag ? '*' : ' ') << mbox; if( NewFlag ) os << " (" << (New - CachedNew) << ')'; os << "\n"; if( NewFlag ) NewMessagesFlag = true; } } } inline bool FromLine(const char *line, int size) { // Simulating: "^From .*:[0-9][0-9] [0-9]*$" if( size < 8 || strncmp(line, "From ", 5) != 0 ) return false; const char *e = line + size - 8; return *e == ':' && (++e, isdigit(*e)) && (++e, isdigit(*e)) && *++e == ' ' && (++e, isdigit(*e)) && (++e, isdigit(*e)) && (++e, isdigit(*e)) && (++e, isdigit(*e)); } inline bool StatusLine(const char *line, int size) { // Simulating: "^Status: [A-Z]*$" // only check short lines: "Status: RO" is the normal case if( size > 14 ) return false; if( strncmp(line, "Status: ", 8) == 0 ) { const char *b = line + 8; while( *b && *b >= 'A' && *b <= 'Z' ) ++b; return *b == 0; } else { return false; } /* const char *b = line; if( *b == 'S' && *++b == 't' && *++b == 'a' && *++b == 't' && *++b == 'u' && *++b == 's' && *++b == ':' && *++b == ' ' ) { ++b; while( *b && *b >= 'A' && *b <= 'Z' ) ++b; return *b == 0; } else { return false; } */ } inline bool SubjectLine(const char *line, int size) { // Simulating: "^Subject: " return strncmp(line, "Subject: ", 9) == 0; } // // In the cache-miss case, this function chews up the majority // of CPU time, so code carefully. // void update_mbox_cache(const string &mboxfile, const string &cachefile, int &Total, int &New, FlagType flag) { Total = New = 0; int Status = 0; ifstream mbox(mboxfile.c_str()); if( !mbox ) return; char line[8192]; while( mbox ) { // // We use pure char* strings here to avoid the performance // hit of std::string, so we can match the efficiency of // the following egrep line: // // egrep "^(From .*:[0-9][0-9] [0-9]*|Status: [A-Z]*)$" // Mail/* > /dev/null // // The interesting thing is that on my test data, the above // line takes 11 seconds of user time, but if I only test // the the "From " part, it takes 2 seconds of use time. (!) // Therefore it would in theory take less time to run // egrep twice than once. (!) // // How do I get similar efficiency here??? It's a puzzle. // // Using getline(istream&, std::string&) is too slow. // mbox.getline(line, sizeof(line)); int size = strlen(line); assert((size_t)size < sizeof(line)); if( FromLine(line, size) ) { Total++; } else if( StatusLine(line, size) ) { Status++; } if( mbox.fail() && !mbox.eof() ) { // failbit is set by getline() when it there is too // much data for the buffer and it can't find the // delimiter... this provides a nice way to // get to the end of the line when we need it while( mbox.fail() && !mbox.eof() ) { mbox.clear(); mbox.getline(line, sizeof(line)); } } } New = Total - Status; // update the cache file if( !(flag & ReadOnly) ) { ofstream cache(cachefile.c_str()); cache << Total << " " << New << endl; } } /////////////////////////////////////////////////////////////////////////////// // various modes of operation void display_mode(const string &maildir, const mbox_container_type &mboxes, grace_map_type &grace); void update_mode(const string &maildir, const mbox_container_type &mboxes, grace_map_type &grace, FlagType flag); void display_mode(const string &maildir, const mbox_container_type &mboxes, grace_map_type &grace, FlagType flag) { if( !(flag & Plain) ) { cout << " Total New Mbox " " \n"; // << endl; cout << "------ ------ -----------------------------" "----------------- \n"; // << endl; } int Total = 0, New = 0; mbox_container_type::const_iterator b = mboxes.begin(), e = mboxes.end(); for( ; b != e; ++b ) { check_mbox(cout, cerr, grace[*b], maildir, *b, flag, Total, New); } if( flag & PrintTotal ) { cout << "------ ------\n" << setw(6) << Total << setw(8) << New << '\n'; } } void update_mode(const string &maildir, const mbox_container_type &mboxes, grace_map_type &grace, FlagType flag) { string mboxbase = maildir + "/"; string cachebase = maildir + "/cache/"; string mboxfile, cachefile; int Total, New; mbox_container_type::const_iterator b = mboxes.begin(), e = mboxes.end(); for( ; b != e; ++b ) { mboxfile = mboxbase + *b; cachefile = cachebase + *b; if( grace[*b] != -1 || (flag & BuildCache) ) { if( (flag & BuildCache) || changed(mboxfile.c_str(), cachefile.c_str(), grace[*b]) ) { update_mbox_cache(mboxfile, cachefile, Total, New, 0); } } } } // // env_getopt // // Behaves like getopt(), but prepends any command line options found // in the specified environment variable. // // Only processes the argument list once. I.e. one while() getopt loop // only. // int env_getopt(const char *ename, int *argc, char **argv[], const char *optstring) { static bool first = true; static int eargc = 0; static char *eargv[200]; static const int eargvSize = 200; static char *eval = 0; assert( *argc >= 1 ); if( first ) { first = false; // first argument is always the program name eargv[0] = (*argv)[0]; eargc = 1; // check for environment extras char *value = getenv(ename); if( value != NULL ) { eval = new char[strlen(value) + 1]; // never freed strcpy(eval, value); // parse into space separated values, // adding to eargv eargv[eargc++] = eval; while( *eval && eargc < eargvSize) { if( *eval == ' ' ) { *eval = 0; ++eval; // add next eargv[eargc++] = eval; } else { ++eval; } } } // now copy argv pointers to eargv for( int i = 1; i < *argc; i++ ) { eargv[eargc++] = (*argv)[i]; } // set argv to point to our new eargv *argc = eargc; *argv = eargv; /* // display all args for debugging cout << "args: " << endl; for( int i = 0; i < eargc; i++ ) { cout << i << ": " << eargv[i] << endl; } */ } return getopt(eargc, eargv, optstring); } /////////////////////////////////////////////////////////////////////////////// // main int main(int argc, char *argv[]) { int optc; ModeType Mode = Display; FlagType Flag = Normal; bool SpecialReturn = false; while( (optc = env_getopt("NEWMAILOPTS", &argc, &argv, "bcdhHSnprtuvz")) != -1 ) { switch( optc ) { case 'b': // build cache Mode = Update; Flag |= BuildCache; break; case 'c': // command line 'new mail' status SpecialReturn = true; break; case 'd': Flag |= PrintDetails; break; case 'H': Flag |= HideBulk; break; case 'S': Flag &= ~HideBulk; break; case 'n': // new only Flag |= PrintLatestNew; break; case 'p': // plain Flag |= Plain; break; case 'r': // read-only Flag |= ReadOnly; break; case 't': // print grand totals Flag |= PrintTotal; break; case 'u': // update mode Mode = Update; Flag &= ~BuildCache; break; case 'v': // verbose Flag |= Verbose; break; case 'z': // print mailboxes with 0 new Flag |= PrintZero; break; case 'h': // help default: Usage(); return 0; } } try { locale::global(locale::classic()); LoginInfo login; string default_mbox = DEFAULT_MAIL_DIR; default_mbox += "/"; default_mbox += login.name(); string main_mail_dir = login.home_dir(); main_mail_dir += "/"; main_mail_dir += MAIN_MAIL_DIR; // // read in .newmailrc in home directory... each line // holds the mbox name, space, and grace setting in // minutes... if grace is -1, the mbox is skipped // completely (like spambucket)... this makes // it much more dynamic // // if grace is -2, mbox is skipped if -H was specified // on the command line // grace_map_type grace; string cfgfile = login.home_dir(); cfgfile += "/.newmailrc"; if( (Flag & Verbose) == 0 ) { ifstream cfg(cfgfile.c_str()); while( cfg ) { string mbox; int grace_min; cfg >> mbox >> grace_min; if( grace_min == -2 ) { if( Flag & HideBulk ) grace[mbox] = grace_min; } else { grace[mbox] = grace_min; } } } // find all mboxes in main directory, and sort mbox_container_type mboxes; if( optind < argc ) { // mboxes have been specified on the command line while( optind < argc ) mboxes.push_back(argv[optind++]); } else { find_mboxes(main_mail_dir, mboxes); } sort(mboxes.begin(), mboxes.end()); // insert default mbox at the beginning // mboxes.insert(mboxes.begin(), default_mbox); switch( Mode ) { case Display: default: display_mode(main_mail_dir, mboxes, grace, Flag); break; case Update: update_mode(main_mail_dir, mboxes, grace, Flag); break; } } catch(std::runtime_error &re) { cerr << "Runtime error: " << re.what() << endl; return 1; } if( SpecialReturn ) { if( NewMessagesFlag ) return 0; else return 1; } }