/* * COPYRIGHT INFORMATION - DO NOT REMOVE * * "Portions Copyright (c) 2000-2001 LinuxMagic Inc. All Rights Reserved. * This file contains Original Code and/or Modifications of Original Code as * defined in and that are subject to the Wizard Software License Version * 1.0 (the 'License'). You may not use this file except in compliance with * the License. Please obtain a copy of the License at: * * http://www.linuxmagic.com/opensource/licensing/GPL-2.text * * and read it before using this file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND LINUXMAGIC HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see * the License for the specific language governing rights and limitations * under the License." * * Please read the terms of this license carefully. By using or downloading * this software or file, you are accepting and agreeing to the terms of this * license with LinuxMagic Inc. If you are agreeing to this license on behalf * of a company, you represent that you are authorized to bind the company to * such a license. If you do not meet this criterion or you do not agree to * any of the terms of this license, do NOT download, distribute, use or alter * this software or file in any way. * */ /* $Id: qmail-remove.c,v 1.13 2005/01/13 20:28:15 josh Exp $ */ /* * This program is loosely based on the python script which * does a similar thing and can be found at: * * http://www.redwoodsoft.com/~dru/programs/ * */ /* * NOTES: * * yanked directory is relative to queue directory unless an * absolute path is specified (begins with /) and assumed to * exist. * * queue directory should be absolute path * * originally tested on Linux and OpenBSD * */ /* TODO: * * cleanup the path generation routines * */ /* * Modified: 12/23/2003 Clint Martin ( c.martin*earthlink.net ) * * I have added two additional parameters. -X and -x both are intended to aid in forcing the expiration * of messages in the qmail queue to happen in a different time frame than the default Qmail time frame. * this is done by modifying the time stamps on the files in the "INFO" directory of the queue. * * -X takes a parameter of the number of seconds to offset the creation/modification time of matching * messages. positive values cause the dates to go back in time, essentially making the message "OLDER" * when passing through the Queue, qmail will bounce any messages that are TOO old. * negative values essentially make the message YOUNGER. thereby lengthening the time they spend in the queue * Passing the value 0 will offset into the past all matching files by exactly 7 days. * * -x takes a complete date/timestamp as it's parameter. All matching messages are re-stamped to this exact value * The format of the parameter is the same as the output of the date(8) command. ie: * "Tue Dec 23 11:42:29 PST 2003" * This makes it easy to use the date command to change time stamps relative to "today" see examples below for details * * * I think the -X parameter is relatively self explanatory... however: * * Make all messages matching Foo@bar.com 1 day older. * * qmail-remove -p Foo@bar.com -X 86400 * * Newer? * * qmail-remove -p Foo@bar.com -X -86400 * * Some examples of the -x option: * * Make all messages matching Foo@bar.com look like they went into the Queue Today * * qmail-remove -p Foo@bar.com -x "`date`" * * Yesterday? * * qmail-remove -p Foo@bar.com -x "`date -v1d`" * * Tomorrow? * * qmail-remove -p Foo@bar.com -x "`date -v+1d`" * * I think you get the idea. * * Notes: * * I think the -X option may be a little confusing. perhaps I should change it so that positive values * offset into the future, whereas negative values offset into the past? Thoughts? * * The X, x and R options are mutually exclusive. Currently R will take precedence * * Using the "X" option causes us to change the file time for all matching files * to be OLDER than the queue expiration time. * * This is intended as an easy way to clean out crap from the queue without having to shut down qmail, to do it. * By changing the file timestamp, qmail will expire and clean out the queue for us.. of course we don't get to keep * the files when this happens * * This can also be used to keep a message in the queue for a longer period of time. Using a negative offset, or a * date in the future will accomplish this. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include /* for the utimes function */ #include /* many linux fcntl.h's seem to be broken */ #ifndef O_NOFOLLOW #define O_NOFOLLOW 0400000 #endif /* prototypes */ void usage(void); int search_file(const char *filename, const char *regex); int remove_file(const char *filename); int delete_file(const char *filename); int expire_file(const char *filename); char *read_file(const char *filename); int find_files(char *path[], const char *pattern); unsigned long digits(unsigned long num); char * mk_nohashpath(char *queue, int inode_name); char * mk_hashpath(char *queue, int inode_name); char * mk_newpath(char *queue, int inode_name); /* globals */ extern const char *__progname; const char *default_queue = "/var/qmail/queue"; const char *default_pattern = ".*"; const char *queuedir; int regex_flags = 0, verbosity = 0, conf_split = 23, remove_files = 0, delete_files = 0; char *yank_dir = "yanked"; const char cvsrid[] = "$Id: qmail-remove.c,v 1.13 2005/01/13 20:28:15 josh Exp $"; int queue_fd = -1; unsigned long read_bytes = 0; /* Added */ char *strptime(const char *s, const char *format, struct tm *tm); int expire_files=0; /* if the eXpire option is specified on the command line, this will reflect that */ time_t expire_offset = 60*60*24*7; /* one week in seconds -- this can be changed in the future by a parameter passed to us */ time_t expire_date = 0; /* if specified, this is the timestamp the file will be stamped with */ /* queues i * * NOTE: the first queue must be mess as it is used as a key * */ const char *queues[] = {"mess", "local", "remote", "info", "intd", "todo", "bounce", NULL}; int main(int argc, char **argv) { long int split = 0; int len, ch, matches; char *yankdirstring, *p, *pattern = NULL; struct tm stime; struct stat statinfo; if (argc < 2) { usage(); } while ((ch = getopt(argc, argv, "ehin:p:q:drs:vy:?X:x:")) != -1) { switch (ch) { case 'e': regex_flags |= REG_EXTENDED; case 'i': regex_flags |= REG_ICASE; break; case 'n': read_bytes = strtoul(optarg, NULL, 10); if(read_bytes == ULONG_MAX) { fprintf(stderr, "invalid number of bytes\n"); exit(EXIT_FAILURE); } break; case 'p': pattern = optarg; break; case 'q': queue_fd = open(optarg, O_RDONLY); if(queue_fd < 0) { perror("open()"); exit(1); } if(fchdir(queue_fd) == -1) { perror("fchdir()"); exit(1); } queuedir = optarg; break; case 'd': delete_files = 1; break; case 'r': remove_files = 1; break; case 's': split = strtoul(optarg, NULL, 10); if(split == (long) ULONG_MAX) { fprintf(stderr, "invalid split value\n"); exit(EXIT_FAILURE); } conf_split = split; break; case 'v': verbosity ++; break; /*** Added: 12/23/2003 Clint Martin ( c.martin*earthlink.net ) ***/ case 'X': expire_files = 1; /* lets see if they specified a parameter for seconds */ split = strtol(optarg, NULL, 10); if(split == (long) ULONG_MAX) { fprintf(stderr, "invalid expire offset value [ %s ]\n", optarg); exit(EXIT_FAILURE); } expire_offset = (split == 0 ? expire_offset : split); fprintf(stderr, "Offsetting timestamps by %d seconds \n", (int)expire_offset); expire_date = -1; /* make sure we only use the offset */ break; case 'x': expire_files = 1; /* lets test our parsing function to see what it can do */ p = strptime(optarg, "%+", &stime); expire_date = mktime(&stime); if (expire_date >= 0){ fprintf(stderr,"time in seconds: %d [ %s ]\n", (int)expire_date, optarg); } else{ fprintf(stderr,"Error parsing the date specified at: [ %s ] col %d [ %s ] ]\n", p, p - optarg, optarg); exit(EXIT_FAILURE); } expire_offset = 0; /* make sure we only use the time stamp passed */ break; /*** End Section Added: 12/23/2003 Clint Martin ( c.martin*earthlink.net ) ***/ case 'y': yank_dir = optarg; break; default: usage(); break; } } if (queue_fd == -1) { queue_fd = open(default_queue, O_RDONLY); if (queue_fd < 0) { fprintf(stderr, "no queue directory specified and %s doesn't exist\n", default_queue); exit(1); } if (fchdir(queue_fd) == -1) { perror("fchdir()"); exit(1); } queuedir = default_queue; } /* if we're going to yank something, make sure there's somewhere to yank it to */ if (remove_files == 1) { /* make room for queuedir '/' yank_dir '\0' */ len = strlen(queuedir) + strlen(yank_dir) + 2; yankdirstring = malloc(len); if (yankdirstring == NULL) { perror("malloc()"); exit(1); } snprintf(yankdirstring, len + 1, "%s/%s", queuedir, yank_dir); if (stat(yankdirstring, &statinfo) != 0) { switch (errno) { case ENOENT: fprintf(stderr, "FATAL: yank_dir [%s] does not exist, you must create it first.\n", yankdirstring); break; default: perror("stat(yank_dir)"); break; } exit(1); } if (!S_ISDIR(statinfo.st_mode)) { fprintf(stderr, "FATAL: yank_dir [%s] is not a directory.\n", yankdirstring); exit(1); } } matches = find_files((char **)queues, (pattern ? pattern : default_pattern)); if (matches >= 0) { fprintf(stderr, "%d file(s) match\n", matches); } exit(EXIT_SUCCESS); } void usage(void) { fprintf(stderr, "%s [options]\n", __progname); fprintf(stderr, " -e\t\tuse extended POSIX regular expressions\n"); fprintf(stderr, " -h, -?\tthis help message\n"); fprintf(stderr, " -i\t\tsearch case insensitively [default: case sensitive]\n"); fprintf(stderr, " -n \tlimit our search to the first bytes of each file\n"); fprintf(stderr, " -p \tspecify the pattern to search for\n"); fprintf(stderr, " -q \tspecify the base qmail queue dir [default: /var/qmail/queue]\n"); fprintf(stderr, " -d\t\tactually remove files not yank them, no -p will delete all the messages!\n"); fprintf(stderr, " -r\t\tactually remove files, without this we'll only print them\n"); fprintf(stderr, " -s \tspecify your conf-split value if non-standard [default: 23]\n"); fprintf(stderr, " -v\t\tincrease verbosity (can be used more than once)\n"); fprintf(stderr, " -y \tdirectory to put files yanked from the queue [default: /yanked]\n"); /* Begin CLM 12/23/2003 */ fprintf(stderr, " -X \tmodify timestamp on matching files, to make qmail expire mail\n"); fprintf(stderr, "\t\t is the number of seconds we want to move the file into the past.\n"); fprintf(stderr, "\t\t specifying a value of 0 causes this to default to (%d)\n", (int)expire_offset); fprintf(stderr, " -x \tmodify timestamp on matching files, to make qmail expire mail\n"); fprintf(stderr, "\t\t is a date/time string in the format of output of the \"date\" program.\n"); fprintf(stderr, "\t\t see manpage for strptime(2) for details of this format\n"); /* End CLM 12/23/2003 */ exit(EXIT_FAILURE); } char * read_file(const char *filename) { off_t bytes; int fd; char *buff = NULL; struct stat fd_stat; if (filename == NULL) { return NULL; } fd = open(filename, (O_RDONLY | O_NOFOLLOW), 0); if (fd != -1) { if (fstat(fd, &fd_stat) == 0) { /* * If they specify the number of bytes they * want to read read that many (or the whole file * if it's smaller). Otherwise read the whole file. */ if (((long)read_bytes > 0) && (fd_stat.st_size > (long)read_bytes)) bytes = read_bytes; else bytes = fd_stat.st_size; buff = malloc(bytes + 1); if(buff != NULL) { buff[bytes] = '\0'; if(read(fd, buff, bytes) != bytes) { fprintf(stderr, "%s: read() too short\n", __progname); free(buff); buff = NULL; } } else { perror("malloc()"); } } else { perror("fstat()"); } close(fd); } return buff; } int search_file(const char *filename, const char *pattern) { regex_t match_me; int err_code, match = 0; char *file_inards = NULL, error_str[20]; if (pattern == NULL) { return (-1); } if (filename == NULL) { return (-1); } file_inards = read_file(filename); if (file_inards != NULL) { err_code = regcomp(&match_me, pattern, regex_flags | REG_NOSUB); if (err_code == 0) { if (regexec(&match_me, file_inards, 0, NULL, 0) == 0) { match = 1; } } else { /* regex error */ regerror(err_code, &match_me, error_str, 20); fprintf(stderr, "regcomp(): %s\n", error_str); } regfree(&match_me); free(file_inards); } if(match == 1) { return (0); } return (-1); } int find_files (char *dir_list[], const char *pattern) { FTS *fts; FTSENT *ftsp; char *error_str = NULL, *argv[2]; int i = 0, tmp_fd = -1; argv[0] = dir_list[0]; argv[1] = NULL; if ((fts = fts_open((char **)argv, FTS_PHYSICAL, NULL)) == NULL) { perror("fts_open"); return -1; } errno = 0; while ((ftsp = fts_read(fts)) != NULL) { switch (ftsp->fts_info) { case FTS_F: printf("%s: ", ftsp->fts_accpath); if(search_file(ftsp->fts_accpath, pattern) == 0) { printf("yes\n"); i++; tmp_fd = open(".", O_RDONLY); if((tmp_fd >= 0) && (remove_files == 1)) { if(fchdir(queue_fd) != 0) { perror("fchdir(queue_fd)"); exit(1); } remove_file(ftsp->fts_name); if(fchdir(tmp_fd) != 0) { perror("fchdir()"); } } else if ((tmp_fd >= 0) && (delete_files == 1)) { fchdir(queue_fd); delete_file(ftsp->fts_name); if(fchdir(tmp_fd) != 0) { perror("fchdir()"); } } else if ((tmp_fd >= 0) && (expire_files == 1)) { /* this makes sure the the Remove option takes precedence over the eXpire options. */ fchdir(queue_fd); expire_file(ftsp->fts_name); if(fchdir(tmp_fd) != 0) { perror("fchdir()"); } } if(tmp_fd >= 0) { close(tmp_fd); tmp_fd = -1; } } else { printf("no\n"); } break; case FTS_DNR: /* couldn't read */ case FTS_ERR: /* error */ case FTS_NS: /* no stat info */ error_str = strerror(ftsp->fts_errno); fprintf(stderr, "fts_read() %s: %s\n", ftsp->fts_path, error_str); break; default: break; } } fts_close(fts); return i; } /* * remove_file() * * Takes a filename and assumes it is the path to a qmail queue file * (named after its inode). It attempts to find it in all the queues and * move them to the global "yank_dir". It returns the inode number of the * file it removed from the queue or -1 on an error. */ int remove_file(const char *filename) { int i, count = 0; unsigned long inode_num; char *my_name, *old_name = NULL, *new_name = NULL; struct stat statinfo; if (filename == NULL) { fprintf(stderr, "remove_file(): no filename\n"); return -1; } my_name = strrchr(filename, '/'); if (my_name == NULL) { my_name = (char *)filename; } else { my_name++; } inode_num = strtoul(my_name, NULL, 10); if ((inode_num == ULONG_MAX) || (inode_num == 0)) { fprintf(stderr, "%s doesn't look like an inode number\n", my_name); return -1; } for (i = 0; (queues[i] != NULL); i++) { new_name = mk_newpath((char *)queues[i], inode_num); if (new_name == NULL) { fprintf(stderr, "remove_file(): unable to create new name\n"); return -1; } old_name = mk_hashpath((char *)queues[i], inode_num); if (old_name == NULL) { free(new_name); fprintf(stderr, "remove_file(): unable to create old name\n"); return -1; } if (rename(old_name, new_name) == 0) { /* succeeded */ fprintf(stderr, "moved %s to %s\n", old_name, new_name); count ++; } else { if (errno == ENOENT) { if (old_name) { if (verbosity >= 2) { fprintf(stderr, "remove_file(%s): not a file\n", old_name); } free(old_name); old_name = NULL; } old_name = mk_nohashpath((char *)queues[i], inode_num); if (old_name == NULL) { free(new_name); return -1; } if (stat(old_name, &statinfo) == -1) { if (verbosity >= 2) { fprintf(stderr, "remove_file(%s): no stat info\n", old_name); } continue; } if ( !S_ISREG(statinfo.st_mode) ) { if (verbosity >= 2) { fprintf(stderr, "remove_file(%s): not a file\n", old_name); } continue; } if (rename(old_name, new_name) == 0) { /* succeeded */ fprintf(stderr, "moved %s to %s\n", old_name, new_name); count ++; } else { if(errno != ENOENT) { perror("rename()"); } } } else { /* failed but exists */ perror("rename()"); } } } /* garbage collection */ if (old_name) { free(old_name); } if (new_name) { free(new_name); } return count; } /* * Clint Martin 12/23/2003 * * expire_file() * * Takes a filename and assumes it is the path to a qmail queue file * (named after its inode). It attempts to find it in the INFO dir, and * changes the file time stamp * * returns the number of successful file changes, or -1 on error. */ int expire_file(const char *filename) { int count = 0; unsigned long inode_num; char *my_name; char *old_name; struct stat statinfo; struct timeval times[2]; /* used to hold the new mtime and ctime values for the file */ if (filename == NULL) { fprintf(stderr, "expire_file(): no filename\n"); return -1; } my_name = strrchr(filename, '/'); if (my_name == NULL) { my_name = (char *)filename; } else { my_name++; } /* make sure the INODE NUMBER is really an INODE */ inode_num = strtoul(my_name, NULL, 10); if ((inode_num == ULONG_MAX) || (inode_num == 0)) { fprintf(stderr, "%s doesn't look like an inode number\n", my_name); return -1; } /* generate the relative path to the specified file */ old_name = mk_hashpath("info", inode_num); if (old_name == NULL) { fprintf(stderr, "expire_file(): unable to create old name\n"); return -1; } /* reset it's time stamp */ if (expire_date > 0L) { /* use the date specified if it was passed */ times[0].tv_sec = expire_date; times[0].tv_usec = 0; times[1].tv_sec = expire_date; times[1].tv_usec = 0; } else { /* otherwise use the relative date offset form */ if (stat(old_name, &statinfo) == 0) { times[0].tv_sec = statinfo.st_mtime - expire_offset; times[0].tv_usec = 0; times[1].tv_sec = statinfo.st_ctime - expire_offset; times[1].tv_usec = 0; } else { /* do some error detection */ if (errno == ENOENT) { if (old_name) { if (verbosity >= 2) { fprintf(stderr, "expire_file(%s): not a file (stat) \n", old_name); } free(old_name); old_name = NULL; return -1; } } } } /* now, update the time stamp */ if (utimes(old_name, times) == 0) { /* succeeded */ fprintf(stderr, "Set timestamp on %s \n", old_name); count ++; } else { if (errno == ENOENT) { if (old_name) { if (verbosity >= 2) { fprintf(stderr, "expire_file(%s): not a file\n", old_name); } free(old_name); old_name = NULL; } } } /* garbage collection */ if (old_name) { free(old_name); } return count; } char * mk_nohashpath(char *queue, int inode_name) { int len = 0; char * old_name = NULL; if ((queue == NULL) || (inode_name <= 0)) { return NULL; } len = strlen(queue); len += digits(inode_name); len += 4; old_name = malloc(len); if (old_name) { snprintf(old_name, len, "%s/%u", queue, inode_name); /* fprintf(stderr, "path: [%s][%u]\n", old_name, inode_name); */ return old_name; } else { perror("malloc()"); return NULL; } } char * mk_hashpath(char *queue, int inode_name) { int len = 0, hash_num = 0; char * old_name = NULL; if ((queue == NULL) || (inode_name <= 0)) { return NULL; } hash_num = (inode_name % conf_split); len = strlen(queue); len += digits(hash_num); len += digits(inode_name); len += 4; old_name = malloc(len); if (old_name) { snprintf(old_name, len, "%s/%u/%u", queue, hash_num, inode_name); /* fprintf(stderr, "path: [%s][%u]\n", old_name, inode_name); */ return old_name; } else { perror("malloc()"); return NULL; } } char * mk_newpath(char *queue, int inode_name) { int len = 0; char *new_name = NULL; if ((queue == NULL) || (inode_name <= 0)) { fprintf(stderr, "mk_newpath(): invalid queue\n"); return NULL; } len = strlen(queue); len += strlen(yank_dir); len += digits(inode_name); len += 4; new_name = malloc(len); if (new_name) { snprintf(new_name, len, "%s/%u.%s", yank_dir, inode_name, queue); return new_name; } else { perror("malloc()"); return NULL; } } /* * digits() * * Returns the number of digits needed to represent the number "num". * */ unsigned long digits(unsigned long num) { unsigned long i = 0; while (num >= 10) { num /= 10; i++; } i++; return (i); } /* * delete_file() * * Takes a filename and assumes it is the path to a qmail queue file * (named after its inode). It attempts to find it in all the queues and * removes the file(s). It returns the inode number of the file it removed * from the queue or -1 on an error. This works with version .94 of qmail * -remove. */ int delete_file(const char *filename) { int i, count = 0; unsigned long inode_num; char *my_name, *old_name = NULL; struct stat statinfo; if (filename == NULL) { fprintf(stderr, "delete_file(): no filename\n"); return -1; } my_name = strrchr(filename, '/'); if (my_name == NULL) { my_name = (char *)filename; } else { my_name++; } inode_num = strtoul(my_name, NULL, 10); if ((inode_num == ULONG_MAX) || (inode_num == 0)) { fprintf(stderr, "%s doesn't look like an inode number\n", my_name); return -1; } for (i = 0; (queues[i] != NULL); i++) { old_name = mk_hashpath((char *)queues[i], inode_num); if (old_name == NULL) { fprintf(stderr, "delete_file(): unable to create old name\n"); return -1; } if ( unlink (old_name) == 0) { /* succeeded */ fprintf(stderr, "remove %s\n", old_name); count ++; } else { if (errno == ENOENT) { if (old_name) { if (verbosity >= 2) { fprintf(stderr, "delete_file(%s): not a file\n", old_name); } free(old_name); old_name = NULL; } old_name = mk_nohashpath((char *)queues[i], inode_num); if (old_name == NULL) { return -1; } if (stat(old_name, &statinfo) == -1) { if (verbosity >= 2) { fprintf(stderr, "delete_file(%s): no stat info\n", old_name); } continue; } if ( !S_ISREG(statinfo.st_mode) ) { if (verbosity >= 2) { fprintf(stderr, "delete_file(%s): not a file\n", old_name); } continue; } if (unlink(old_name) == 0) { /* succeeded */ fprintf(stderr, "remove %s\n", old_name ); count ++; } else { if(errno != ENOENT) { perror("unlink()"); } } } else { /* failed but exists */ perror("unlink()"); } } } /* garbage collection */ if (old_name) { free(old_name); } return count; }