On Sun, 19 Dec 2004, Mark Crispin wrote:

> On Sun, 19 Dec 2004, Dan Mahoney, System Admin wrote:
> > I was wondering if UW Imap had the ability to report the system quota of the
> > mail directory (i.e. the home directory) to users who were using the QUOTA
> > extension.
>
> At the current time, the answer is "no".
>
> > I know this is a pretty standard system call (I think)...so it shouldn't be
> > too difficult to implement.
>
> Unfortunately, this is not the case; there are at least three different
> implementations of UNIX quotas that I am aware of, each with different
> semantics.  It would be fairly easy to assume that "all the world is
> Linux" and implement the QUOTA extension for Linux only; but imapd runs on
> many platforms besides Linux.
>
[snip..]
> Finally, IMAP QUOTA isn't defined in terms that directly map to UNIX
> quota; the suggested resources are STORAGE and MESSAGE, however on UNIX it
> is typically by disk block.  There are also independent concepts of
> "grace" space which isn't represented in IMAP QUOTA.
>
> None of this mean that "it can't be done"; however, to be useful and
> correct is quite a bit more complicated than the obvious simple
> implementation.

I fully respect Mark's comments & opinions (after all, he's the person
who has to deal with complaints about UW Imap not working as expected. ;)

Be that as it may, if you're willing to settle for a "gas gauge" kind of
implementation (IE a general indication of 'how close to empty' rather
than an exact impementation of the standard) then the Unix quota can be
used for a practical QUOTA system. (Particularly for webmail systems
where the users will probably never be able to get an exact handle on
the mailboxes, they just want a general "how close am I" kind of metric).

We have found this to be quite useful even if not technically 'correct'.

I hacked UW Imap to add two kinds of QUOTA info.
The first (see code defined by QUOTA_CHECK) regularly checks the
user's quota and sends IMAP ALERT messages if they're over.
It sends the text contained in one of two different files
(see QUOTA_CHECK_WARN_MESS_FILE & QUOTA_CHECK_ERROR_MESS_FILE)
via 'palert'.
The second is a quick-and-dirty implementation of the QUOTA extension
(see code defined by DO_QUOTA).

I've attached two files, the first is a patch to imapd.c (based upon
the 2004a version), the second contains the actual system dependent
quota gathering routines. That code is written for a Sys5 type OS
(we use HP-UX) but there are enough comments in it that a good hacker
should be able to adapt it to other platforms.

patch imapd.c, put "quota_check.c" in src/osdep/unix, add the
defines to the Makefile for imapd, and rebuild.

If you use the QUOTA_CHECK part, you'll have to create two text files
that contain the messages, one 'alert' for going over soft quota
one for hitting hard. suggest keeping the messages to a single line.

Please do not bother Mark about this stuff. I'll try to answer
questions about it but cannot promise anything WRT porting the OS
dependent part to another platform.

Dave

