This is a very small patch for a very big change (and incomplete, it doesn't alter rehash, dohash, etc) Anyway... Considering the fast-rename and extending it to delete, we have the following problem: folders: user.brong user.brong.foo user.brong.foo.bar Delete/rename user.brong.foo WITHOUT affecting user.brong.foo.bar. On disk these are: BASEDIR/b/user/brong/ BASEDIR/b/user/brong/foo/ BASEDIR/b/user/brong/foo/bar/ So you either have to: a) move every single non "bar" file to the new location, or b) move user/brong/foo to the new location, then create a new user/brong/foo and move bar back into it. I realised there was: c) change folder layout on disk such that sub folders in the IMAP world aren't sub-folders in the on-disk world. (c) started looking more attractive all the time, especially since I've seen it at work with my Maildir copy which offlineimap keeps up to date for me. The following patch creates: BASEDIR/user/b/brong/user.brong/ BASEDIR/user/b/brong/user.brong.foo/ BASEDIR/user/b/brong/user.brong.foo.bar/ and handles shared namespace, etc as: BASEDIR/shared/shared BASEDIR/shared/shared.foo BASEDIR/shared/shared.foo.bar Basically, if mboxlist_isusermailbox(name, 0) is true then it gets hashed the first way, otherwise the second. This has the nice property with our rename/delete patch (also attached) and required for this patch you get: BASEDIR/user/b/brong/DELETED.user.brong.foo.TIMESTAMP/ keeping the user's stuff all together on disk. I really do prefer the "each IMAP folder is a different folder" and "hashing based on realistic use patterns" (at least for us... and I'm a believer in Hans Reiser's attitude that if your filesystem can't handle a folder with thousands of items then your filesystem needs fixing) I'll be working on fast-rename in this new universe next, but I thought I should throw this out there for comments. So, what do you think (and yes, I'll be making rehash at least work happily with this, because we'll need to do it ourselves. Current plan - down the replica, rehash it, change the config, bring it back up. Failover, rinse, repeat) Bron. P.S. I'm using quilt now to work on these patches. It's an order of magnitude nicer than working on each patch in isolation and using diff/patch by hand, but it means that patches are now in a series that apply one after the other rather than all against the raw upstream source. I hope to post my entire quilt series once I fix the one thing that has a FastMail specific key encoded in it to use a config option instead. -- Bron Gondwana brong@xxxxxxxxxxx
Index: cyrus-imapd-2.3.9/imap/mailbox.c =================================================================== --- cyrus-imapd-2.3.9.orig/imap/mailbox.c 2007-08-21 01:28:56.000000000 -0400 +++ cyrus-imapd-2.3.9/imap/mailbox.c 2007-08-21 01:36:23.000000000 -0400 @@ -3362,6 +3362,7 @@ { const char *idx; char c, *p; + char itemname[MAX_MAILBOX_PATH+1]; snprintf(buf, buf_len, "%s", root); buf_len -= strlen(buf); @@ -3369,7 +3370,7 @@ if (config_virtdomains && (p = strchr(name, '!'))) { *p = '\0'; /* split domain!user */ - if (config_hashimapspool) { + if (config_hashimapspool != IMAP_ENUM_HASHIMAPSPOOL_OFF) { c = (char) dir_hash_c(name, config_fulldirhash); snprintf(buf, buf_len, "%s%c/%s", FNAME_DOMAINDIR, c, name); } @@ -3382,7 +3383,7 @@ buf += strlen(buf); } - if (config_hashimapspool) { + if (config_hashimapspool == IMAP_ENUM_HASHIMAPSPOOL_ON) { idx = strchr(name, '.'); if (idx == NULL) { idx = name; @@ -3392,13 +3393,30 @@ c = (char) dir_hash_c(idx, config_fulldirhash); snprintf(buf, buf_len, "/%c/%s", c, name); + + /* change all '.'s to '/' */ + for (p = buf; *p; p++) { + if (*p == '.') *p = '/'; + } + } else if (config_hashimapspool == IMAP_ENUM_HASHIMAPSPOOL_USERID) { + if (idx = mboxname_isusermailbox(name, 0)) { + c = (char) dir_hash_c(idx, config_fulldirhash); + strncpy(itemname, idx, MAX_MAILBOX_PATH); + if (p = strchr(itemname, '.')) *p = '\0'; /* trim to username */ + snprintf(buf, buf_len, "/user/%c/%s/%s", c, itemname, name); + } else { + strncpy(itemname, name, MAX_MAILBOX_PATH); + if (p = strchr(itemname, '.')) *p = '\0'; /* trim to first part */ + snprintf(buf, buf_len, "/%s/%s", itemname, name); + } } else { /* standard mailbox placement */ snprintf(buf, buf_len, "/%s", name); - } - /* change all '.'s to '/' */ - for (p = buf; *p; p++) { - if (*p == '.') *p = '/'; + /* change all '.'s to '/' */ + for (p = buf; *p; p++) { + if (*p == '.') *p = '/'; + } } + } Index: cyrus-imapd-2.3.9/lib/imapoptions =================================================================== --- cyrus-imapd-2.3.9.orig/lib/imapoptions 2007-08-21 01:28:56.000000000 -0400 +++ cyrus-imapd-2.3.9/lib/imapoptions 2007-08-21 01:28:59.000000000 -0400 @@ -266,10 +266,14 @@ server must be quiesced and then the directories moved with the \fBrehash\fR utility. */ -{ "hashimapspool", 0, SWITCH } +{ "hashimapspool", "off", ENUM("off", "userid", "on") } /* If enabled, the partitions will also be hashed, in addition to the hashing done on configuration directories. This is recommended if - one partition has a very bushy mailbox tree. */ + one partition has a very bushy mailbox tree. +.PP + If set to "userid" then the second item will be stripped out as + the directory path and the other dots won't be converted: + e.g. /b/brong/user.brong.Trash/ */ # Commented out - there's no such thing as "hostname_mechs", but we need # this for the man page Index: cyrus-imapd-2.3.9/lib/libconfig.c =================================================================== --- cyrus-imapd-2.3.9.orig/lib/libconfig.c 2007-08-21 01:28:56.000000000 -0400 +++ cyrus-imapd-2.3.9/lib/libconfig.c 2007-08-21 01:28:59.000000000 -0400 @@ -74,7 +74,7 @@ const char *config_mupdate_server = NULL;/* NULL */ const char *config_defdomain = NULL; /* NULL */ const char *config_ident = NULL; /* the service name */ -int config_hashimapspool; /* f */ +enum enum_value config_hashimapspool; /* f */ enum enum_value config_virtdomains; /* f */ enum enum_value config_mupdate_config; /* IMAP_ENUM_MUPDATE_CONFIG_STANDARD */ @@ -274,7 +274,7 @@ } /* look up mailbox hashing */ - config_hashimapspool = config_getswitch(IMAPOPT_HASHIMAPSPOOL); + config_hashimapspool = config_getenum(IMAPOPT_HASHIMAPSPOOL); /* are we supporting virtual domains? */ config_virtdomains = config_getenum(IMAPOPT_VIRTDOMAINS); Index: cyrus-imapd-2.3.9/lib/libconfig.h =================================================================== --- cyrus-imapd-2.3.9.orig/lib/libconfig.h 2007-08-21 01:28:56.000000000 -0400 +++ cyrus-imapd-2.3.9/lib/libconfig.h 2007-08-21 01:28:59.000000000 -0400 @@ -71,7 +71,7 @@ extern const char *config_mupdate_server; extern const char *config_defdomain; extern const char *config_ident; -extern int config_hashimapspool; +extern enum enum_value config_hashimapspool; extern int config_implicitrights; extern enum enum_value config_virtdomains; extern enum enum_value config_mupdate_config;
Index: cyrus-imapd-2.3.9/imap/cyr_expire.c =================================================================== --- cyrus-imapd-2.3.9.orig/imap/cyr_expire.c 2007-08-16 21:42:14.000000000 -0400 +++ cyrus-imapd-2.3.9/imap/cyr_expire.c 2007-08-16 21:46:47.000000000 -0400 @@ -91,6 +91,20 @@ int skip_annotate; }; +struct delete_node { + struct delete_node *next; + char *name; +}; + +struct delete_rock { + char prefix[100]; + int prefixlen; + time_t delete_mark; + struct delete_node *head; + struct delete_node *tail; + int verbose; +}; + /* * mailbox_expunge() callback to expunge expired articles. */ @@ -273,14 +287,72 @@ return 0; } +int delete(char *name, int matchlen, int maycreate __attribute__((unused)), + void *rock) +{ + struct delete_rock *drock = (struct delete_rock *) rock; + char fnamebuf[MAX_MAILBOX_PATH+1]; + struct stat sbuf; + char *p; + int i, r, domainlen = 0; + struct delete_node *node; + int mbtype; + char *path, *mpath; + + if (config_virtdomains && (p = strchr(name, '!'))) + domainlen = p - name + 1; + + /* check if this is a mailbox we want to examine */ + if (strncmp(name+domainlen, drock->prefix, drock->prefixlen)) + return 0; + + /* Skip remote mailboxes */ + r = mboxlist_detail(name, &mbtype, &path, &mpath, NULL, NULL, NULL); + if (r) { + if (drock->verbose) { + printf("error looking up %s: %s\n", name, error_message(r)); + } + return 1; + } + if (mbtype & MBTYPE_REMOTE) return 0; + + /* check that the header is older than the number of days we care about */ + if (mpath && + (config_metapartition_files & + IMAP_ENUM_METAPARTITION_FILES_HEADER)) { + strlcpy(fnamebuf, mpath, sizeof(fnamebuf)); + } else { + strlcpy(fnamebuf, path, sizeof(fnamebuf)); + } + strlcat(fnamebuf, FNAME_HEADER, sizeof(fnamebuf)); + if (stat(fnamebuf, &sbuf)) return 0; + if ((sbuf.st_mtime == 0) || (sbuf.st_mtime > drock->delete_mark)) + return(0); + + /* Add this mailbox to list of mailboxes to delete */ + node = xmalloc(sizeof(struct delete_node)); + node->next = NULL; + node->name = xstrdup(name); + + if (drock->tail) { + drock->tail->next = node; + drock->tail = node; + } else { + drock->head = drock->tail = node; + } + return(0); +} + int main(int argc, char *argv[]) { extern char *optarg; - int opt, r = 0, expire_days = 0, expunge_days = 0; + int opt, r = 0, expire_days = 0, expunge_days = 0, delete_days = 0; char *alt_config = NULL; char buf[100]; struct hash_table expire_table; struct expire_rock erock; + struct delete_rock drock; + const char *deleteprefix; if ((geteuid()) == 0 && (become_cyrus() != 0)) { fatal("must run as the Cyrus user", EC_USAGE); @@ -288,13 +360,19 @@ /* zero the expire_rock */ memset(&erock, 0, sizeof(erock)); + memset(&drock, 0, sizeof(drock)); - while ((opt = getopt(argc, argv, "C:E:X:va")) != EOF) { + while ((opt = getopt(argc, argv, "C:D:E:X:va")) != EOF) { switch (opt) { case 'C': /* alt config file */ alt_config = optarg; break; + case 'D': + if (delete_days) usage(); + delete_days = atoi(optarg); + break; + case 'E': if (expire_days) usage(); expire_days = atoi(optarg); @@ -307,6 +385,7 @@ case 'v': erock.verbose++; + drock.verbose++; break; case 'a': @@ -367,6 +446,44 @@ erock.deleted, erock.messages, erock.mailboxes); } + if (mboxlist_delayed_delete_isenabled() && + (deleteprefix = config_getstring(IMAPOPT_DELETEPREFIX))) { + struct delete_node *node; + int count = 0; + + if (drock.verbose) { + fprintf(stderr, + "Removing deleted mailboxes older than %d days\n", + delete_days); + } + + strlcpy(drock.prefix, deleteprefix, sizeof(drock.prefix)); + strlcat(drock.prefix, ".", sizeof(drock.prefix)); + drock.prefixlen = strlen(drock.prefix); + drock.delete_mark = time(0) - (delete_days * 60 * 60 * 24); + drock.head = NULL; + drock.tail = NULL; + + mboxlist_findall(NULL, buf, 1, 0, 0, &delete, &drock); + + for (node = drock.head ; node ; node = node->next) { + if (drock.verbose) { + fprintf(stderr, "Removing: %s\n", node->name); + } + r = mboxlist_deletemailbox(node->name, 1, NULL, NULL, 0, 0, 0); + count++; + } + + if (drock.verbose) { + if (count != 1) { + fprintf(stderr, "Removed %d deleted mailboxes\n", count); + } else { + fprintf(stderr, "Removed 1 deleted mailbox\n"); + } + } + syslog(LOG_NOTICE, "Removed %d deleted mailboxes", count); + } + /* purge deliver.db entries of expired messages */ r = duplicate_prune(expire_days, &expire_table); Index: cyrus-imapd-2.3.9/imap/imapd.c =================================================================== --- cyrus-imapd-2.3.9.orig/imap/imapd.c 2007-08-16 21:42:58.000000000 -0400 +++ cyrus-imapd-2.3.9/imap/imapd.c 2007-08-16 21:45:58.000000000 -0400 @@ -5016,9 +5016,19 @@ { int r; - r = mboxlist_deletemailbox(name, imapd_userisadmin, - imapd_userid, imapd_authstate, - 0, 0, 0); + if (!mboxlist_delayed_delete_isenabled()) { + r = mboxlist_deletemailbox(name, imapd_userisadmin, + imapd_userid, imapd_authstate, + 0, 0, 0); + } else if (imapd_userisadmin && mboxlist_in_deleteprefix(name)) { + r = mboxlist_deletemailbox(name, imapd_userisadmin, + imapd_userid, imapd_authstate, + 0, 0, 0); + } else { + r = mboxlist_delayed_deletemailbox(name, imapd_userisadmin, + imapd_userid, imapd_authstate, + 0, 0, 0); + } if (!r) sync_log_mailbox(name); @@ -5098,9 +5108,20 @@ if (config_virtdomains && (p = strchr(mailboxname, '!'))) domainlen = p - mailboxname + 1; - r = mboxlist_deletemailbox(mailboxname, imapd_userisadmin, - imapd_userid, imapd_authstate, 1-force, - localonly, 0); + if (localonly || !mboxlist_delayed_delete_isenabled()) { + r = mboxlist_deletemailbox(mailboxname, imapd_userisadmin, + imapd_userid, imapd_authstate, + 1-force, localonly, 0); + } else if (imapd_userisadmin && + mboxlist_in_deleteprefix(mailboxname)) { + r = mboxlist_deletemailbox(mailboxname, imapd_userisadmin, + imapd_userid, imapd_authstate, + 0 /* checkacl */, localonly, 0); + } else { + r = mboxlist_delayed_deletemailbox(mailboxname, imapd_userisadmin, + imapd_userid, imapd_authstate, + 1-force, localonly, 0); + } } /* was it a top-level user mailbox? */ @@ -9323,6 +9344,12 @@ else strlcpy(mboxname, lastname, sizeof(mboxname)); + /* Suppress DELETED hierachy unless admin */ + if (!imapd_userisadmin && + mboxlist_delayed_delete_isenabled() && + mboxlist_in_deleteprefix(mboxname)) + return; + /* Look it up */ nonexistent = mboxlist_detail(mboxname, &mbtype, NULL, NULL, NULL, NULL, NULL); Index: cyrus-imapd-2.3.9/imap/mboxlist.c =================================================================== --- cyrus-imapd-2.3.9.orig/imap/mboxlist.c 2007-08-16 21:42:14.000000000 -0400 +++ cyrus-imapd-2.3.9/imap/mboxlist.c 2007-08-16 21:46:47.000000000 -0400 @@ -82,6 +82,7 @@ #include "mboxlist.h" #include "quota.h" +#include "sync_log.h" #define DB config_mboxlist_db #define SUBDB config_subscription_db @@ -875,6 +876,103 @@ } /* + * Delayed Delete a mailbox: translate delete into rename + * + * XXX local_only? + */ +int +mboxlist_delayed_deletemailbox(const char *name, int isadmin, char *userid, + struct auth_state *auth_state, int checkacl, + int local_only, int force) +{ + char newname[MAX_MAILBOX_PATH+1]; + char *path, *mpath; + char *acl; + char *partition; + int r; + long access; + struct mailbox mailbox; + int deletequotaroot = 0; + struct txn *tid = NULL; + int isremote = 0; + int mbtype; + const char *p; + const char *deleteprefix = config_getstring(IMAPOPT_DELETEPREFIX); + int domainlen = 0; + struct timeval tv; + + if(!isadmin && force) return IMAP_PERMISSION_DENIED; + + /* Check for request to delete a user: + user.<x> with no dots after it */ + if ((p = mboxname_isusermailbox(name, 1))) { + /* Can't DELETE INBOX (your own inbox) */ + if (userid) { + int len = config_virtdomains ? + strcspn(userid, "@") : strlen(userid); + if ((len == strlen(p)) && !strncmp(p, userid, len)) { + return(IMAP_MAILBOX_NOTSUPPORTED); + } + } + + /* Only admins may delete user */ + if (!isadmin) return(IMAP_PERMISSION_DENIED); + } + + do { + r = mboxlist_mylookup(name, &mbtype, + &path, &mpath, &partition, &acl, NULL, 1); + } while (r == IMAP_AGAIN); + + if (r) return(r); + + isremote = mbtype & MBTYPE_REMOTE; + + /* are we reserved? (but for remote mailboxes this is okay, since + * we don't touch their data files at all) */ + if(!isremote && (mbtype & MBTYPE_RESERVE) && !force) { + return(IMAP_MAILBOX_RESERVED); + } + + /* check if user has Delete right (we've already excluded non-admins + * from deleting a user mailbox) */ + if (checkacl) { + access = cyrus_acl_myrights(auth_state, acl); + if(!(access & ACL_DELETEMBOX)) { + /* User has admin rights over their own mailbox namespace */ + if (mboxname_userownsmailbox(userid, name) && + (config_implicitrights & ACL_ADMIN)) { + isadmin = 1; + } + + /* Lie about error if privacy demands */ + r = (isadmin || (access & ACL_LOOKUP)) ? + IMAP_PERMISSION_DENIED : IMAP_MAILBOX_NONEXISTENT; + return(r); + } + } + + if (config_virtdomains && (p = strchr(name, '!'))) + domainlen = p - name + 1; + + gettimeofday( &tv, NULL ); + + if (domainlen && domainlen < sizeof(newname)) + strncpy(newname, name, domainlen); + snprintf(newname+domainlen, sizeof(newname)-domainlen, "%s.%s.%X", + deleteprefix, name+domainlen, tv.tv_sec); + + /* Get mboxlist_renamemailbox to do the hard work. No ACL checks needed */ + r = mboxlist_renamemailbox((char *)name, newname, partition, + 1 /* isadmin */, userid, + auth_state, force); + + /* don't forget to log the rename! */ + sync_log_mailbox_double((char *)name, newname); + return r; +} + +/* * Delete a mailbox. * Deleting the mailbox user.FOO may only be performed by an admin. * @@ -3261,3 +3359,42 @@ return DB->abort(mbdb, tid); } + +int +mboxlist_delayed_delete_isenabled(void) +{ + static int defined = 0; + static enum enum_value config_delete_mode; + + if (!defined) { + defined = 1; + config_delete_mode = config_getenum(IMAPOPT_DELETE_MODE); + } + + return(config_delete_mode == IMAP_ENUM_DELETE_MODE_DELAYED); +} + +int mboxlist_in_deleteprefix(const char *mailboxname) +{ + static int defined = 0; + static const char *deleteprefix = NULL; + static int deleteprefix_len = 0; + int domainlen = 0; + char *p; + + if (!defined) { + defined = 1; + deleteprefix = config_getstring(IMAPOPT_DELETEPREFIX); + if (deleteprefix) + deleteprefix_len = strlen(deleteprefix); + } + + if (!deleteprefix || !mboxlist_delayed_delete_isenabled()) + return(0); + + if (config_virtdomains && (p = strchr(mailboxname, '!'))) + domainlen = p - mailboxname + 1; + + return ((!strncmp(mailboxname + domainlen, deleteprefix, deleteprefix_len) && + mailboxname[domainlen + deleteprefix_len] == '.') ? 1 : 0); +} Index: cyrus-imapd-2.3.9/imap/mboxlist.h =================================================================== --- cyrus-imapd-2.3.9.orig/imap/mboxlist.h 2007-08-16 21:42:14.000000000 -0400 +++ cyrus-imapd-2.3.9/imap/mboxlist.h 2007-08-16 21:45:58.000000000 -0400 @@ -118,6 +118,12 @@ struct auth_state *auth_state, int localonly, int forceuser, int dbonly); +/* delated delete */ +/* Translate delete into rename */ +int +mboxlist_delayed_deletemailbox(const char *name, int isadmin, char *userid, + struct auth_state *auth_state, int checkacl, + int local_only, int force); /* Delete a mailbox. */ /* setting local_only disables any communication with the mupdate server * and deletes the mailbox from the filesystem regardless of if it is @@ -204,4 +210,6 @@ int mboxlist_commit(struct txn *tid); int mboxlist_abort(struct txn *tid); +int mboxlist_delayed_delete_isenabled(void); +int mboxlist_in_deleteprefix(const char *mailboxname); #endif Index: cyrus-imapd-2.3.9/imap/mboxname.c =================================================================== --- cyrus-imapd-2.3.9.orig/imap/mboxname.c 2007-08-16 21:42:14.000000000 -0400 +++ cyrus-imapd-2.3.9/imap/mboxname.c 2007-08-16 21:46:47.000000000 -0400 @@ -599,11 +599,25 @@ char *mboxname_isusermailbox(const char *name, int isinbox) { const char *p; + const char *start = name; + const char *deleteprefix = config_getstring(IMAPOPT_DELETEPREFIX); + const char sep = config_getswitch(IMAPOPT_UNIXHIERARCHYSEP) ? '/' : '.'; + int len = strlen(deleteprefix); + int isdel = 0; + + /* step past the domain part */ + if (config_virtdomains && (p = strchr(start, '!'))) + start = p + 1; + + /* step past any deleted bit */ + if (mboxlist_delayed_delete_isenabled() && strlen(start) > len && !strncmp(start, deleteprefix, len) && start[len] == sep) { + start += len + 1; + isdel = 1; /* there's an additional sep + hextimestamp on isdel folders */ + } - if (((!strncmp(name, "user.", 5) && (p = name+5)) || - ((p = strstr(name, "!user.")) && (p += 6))) && - (!isinbox || !strchr(p, '.'))) - return (char*) p; + if (strlen(start) > 5 && !strncmp(start, "user", 4) && start[4] == sep && + (!isinbox || !strchr(start+5, sep)) || (isdel && (p = strchr(start+5, sep)) && !strchr(p+1, sep))) + return (char*) start+5; else return NULL; } Index: cyrus-imapd-2.3.9/lib/imapoptions =================================================================== --- cyrus-imapd-2.3.9.orig/lib/imapoptions 2007-08-16 21:42:58.000000000 -0400 +++ cyrus-imapd-2.3.9/lib/imapoptions 2007-08-16 21:47:21.000000000 -0400 @@ -201,6 +201,16 @@ { "defaultpartition", "default", STRING } /* The partition name used by default for new mailboxes. */ +{ "deleteprefix", "DELETED", STRING } +/* Location for deleted mailboxes, if "delete_mode" set to be "delayed" */ + +{ "delete_mode", "immediate", ENUM("immediate", "delayed") } +/* The manner in which mailboxes are deleted. "Immediate" mode is the + default behavior in which mailboxes are removed immediately. In + "Delayed" mode, mailboxes are renamed to a special hiearchy defined + by the "deleteprefix" option to be removed later by cyr_expire. +*/ + { "deleteright", "c", STRING } /* Deprecated - only used for backwards compatibility with existing installations. Lists the old RFC 2086 right which was used to Index: cyrus-imapd-2.3.9/man/cyr_expire.8 =================================================================== --- cyrus-imapd-2.3.9.orig/man/cyr_expire.8 2007-08-16 21:42:14.000000000 -0400 +++ cyrus-imapd-2.3.9/man/cyr_expire.8 2007-08-16 21:44:30.000000000 -0400 @@ -48,6 +48,9 @@ .B \-C .I config-file ] +[ +.BI \-D " delete-days" +] .BI \-E " expire-days" [ .BI \-X " expunge-days" @@ -84,6 +87,11 @@ .BI \-C " config-file" Read configuration options from \fIconfig-file\fR. .TP +\fB\-D \fIdelete-days\fR +Remove previously deleted mailboxes older than \fIdelete-days\fR +(when using the "delayed" delete mode). The default is 0 (zero) +days, which will delete \fBall\fR previously deleted mailboxes. +.TP \fB\-E \fIexpire-days\fR Prune the duplicate database of entries older than \fIexpire-days\fR. This value is only used for entries which do not have a corresponding
---- Cyrus Home Page: http://cyrusimap.web.cmu.edu/ Cyrus Wiki/FAQ: http://cyrusimap.web.cmu.edu/twiki List Archives/Info: http://asg.web.cmu.edu/cyrus/mailing-list.html