/* ** Copyright 2003 Double Precision, Inc. ** See COPYING for distribution information. */ #if HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #if HAVE_UNISTD_H #include #endif #if HAVE_UTIME_H #include #endif #include #if HAVE_SYS_TIME_H #include #endif #if HAVE_LOCALE_H #include #endif #include #include #include "mysignal.h" #include "imapd.h" #include "imapscanclient.h" #include "imapwrite.h" #include "maildir/config.h" #include "maildir/maildircreate.h" #include "maildir/maildirrequota.h" #include "maildir/maildirgetquota.h" #include "maildir/maildirquota.h" #include "maildir/maildirmisc.h" #include "maildir/maildirwatch.h" #if HAVE_DIRENT_H #include #define NAMLEN(dirent) strlen((dirent)->d_name) #else #define dirent direct #define NAMLEN(dirent) (dirent)->d_namlen #if HAVE_SYS_NDIR_H #include #endif #if HAVE_SYS_DIR_H #include #endif #if HAVE_NDIR_H #include #endif #endif extern int keywords(); /* ** Implement SMAP snapshots. A snapshot is implemented, essentially, by ** saving the current folder index, restoring it, then doing a noop(). ** ** The snapshot file saves uids, not complete filenames. The complete ** filenames are already in courierimapuiddb. Filenames are long, and saving ** them can result in huge snapshot files for large folders. So only uids ** are saved, and when the snapshot is restored the courierimapuiddb file is ** read to obtain the filenames. */ extern char *current_mailbox; extern struct imapscaninfo current_maildir_info; extern char *readline(unsigned i, FILE *); static char *snapshot_dir; /* Directory with snapshots */ static char *snapshot_last; /* Last snapshot */ static char *snapshot_cur; /* Current snapshot */ static int index_dirty; static int snapshots_enabled; extern void set_time(const char *tmpname, time_t timestamp); extern void smapword(const char *); struct snapshot_list { struct snapshot_list *next; char *filename; char *prev; time_t mtime; }; /* ** When cleaning up a snapshot directory, we need to know whether there's ** a later snapshot that claims that this snapshot is the previous ** snapshot (we can safely dump snapshots that were previous snapshots ** of previous snapshots). */ static struct snapshot_list *find_next_snapshot(struct snapshot_list *s, const char *n) { const char *p, *q; p=strrchr(n, '/'); if (p) n=p+1; while (s) { p=s->prev; q=p ? strrchr(p, '/'):NULL; if (q && strcmp(q+1, n) == 0) return s; s=s->next; } return NULL; } /* ** Delete a snapshot structure, and the actual file */ static void delete_snapshot(struct snapshot_list *snn) { char *p=malloc(strlen(snapshot_dir)+strlen(snn->filename)+2); if (p) { strcat(strcat(strcpy(p, snapshot_dir), "/"), snn->filename); unlink(p); free(p); } free(snn->filename); free(snn->prev); free(snn); } /* ** Restore a snapshot */ static int restore_snapshot2(const char *snapshot_dir, FILE *fp, struct imapscaninfo *new_index); /* ** Part 1: process the first header line of a snapshot file, and allocate a ** new folder index list. */ static int restore_snapshot(const char *dir, FILE *snapshot_fp, char **last_snapshot) { int format; unsigned long s_nmessages, s_uidv, s_nextuid; char *p; char *buf; struct imapscaninfo new_index; if ((buf=readline(0, snapshot_fp)) == NULL) return 0; p=strchr(buf, ':'); if (p) *p++=0; *last_snapshot=NULL; if (sscanf(buf, "%d %lu %lu %lu", &format, &s_nmessages, &s_uidv, &s_nextuid) != 4 || format != SNAPSHOTVERSION) return 0; /* Don't recognize the header */ /* Save the previous snapshot ID */ if (p) { *last_snapshot=malloc(strlen(dir)+strlen(p)+2); if (!last_snapshot) { write_error_exit(0); return 0; } strcat(strcat(strcpy(*last_snapshot, dir), "/"), p); } imapscan_init(&new_index); if (s_nmessages && (new_index.msgs=(struct imapscanmessageinfo *) malloc(s_nmessages * sizeof(*new_index.msgs))) == 0) { write_error_exit(0); return (0); } memset(new_index.msgs, 0, s_nmessages * sizeof(*new_index.msgs)); new_index.nmessages=s_nmessages; new_index.uidv=s_uidv; new_index.nextuid=s_nextuid; if (restore_snapshot2(dir, snapshot_fp, &new_index)) { imapscan_copy(¤t_maildir_info, &new_index); imapscan_free(&new_index); return 1; } imapscan_free(&new_index); if (*last_snapshot) { free(*last_snapshot); *last_snapshot=0; } return 0; } /* ** Part 2: combine the snapshot and courierimapuiddb, create a halfbaked ** index from the combination. */ static int restore_snapshot2(const char *snapshot_dir, FILE *fp, struct imapscaninfo *new_index) { unsigned long i; char *p=malloc(strlen(snapshot_dir) + sizeof("/../" IMAPDB)); FILE *courierimapuiddb; int version; unsigned long uidv; unsigned long nextuid; char *uid_line; unsigned long uid=0; if (!p) { write_error_exit(0); return 0; } strcat(strcpy(p, snapshot_dir), "/../" IMAPDB); courierimapuiddb=fopen(p, "r"); free(p); if (!courierimapuiddb) return 0; /* Can't open the uiddb file, no dice */ if ((p=readline(0, courierimapuiddb)) == NULL || sscanf(p, "%d %lu %lu", &version, &uidv, &nextuid) != 3 || version != IMAPDBVERSION /* Do not recognize the uiddb file */ || uidv != new_index->uidv /* Something major happened, abort */ ) { fclose(courierimapuiddb); return 0; } uid_line=readline(0, courierimapuiddb); if (uid_line) { if (sscanf(uid_line, "%lu", &uid) != 1) { fclose(courierimapuiddb); return 0; } } /* ** Both the snapshot file and courierimapuiddb should be in sorted ** order, by UIDs, rely on that and do what amounts to a merge sort. */ for (i=0; inmessages; i++) { unsigned long s_uid; char flag_buf[128]; p=fgets(flag_buf, sizeof(flag_buf)-1, fp); if (p == NULL || (p=strchr(p, '\n')) == NULL || (*p = 0, sscanf(flag_buf, "%lu", &s_uid)) != 1 || (p=strchr(flag_buf, ':')) == NULL) /* Corrupted file */ { fclose(courierimapuiddb); return 0; } new_index->msgs[i].uid=s_uid; /* Try to fill in the filenames to as much of an extent as ** possible. If IMAPDB no longer has a particular uid listed, ** that's ok, because the message is now gone, so we just ** insert an empty filename, which will be expunged by ** noop() processing, after the snapshot is restored. */ while (uid_line && uid <= s_uid) { if (uid == s_uid && (uid_line=strchr(uid_line, ' ')) != NULL) /* Jackpot */ { new_index->msgs[i].filename= malloc(strlen(uid_line)+ strlen(flag_buf)+2); if (!new_index->msgs[i].filename) { fclose(courierimapuiddb); write_error_exit(0); return 0; } strcpy(new_index->msgs[i].filename, uid_line+1); if (p) { strcat(strcat(new_index->msgs[i] .filename, MDIRSEP), p+1); } } uid_line=readline(0, courierimapuiddb); if (uid_line) { if (sscanf(uid_line, "%lu", &uid) != 1) { fclose(courierimapuiddb); return 0; } } } if (new_index->msgs[i].filename == 0) { new_index->msgs[i].filename=strdup(""); /* A noop should get rid of this entry anyway */ if (!new_index->msgs[i].filename) { fclose(courierimapuiddb); write_error_exit(0); return 0; } } } fclose(courierimapuiddb); if (keywords()) imapscan_restoreKeywordSnapshot(fp, new_index); return 1; } void snapshot_select(int flag) { snapshots_enabled=flag; } /* ** Initialize snapshots for an opened folder. ** ** Parameters: ** ** folder - the path to a folder that's in the process of opening. ** ** snapshot - not NULL if the client requested a snapshot restore. ** ** Exit code: ** ** When a snapshot is requested, a non-zero exit code means that the ** snapshot has been succesfully restored, and current_mailbox is now ** initialized based on the snapshot. A zero exit code means that the ** snapshot has not been restored, and snapshot_init() needs to be called ** again with snapshot=NULL in order to initialize the snapshot structures. ** ** When a snapshot is not requested, the exit code is always 0 */ int snapshot_init(const char *folder, const char *snapshot) { struct snapshot_list *sl=NULL; DIR *dirp; struct dirent *de; struct snapshot_list *snn, **ptr; int cnt; char *new_dir; int rc=0; char *new_snapshot_cur=NULL; char *new_snapshot_last=NULL; if ((new_dir=malloc(strlen(folder)+sizeof("/" SNAPSHOTDIR))) == NULL) { write_error_exit(0); return rc; } strcat(strcpy(new_dir, folder), "/" SNAPSHOTDIR); mkdir(new_dir, 0755); /* Create, if doesn't exist */ if (snapshot) { FILE *fp; if (*snapshot == 0 || strchr(snapshot, '/') || *snapshot == '.') /* Monkey business */ { free(new_dir); return 0; } new_snapshot_cur=malloc(strlen(new_dir) + strlen(snapshot) + 2); if (!new_snapshot_cur) { free(new_dir); write_error_exit(0); return rc; } strcat(strcat(strcpy(new_snapshot_cur, new_dir), "/"), snapshot); if ((fp=fopen(new_snapshot_cur, "r")) != NULL && restore_snapshot(new_dir, fp, &new_snapshot_last)) { set_time(new_snapshot_cur, time(NULL)); rc=1; /* We're good to go. Finish everything else */ } if (fp) { fclose(fp); fp=NULL; } if (!rc) /* Couldn't get the snapshot, abort */ { free(new_snapshot_cur); free(new_dir); return 0; } } if (snapshot_dir) free(snapshot_dir); if (snapshot_last) free(snapshot_last); if (snapshot_cur) free(snapshot_cur); snapshot_dir=NULL; snapshot_last=new_snapshot_last; snapshot_cur=new_snapshot_cur; snapshot_dir=new_dir; index_dirty=1; /* Get rid of old snapshots as follows */ /* Step 1, compile a list of snapshots, sorted in mtime order */ dirp=opendir(snapshot_dir); while (dirp && (de=readdir(dirp)) != NULL) { FILE *fp; struct stat stat_buf; char *n; if (de->d_name[0] == '.') continue; n=malloc(strlen(snapshot_dir)+strlen(de->d_name)+2); if (!n) break; /* Furrfu */ strcat(strcat(strcpy(n, snapshot_dir), "/"), de->d_name); fp=fopen(n, "r"); if (fp) { char buf[1024]; if (fgets(buf, sizeof(buf)-1, fp) != NULL && fstat(fileno(fp), &stat_buf) == 0) { char *p=strchr(buf, '\n'); int fmt; if (p) *p=0; p=strchr(buf, ':'); if (p) *p++=0; if (sscanf(buf, "%d", &fmt) == 1 && fmt == SNAPSHOTVERSION) { snn=malloc(sizeof(*sl)); if (snn) memset(snn, 0, sizeof(*snn)); if (snn == NULL || (snn->filename=strdup(de->d_name)) == NULL || (snn->prev=strdup(p ? p:"")) == NULL) { if (snn && snn->filename) free(snn->filename); if (snn) free(snn); snn=NULL; } if (snn) { snn->mtime=stat_buf.st_mtime; for (ptr= &sl; *ptr; ptr=&(*ptr)->next) { if ( (*ptr)->mtime > snn->mtime) break; } snn->next= *ptr; *ptr=snn; } free(n); n=NULL; } } fclose(fp); } if (n) { unlink(n); free(n); } } if (dirp) closedir(dirp); /* Step 2: drop snapshots that are definitely obsolete */ for (ptr= &sl; *ptr; ) { if ((snn=find_next_snapshot(sl, (*ptr)->filename)) && find_next_snapshot(sl, snn->filename)) { snn= *ptr; *ptr=snn->next; delete_snapshot(snn); } else ptr=&(*ptr)->next; } /* If there are more than 10 snapshots, drop older snapshots */ cnt=0; for (snn=sl; snn; snn=snn->next) ++cnt; if (cnt > 10) { time_t now=time(NULL); while (sl && sl->mtime < now && (now - sl->mtime) > 60 * 60 * 24 * (7 + (cnt-10)*2)) { snn=sl; sl=sl->next; delete_snapshot(snn); --cnt; } } /* All right, put a lid on 50 snapshots */ while (cnt > 50) { snn=sl; sl=sl->next; delete_snapshot(snn); --cnt; } while (sl) { snn=sl; sl=sl->next; free(snn->filename); free(snn->prev); free(snn); } return rc; } /* ** Something changed in the folder, so next time snapshot_save() was called, ** take a snapshot. */ void snapshot_needed() { index_dirty=1; } /* ** Save a snapshot, if the folder was changed. */ void snapshot_save() { int rc; struct maildir_tmpcreate_info createInfo; FILE *fp; unsigned long i; const char *q; if (!index_dirty || !snapshots_enabled) return; index_dirty=0; maildir_tmpcreate_init(&createInfo); createInfo.maildir=current_mailbox; createInfo.uniq="snapshot"; createInfo.hostname=getenv("HOSTNAME"); createInfo.doordie=1; if ((rc=maildir_tmpcreate_fd(&createInfo)) < 0) { perror("maildir_tmpcreate_fd"); return; } close(rc); q=strrchr(createInfo.tmpname, '/'); /* Always there */ free(createInfo.newname); createInfo.newname=malloc(strlen(snapshot_dir)+strlen(q)+2); if (!createInfo.newname) { unlink(createInfo.tmpname); maildir_tmpcreate_free(&createInfo); perror("malloc"); return; } strcat(strcat(strcpy(createInfo.newname, snapshot_dir), "/"), q); if ((fp=fopen(createInfo.tmpname, "w")) == NULL) { perror(createInfo.tmpname); maildir_tmpcreate_free(&createInfo); return; } fprintf(fp, "%d %lu %lu %lu", SNAPSHOTVERSION, current_maildir_info.nmessages, current_maildir_info.uidv, current_maildir_info.nextuid); if (snapshot_cur) fprintf(fp, ":%s", strrchr(snapshot_cur, '/')+1); fprintf(fp, "\n"); for (i=0; ifilename, MDIRSEP[0]); fprintf(fp, "%lu:%s\n", p->uid, q ? q+1:""); } if (keywords()) imapscan_saveKeywordSnapshot(fp, ¤t_maildir_info); if (fflush(fp) < 0 || ferror(fp) < 0) { fclose(fp); perror(createInfo.tmpname); unlink(createInfo.tmpname); maildir_tmpcreate_free(&createInfo); return; } fclose(fp); if (rename(createInfo.tmpname, createInfo.newname) < 0) { perror(createInfo.tmpname); unlink(createInfo.tmpname); maildir_tmpcreate_free(&createInfo); return; } if (snapshot_last) { unlink(snapshot_last); /* Obsolete snapshot */ free(snapshot_last); } snapshot_last=snapshot_cur; snapshot_cur=createInfo.newname; createInfo.newname=NULL; maildir_tmpcreate_free(&createInfo); writes("* SNAPSHOT "); smapword(strrchr(snapshot_cur, '/')+1); writes("\n"); }