-- 
Dave Funk                                  University of Iowa
<dbfunk (at) engineering.uiowa.edu>        College of Engineering
319/335-5751   FAX: 319/384-0549           1256 Seamans Center
Sys_admin/Postmaster/cell_admin            Iowa City, IA 52242-1527
#include <std_disclaimer.h>
Better is not better, 'standard' is better. B{
*** imapd.c.orig        Tue Jun 29 16:56:17 2004
--- imapd.c     Mon Dec 20 14:59:48 2004
***************
*** 17,22 ****
--- 17,26 ----
   * The full text of our legal notices is contained in the file called
   * CPYRIGHT, included with this Distribution.
   */
+ /*
+  * 2003/12/1 dbf    Add QUOTA capability
+  */
+ 
  
  /* Parameter files */
  
***************
*** 23,28 ****
--- 27,35 ----
  #include <stdio.h>
  #include <ctype.h>
  #include <errno.h>
+ #ifdef QUOTA_CHECK
+ #include <time.h>
+ #endif
  extern int errno;             /* just in case */
  #include <signal.h>
  #include <time.h>
***************
*** 43,48 ****
--- 50,64 ----
  #define SHUTDOWNTIMER 1 MINUTES       /* shutdown dally timer */
  #define IDLETIMER 1 MINUTES   /* IDLE command poll timer */
  #define CHECKTIMER 15 MINUTES /* IDLE command last checkpoint timer */
+ #ifdef QUOTA_CHECK
+ #define QUOTATIMER 30 MINUTES /* quota check timer, how often to check */
+ #ifndef QUOTA_CHECK_WARN_MESS_FILE    /* error message to send when over 
quota */
+ #define QUOTA_CHECK_WARN_MESS_FILE "/etc/imap_quota_alert"
+ #endif
+ #ifndef QUOTA_CHECK_ERROR_MESS_FILE
+ #define QUOTA_CHECK_ERROR_MESS_FILE "/etc/imap_quota_error"
+ #endif
+ #endif        /* QUOTA_CHECK */
  
  
  #define LITSTKLEN 20          /* length of literal stack */
***************
*** 124,129 ****
--- 140,151 ----
  long crit_set (SEARCHSET **set,unsigned char **arg,unsigned long maxima);
  long crit_number (unsigned long *number,unsigned char **arg);
  long crit_string (STRINGLIST **string,unsigned char **arg);
+ #ifdef DO_QUOTA
+ int get_quota(char *, char *, int *, int*);
+ #endif
+ #ifdef QUOTA_CHECK
+ int quota_check(char *, char *);
+ #endif
  
  void fetch (char *t,unsigned long uid);
  typedef void (*fetchfn_t) (unsigned long i,void *args);
***************
*** 183,192 ****
  
  /* Global storage */
  
! char *version = "2004.352";   /* version number of this server */
  time_t alerttime = 0;         /* time of last alert */
  time_t sysalerttime = 0;      /* time of last system alert */
  time_t useralerttime = 0;     /* time of last user alert */
  time_t lastcheck = 0;         /* time of last checkpoint */
  time_t shutdowntime = 0;      /* time of last shutdown */
  int state = LOGIN;            /* server state */
--- 205,218 ----
  
  /* Global storage */
  
! char *version = "2004a.352L-mod.2";   /* version number of this server */
!                               /* Note local mods in version no. */
  time_t alerttime = 0;         /* time of last alert */
  time_t sysalerttime = 0;      /* time of last system alert */
  time_t useralerttime = 0;     /* time of last user alert */
+ #ifdef QUOTA_CHECK
+ time_t quotatime = 0;         /* time of last quota check */
+ #endif
  time_t lastcheck = 0;         /* time of last checkpoint */
  time_t shutdowntime = 0;      /* time of last shutdown */
  int state = LOGIN;            /* server state */
***************
*** 893,898 ****
--- 919,951 ----
          if (stream)           /* allow untagged EXPUNGE */
            mail_parameters (stream,SET_ONETIMEEXPUNGEATPING,(void *) stream);
        }
+ #ifdef DO_QUOTA
+                               /* give user's quota */
+       else if (!strcmp(cmd,"GETQUOTA") || !strcmp(cmd,"GETQUOTAROOT")) {
+         int used, limit;
+         char getq_resp[256];
+         response = badarg;    /* assume failure */
+                               /* get mailbox argument, ignore it, 
simple-minded hack */
+         if (!(s = snarf (&arg)))
+           response = misarg;
+         else if (arg) response = badarg;
+       /* make sure not anonymous and have valid user-name */
+         else if ((!anonymous) && user && 
+                 (get_quota(myhomedir(),user, &used, &limit) == 0)) {
+                 if (!strcmp(cmd,"GETQUOTAROOT")) {
+                    sprintf(getq_resp,"* QUOTAROOT %.80s \"\"\015\012",s);
+                    PSOUT(getq_resp);
+                    s= "\"\"";
+                 }
+ /* build response string: "* QUOTA ROOT (STORAGE use max)" */
+               sprintf(getq_resp,"* QUOTA %.80s (STORAGE %d 
%d)\015\012",s,used,limit);
+               PSOUT(getq_resp);
+               response = win;
+           }
+         if (stream)           /* allow untagged EXPUNGE */
+           mail_parameters (stream,SET_ONETIMEEXPUNGEATPING,(void *) stream);
+       }
+ #endif        /* DO_QUOTA */
  
                                /* find mailboxes */
        else if (!strcmp (cmd,"FIND")) {
***************
*** 1332,1337 ****
--- 1385,1408 ----
      if (state != LOGIN)               /* do user alert if logged in */
        useralerttime = palert (mailboxfile (tmp,USERALERTFILE),useralerttime);
    }
+ #ifdef QUOTA_CHECK
+   if ((time (0) > quotatime + QUOTATIMER) && (strlen(user) > 1) && 
(strlen(myhomedir()) > 1 )) {
+     quotatime = time (0);     /* check for quota abuse */
+     switch (quota_check(myhomedir(),user)) { /* possible quota overflows */
+       case 0:                 /* no problem, skip */
+       break;
+       case 1:                 /* over soft quota, give alert message */
+       (void) palert (QUOTA_CHECK_WARN_MESS_FILE,0);
+       break;
+       case 2:                 /* over hard quota, give error message */
+       (void) palert (QUOTA_CHECK_WARN_MESS_FILE,0);
+       (void) palert (QUOTA_CHECK_ERROR_MESS_FILE,0);
+       break;
+       default:                /* ignore errors from quota_check */
+       break;
+     }
+   }
+ #endif /* QUOTA_CHECK */
  }
  
  /* Print an alert file
***************
*** 3381,3386 ****
--- 3452,3460 ----
        thr = thr->next;
      }
      if (!anonymous) PSOUT (" MULTIAPPEND");
+ #ifdef DO_QUOTA
+     if (!anonymous) PSOUT (" QUOTA");
+ #endif
    }
    if (flag <= 0) {            /* want pre-authentication capabilities? */
      PSOUT (" SASL-IR LOGIN-REFERRALS");
***************
*** 4043,4045 ****
--- 4117,4122 ----
          user ? (char *) user : "???",tcp_clienthost (),
          (stream && stream->mailbox) ? stream->mailbox : "???",string);
  }
+ #if defined(QUOTA_CHECK) || defined (DO_QUOTA)
+ #include "../src/osdep/unix/quota_check.c"
+ #endif
/*
 * Check user's current quota against hard/soft limits
 * this code based upon a Sys5 type system.
 * it assumes that the file-system are locally mounted
 */
#if defined(QUOTA_CHECK) || defined (DO_QUOTA)
/* only build this module if requested */

#include <pwd.h>
#include <sys/stat.h>
#include <errno.h>
#include <mntent.h>
#include <sys/quota.h>

#ifndef DISKTAB /* file that contains mounted file system list */
#define DISKTAB  "/etc/fstab"
#endif

#ifndef debug
int debug= 0;
#endif

int quota_check(char *, char *);
int get_quota(char *, char *, int *, int *);

/*
 **********************************************************************
 * Check UNIX quota of named user for the specified file system
 * Inputs:
 *      home = path to directory to check quota on
 *      user = name of user to check quota for (only used if current UID == 0)
 * Output:
 *      -1   = error (message is syslogged )
 *      0    = all OK
 *      1    = over soft quota limit
 *      2    = over hard quota limit
 */

int quota_check(char *home, char *user)
{
FILE *mntf;
struct mntent *fsent;
struct stat dir_stat, mnt_stat;
struct dqblk quota_dat;
char special[100];
struct passwd *pw;
uid_t user_uid;
time_t left, now;

   if (debug)
      syslog (LOG_DEBUG,"Checking disk quota for %.80s, directory: 
%.80s",user,home);

   if (stat(home,&dir_stat)) {
      syslog (LOG_ALERT,"quota_check: stat of user %.80s directory: %.80s 
failed, %m",user,home);
      return(-1);
   }

   if ((mntf= setmntent(DISKTAB,"r")) == 0){
      syslog (LOG_ALERT,"quota_check: open of disk-table %.80s failed, 
%m",DISKTAB);
      return(-1);
   }

   while(fsent= getmntent(mntf)){
      if (stat(fsent->mnt_fsname,&mnt_stat)){
         syslog (LOG_ALERT,"quota_check: stat of mount point %.80s failed, 
%m",fsent->mnt_fsname);
         endmntent(mntf);
         return(-1);
      }
      if (dir_stat.st_dev == mnt_stat.st_rdev) {
         strncpy(special,fsent->mnt_fsname,99);
/*         printf("found mount point device %.80s\n",special); */
         break;
      }
   }
   endmntent(mntf);

   user_uid= geteuid(); /* get the current user's UID */

/* If UID == 0, still root, havn't completed login process.
   Get UID of person that we'll become so can check quota during login 
processing
 */
   if ((user_uid == 0 ) && (strlen(user) > 1)) {
      if (pw= getpwnam(user)) {
         user_uid= pw->pw_uid;
      } else {
         syslog (LOG_ALERT,"quota_check: unknown user: %.80s, %m",user);
         return(-1);
      }
   }
   if (user_uid == 0) return(0);   /* skip check for root */

   if (quotactl(Q_GETQUOTA, special, user_uid, &quota_dat) == 0) {
/*      printf("quota: quota=%d 
usage=%d\n",quota_dat.dqb_bsoftlimit,quota_dat.dqb_curblocks);
 */
      if (quota_dat.dqb_curblocks >= quota_dat.dqb_bhardlimit) {
         return(2);     /* over hard limit */
      }
      if (quota_dat.dqb_curblocks > quota_dat.dqb_bsoftlimit) {
/*         printf("Danger Will Robinson, you're over quota\n"); */
         if ((quota_dat.dqb_btimelimit != 0) && (quota_dat.dqb_btimelimit < 
time(0))) return(2);
         return(1);     /* over soft limit */
      }
/*      if ((quota_dat.dqb_btimelimit != 0) && (quota_dat.dqb_btimelimit < 
time(0))) {
         left = quota_dat.dqb_btimelimit - time(0);
         printf("you have %d hours left\n",(long)(left/3600));
      }
*/
   }
   return(0);   /* no errors, not over quota */
}

/*
 **********************************************************************
 * Get UNIX quota of named user for the specified file system
 * Inputs:
 *      home = path to directory to check quota on
 *      user = name of user to check quota for (only used if current UID == 0)
 * Output:
 *      used == blocks used
 *      limit == soft-limit (or ==used if over soft limit)
 *      -1   = error (message is syslogged )
 *      0    = all OK
 */

int get_quota(char *home, char *user, int *used, int *limit)
{
FILE *mntf;
struct mntent *fsent;
struct stat dir_stat, mnt_stat;
struct dqblk quota_dat;
char special[100];
struct passwd *pw;
uid_t user_uid;
time_t left, now;

   if (stat(home,&dir_stat)) {
      syslog (LOG_ALERT,"get_quota: stat of user %.80s directory: %.80s failed, 
%m",user,home);
      return(-1);
   }

   if ((mntf= setmntent(DISKTAB,"r")) == 0){
      syslog (LOG_ALERT,"get_quota: open of disk-table %.80s failed, 
%m",DISKTAB);
      return(-1);
   }

   while(fsent= getmntent(mntf)){
      if (stat(fsent->mnt_fsname,&mnt_stat)){
         syslog (LOG_ALERT,"get_quota: stat of mount point %.80s failed, 
%m",fsent->mnt_fsname);
         endmntent(mntf);
         return(-1);
      }
      if (dir_stat.st_dev == mnt_stat.st_rdev) {
         strncpy(special,fsent->mnt_fsname,99);
/*         printf("found mount point device %.80s\n",special); */
         break;
      }
   }
   endmntent(mntf);

   user_uid= geteuid(); /* get the current user's UID */

/* If UID == 0, still root, havn't completed login process.
   Get UID of person that we'll become so can check quota during login 
processing
 */
   if ((user_uid == 0 ) && (strlen(user) > 1)) {
      if (pw= getpwnam(user)) {
         user_uid= pw->pw_uid;
      } else {
         syslog (LOG_ALERT,"get_quota: unknown user: %.80s, %m",user);
         return(-1);
      }
   }
   if (user_uid == 0) return(-1);   /* skip check for root */

   if (quotactl(Q_GETQUOTA, special, user_uid, &quota_dat) == 0) {
/*      printf("quota: quota=%d 
usage=%d\n",quota_dat.dqb_bsoftlimit,quota_dat.dqb_curblocks);
 * quota_dat.dqb_curblocks == current blocks used
 * quota_dat.dqb_bsoftlimit == soft quota limit
 * quota_dat.dqb_bhardlimit == hard quota limit
 * quota_dat.dqb_btimelimit == grace time expiration if over soft limit
 */

      *used= quota_dat.dqb_curblocks;   /* return used */

/* Logic for determining value of limit:
 *      if used <= soft-limit, return limit == soft-limit
 *      if used >= hard-limit, return limit == soft-limit (show over 100% usage)
 *      if soft-limit < used < hard-limit && grace-time not expired,
 *                      return limit == amount used (so to show exactly 100% 
usage)
 *      else used > soft-limit && grace-time expired,
 *                      return limit == soft-limit (show over 100% usage)
 */
      if (quota_dat.dqb_curblocks <= quota_dat.dqb_bsoftlimit) {        /* 
under soft quota */
         *limit= quota_dat.dqb_bsoftlimit;      /* return limit == soft quota */
      } else if ((quota_dat.dqb_curblocks+3) >= quota_dat.dqb_bhardlimit ){ /* 
over hard limit */
         *limit= quota_dat.dqb_bsoftlimit;      /* return limit == soft quota */
      } else if ((quota_dat.dqb_btimelimit != 0) && (quota_dat.dqb_btimelimit > 
time(0))) {
         *limit= quota_dat.dqb_curblocks;   /* still have grace time, return 
limit == amount used */
      } else {                  /* over soft, grace expired */
         *limit= quota_dat.dqb_bsoftlimit;      /* return limit == soft quota */
      }
      if (debug)
        syslog (LOG_DEBUG,"disk quota for %.80s, used: %d, limit: 
%d",user,*used,*limit);
      return(0);   /* no errors */
   }
   return(-1);  /* errors or no quota info for this user */
}
#endif  /* QUOTA_CHECK */

Reply via email to