From: Richard Russon Date: Mon, 23 Oct 2017 13:39:53 +0000 (+0100) Subject: rearrange functions X-Git-Tag: neomutt-20171208~71^2~3 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=d58a649a71e922009c670afc7ed563bd71f87479;p=neomutt rearrange functions No code changes Static functions first General public functions Mailbox functions --- diff --git a/imap/auth_cram.c b/imap/auth_cram.c index c0ab69222..bc4745da9 100644 --- a/imap/auth_cram.c +++ b/imap/auth_cram.c @@ -37,8 +37,54 @@ #define MD5_BLOCK_LEN 64 #define MD5_DIGEST_LEN 16 -/* forward declarations */ -static void hmac_md5(const char *password, char *challenge, unsigned char *response); +/** + * hmac_md5 - hmac_md5: produce CRAM-MD5 challenge response + */ +static void hmac_md5(const char *password, char *challenge, unsigned char *response) +{ + struct Md5Ctx ctx; + unsigned char ipad[MD5_BLOCK_LEN], opad[MD5_BLOCK_LEN]; + unsigned char secret[MD5_BLOCK_LEN + 1]; + unsigned char hash_passwd[MD5_DIGEST_LEN]; + unsigned int secret_len, chal_len; + + secret_len = strlen(password); + chal_len = strlen(challenge); + + /* passwords longer than MD5_BLOCK_LEN bytes are substituted with their MD5 + * digests */ + if (secret_len > MD5_BLOCK_LEN) + { + md5_buffer(password, secret_len, hash_passwd); + strfcpy((char *) secret, (char *) hash_passwd, MD5_DIGEST_LEN); + secret_len = MD5_DIGEST_LEN; + } + else + strfcpy((char *) secret, password, sizeof(secret)); + + memset(ipad, 0, sizeof(ipad)); + memset(opad, 0, sizeof(opad)); + memcpy(ipad, secret, secret_len); + memcpy(opad, secret, secret_len); + + for (int i = 0; i < MD5_BLOCK_LEN; i++) + { + ipad[i] ^= 0x36; + opad[i] ^= 0x5c; + } + + /* inner hash: challenge and ipadded secret */ + md5_init_ctx(&ctx); + md5_process_bytes(ipad, MD5_BLOCK_LEN, &ctx); + md5_process_bytes(challenge, chal_len, &ctx); + md5_finish_ctx(&ctx, response); + + /* outer hash: inner hash and opadded secret */ + md5_init_ctx(&ctx); + md5_process_bytes(opad, MD5_BLOCK_LEN, &ctx); + md5_process_bytes(response, MD5_DIGEST_LEN, &ctx); + md5_finish_ctx(&ctx, response); +} /** * imap_auth_cram_md5 - imap_auth_cram_md5: AUTH=CRAM-MD5 support @@ -138,52 +184,3 @@ bail: mutt_sleep(2); return IMAP_AUTH_FAILURE; } - -/** - * hmac_md5 - hmac_md5: produce CRAM-MD5 challenge response - */ -static void hmac_md5(const char *password, char *challenge, unsigned char *response) -{ - struct Md5Ctx ctx; - unsigned char ipad[MD5_BLOCK_LEN], opad[MD5_BLOCK_LEN]; - unsigned char secret[MD5_BLOCK_LEN + 1]; - unsigned char hash_passwd[MD5_DIGEST_LEN]; - unsigned int secret_len, chal_len; - - secret_len = strlen(password); - chal_len = strlen(challenge); - - /* passwords longer than MD5_BLOCK_LEN bytes are substituted with their MD5 - * digests */ - if (secret_len > MD5_BLOCK_LEN) - { - md5_buffer(password, secret_len, hash_passwd); - strfcpy((char *) secret, (char *) hash_passwd, MD5_DIGEST_LEN); - secret_len = MD5_DIGEST_LEN; - } - else - strfcpy((char *) secret, password, sizeof(secret)); - - memset(ipad, 0, sizeof(ipad)); - memset(opad, 0, sizeof(opad)); - memcpy(ipad, secret, secret_len); - memcpy(opad, secret, secret_len); - - for (int i = 0; i < MD5_BLOCK_LEN; i++) - { - ipad[i] ^= 0x36; - opad[i] ^= 0x5c; - } - - /* inner hash: challenge and ipadded secret */ - md5_init_ctx(&ctx); - md5_process_bytes(ipad, MD5_BLOCK_LEN, &ctx); - md5_process_bytes(challenge, chal_len, &ctx); - md5_finish_ctx(&ctx, response); - - /* outer hash: inner hash and opadded secret */ - md5_init_ctx(&ctx); - md5_process_bytes(opad, MD5_BLOCK_LEN, &ctx); - md5_process_bytes(response, MD5_DIGEST_LEN, &ctx); - md5_finish_ctx(&ctx, response); -} diff --git a/imap/imap.c b/imap/imap.c index c17bd6076..1b037886a 100644 --- a/imap/imap.c +++ b/imap/imap.c @@ -64,988 +64,998 @@ #include "mutt_ssl.h" #endif -/* imap forward declarations */ -static char *imap_get_flags(struct ListHead *hflags, char *s); -static int imap_check_capabilities(struct ImapData *idata); -static void imap_set_flag(struct ImapData *idata, int aclbit, int flag, - const char *str, char *flags, size_t flsize); - /** - * imap_access - Check permissions on an IMAP mailbox - * - * TODO: ACL checks. Right now we assume if it exists we can mess with it. + * imap_check_capabilities - Make sure we can log in to this server */ -int imap_access(const char *path) +static int imap_check_capabilities(struct ImapData *idata) { - struct ImapData *idata = NULL; - struct ImapMbox mx; - char buf[LONG_STRING]; - char mailbox[LONG_STRING]; - char mbox[LONG_STRING]; - int rc; - - if (imap_parse_path(path, &mx)) - return -1; - - idata = imap_conn_find(&mx.account, option(OPT_IMAP_PASSIVE) ? MUTT_IMAP_CONN_NONEW : 0); - if (!idata) + if (imap_exec(idata, "CAPABILITY", 0) != 0) { - FREE(&mx.mbox); + imap_error("imap_check_capabilities", idata->buf); return -1; } - imap_fix_path(idata, mx.mbox, mailbox, sizeof(mailbox)); - if (!*mailbox) - strfcpy(mailbox, "INBOX", sizeof(mailbox)); - - /* we may already be in the folder we're checking */ - if (mutt_strcmp(idata->mailbox, mx.mbox) == 0) - { - FREE(&mx.mbox); - return 0; - } - FREE(&mx.mbox); - - if (imap_mboxcache_get(idata, mailbox, 0)) + if (!(mutt_bit_isset(idata->capabilities, IMAP4) || + mutt_bit_isset(idata->capabilities, IMAP4REV1))) { - mutt_debug(3, "imap_access: found %s in cache\n", mailbox); - return 0; - } - - imap_munge_mbox_name(idata, mbox, sizeof(mbox), mailbox); + mutt_error( + _("This IMAP server is ancient. NeoMutt does not work with it.")); + mutt_sleep(2); /* pause a moment to let the user see the error */ - if (mutt_bit_isset(idata->capabilities, IMAP4REV1)) - snprintf(buf, sizeof(buf), "STATUS %s (UIDVALIDITY)", mbox); - else if (mutt_bit_isset(idata->capabilities, STATUS)) - snprintf(buf, sizeof(buf), "STATUS %s (UID-VALIDITY)", mbox); - else - { - mutt_debug(2, "imap_access: STATUS not supported?\n"); return -1; } - rc = imap_exec(idata, buf, IMAP_CMD_FAIL_OK); - if (rc < 0) - { - mutt_debug(1, "imap_access: Can't check STATUS of %s\n", mbox); - return rc; - } - return 0; } -int imap_create_mailbox(struct ImapData *idata, char *mailbox) +/** + * imap_get_flags - Make a simple list out of a FLAGS response + * + * return stream following FLAGS response + */ +static char *imap_get_flags(struct ListHead *hflags, char *s) { - char buf[LONG_STRING], mbox[LONG_STRING]; - - imap_munge_mbox_name(idata, mbox, sizeof(mbox), mailbox); - snprintf(buf, sizeof(buf), "CREATE %s", mbox); + char *flag_word = NULL; + char ctmp; - if (imap_exec(idata, buf, 0) != 0) + /* sanity-check string */ + if (mutt_strncasecmp("FLAGS", s, 5) != 0) { - mutt_error(_("CREATE failed: %s"), imap_cmd_trailer(idata)); - return -1; + mutt_debug(1, "imap_get_flags: not a FLAGS response: %s\n", s); + return NULL; } - - return 0; -} - -int imap_rename_mailbox(struct ImapData *idata, struct ImapMbox *mx, const char *newname) -{ - char oldmbox[LONG_STRING]; - char newmbox[LONG_STRING]; - char buf[LONG_STRING]; - - imap_munge_mbox_name(idata, oldmbox, sizeof(oldmbox), mx->mbox); - imap_munge_mbox_name(idata, newmbox, sizeof(newmbox), newname); - - snprintf(buf, sizeof(buf), "RENAME %s %s", oldmbox, newmbox); - - if (imap_exec(idata, buf, 0) != 0) - return -1; - - return 0; -} - -int imap_delete_mailbox(struct Context *ctx, struct ImapMbox *mx) -{ - char buf[LONG_STRING], mbox[LONG_STRING]; - struct ImapData *idata = NULL; - - if (!ctx || !ctx->data) + s += 5; + SKIPWS(s); + if (*s != '(') { - idata = imap_conn_find(&mx->account, - option(OPT_IMAP_PASSIVE) ? MUTT_IMAP_CONN_NONEW : 0); - if (!idata) - { - FREE(&mx->mbox); - return -1; - } + mutt_debug(1, "imap_get_flags: bogus FLAGS response: %s\n", s); + return NULL; } - else + + /* update caller's flags handle */ + while (*s && *s != ')') { - idata = ctx->data; + s++; + SKIPWS(s); + flag_word = s; + while (*s && (*s != ')') && !ISSPACE(*s)) + s++; + ctmp = *s; + *s = '\0'; + if (*flag_word) + mutt_list_insert_tail(hflags, safe_strdup(flag_word)); + *s = ctmp; } - imap_munge_mbox_name(idata, mbox, sizeof(mbox), mx->mbox); - snprintf(buf, sizeof(buf), "DELETE %s", mbox); + /* note bad flags response */ + if (*s != ')') + { + mutt_debug(1, "imap_get_flags: Unterminated FLAGS response: %s\n", s); + mutt_list_free(hflags); - if (imap_exec(idata, buf, 0) != 0) - return -1; + return NULL; + } - return 0; + s++; + + return s; } /** - * imap_logout_all - close all open connections + * imap_set_flag - append str to flags if we currently have permission * - * Quick and dirty until we can make sure we've got all the context we need. + * according to aclbit */ -void imap_logout_all(void) +static void imap_set_flag(struct ImapData *idata, int aclbit, int flag, + const char *str, char *flags, size_t flsize) { - struct ConnectionList *head = mutt_socket_head(); - struct Connection *np, *tmp; - TAILQ_FOREACH_SAFE(np, head, entries, tmp) - { - if (np->account.type == MUTT_ACCT_TYPE_IMAP && np->fd >= 0) - { - TAILQ_REMOVE(head, np, entries); - mutt_message(_("Closing connection to %s..."), np->account.host); - imap_logout((struct ImapData **) (void *) &np->data); - mutt_clear_error(); - mutt_socket_free(np); - } - } + if (mutt_bit_isset(idata->ctx->rights, aclbit)) + if (flag && imap_has_flag(&idata->flags, str)) + safe_strcat(flags, flsize, str); } /** - * imap_read_literal - Read bytes bytes from server into file + * imap_make_msg_set - Make a message set * - * Not explicitly buffered, relies on FILE buffering. NOTE: strips `\r` from - * `\r\n`. Apparently even literals use `\r\n`-terminated strings ?! + * Note: headers must be in SORT_ORDER. See imap_exec_msgset for args. + * Pos is an opaque pointer a la strtok. It should be 0 at first call. */ -int imap_read_literal(FILE *fp, struct ImapData *idata, long bytes, struct Progress *pbar) +static int imap_make_msg_set(struct ImapData *idata, struct Buffer *buf, + int flag, bool changed, bool invert, int *pos) { - char c; - bool r = false; + struct Header **hdrs = idata->ctx->hdrs; + int count = 0; /* number of messages in message set */ + bool match = false; /* whether current message matches flag condition */ + unsigned int setstart = 0; /* start of current message range */ + int n; + bool started = false; - mutt_debug(2, "imap_read_literal: reading %ld bytes\n", bytes); + hdrs = idata->ctx->hdrs; - for (long pos = 0; pos < bytes; pos++) + for (n = *pos; n < idata->ctx->msgcount && buf->dptr - buf->data < IMAP_MAX_CMDLEN; n++) { - if (mutt_socket_readchar(idata->conn, &c) != 1) - { - mutt_debug(1, "imap_read_literal: error during read, %ld bytes read\n", pos); - idata->status = IMAP_FATAL; - - return -1; - } + match = false; + /* don't include pending expunged messages */ + if (hdrs[n]->active) + switch (flag) + { + case MUTT_DELETED: + if (hdrs[n]->deleted != HEADER_DATA(hdrs[n])->deleted) + match = invert ^ hdrs[n]->deleted; + break; + case MUTT_FLAG: + if (hdrs[n]->flagged != HEADER_DATA(hdrs[n])->flagged) + match = invert ^ hdrs[n]->flagged; + break; + case MUTT_OLD: + if (hdrs[n]->old != HEADER_DATA(hdrs[n])->old) + match = invert ^ hdrs[n]->old; + break; + case MUTT_READ: + if (hdrs[n]->read != HEADER_DATA(hdrs[n])->read) + match = invert ^ hdrs[n]->read; + break; + case MUTT_REPLIED: + if (hdrs[n]->replied != HEADER_DATA(hdrs[n])->replied) + match = invert ^ hdrs[n]->replied; + break; - if (r && c != '\n') - fputc('\r', fp); + case MUTT_TAG: + if (hdrs[n]->tagged) + match = true; + break; + case MUTT_TRASH: + if (hdrs[n]->deleted && !hdrs[n]->purge) + match = true; + break; + } - if (c == '\r') + if (match && (!changed || hdrs[n]->changed)) { - r = true; - continue; + count++; + if (setstart == 0) + { + setstart = HEADER_DATA(hdrs[n])->uid; + if (!started) + { + mutt_buffer_printf(buf, "%u", HEADER_DATA(hdrs[n])->uid); + started = true; + } + else + mutt_buffer_printf(buf, ",%u", HEADER_DATA(hdrs[n])->uid); + } + /* tie up if the last message also matches */ + else if (n == idata->ctx->msgcount - 1) + mutt_buffer_printf(buf, ":%u", HEADER_DATA(hdrs[n])->uid); + } + /* End current set if message doesn't match or we've reached the end + * of the mailbox via inactive messages following the last match. */ + else if (setstart && (hdrs[n]->active || n == idata->ctx->msgcount - 1)) + { + if (HEADER_DATA(hdrs[n - 1])->uid > setstart) + mutt_buffer_printf(buf, ":%u", HEADER_DATA(hdrs[n - 1])->uid); + setstart = 0; } - else - r = false; - - fputc(c, fp); - - if (pbar && !(pos % 1024)) - mutt_progress_update(pbar, pos, -1); -#ifdef DEBUG - if (debuglevel >= IMAP_LOG_LTRL) - fputc(c, debugfile); -#endif } - return 0; + *pos = n; + + return count; } /** - * imap_expunge_mailbox - Purge messages from the server - * - * Purge IMAP portion of expunged messages from the context. Must not be done - * while something has a handle on any headers (eg inside pager or editor). - * That is, check IMAP_REOPEN_ALLOW. + * compare_flags_for_copy - Compare local flags against the server + * @retval 0 if neomutt's flags match cached server flags + * EXCLUDING the deleted flag. */ -void imap_expunge_mailbox(struct ImapData *idata) +static bool compare_flags_for_copy(struct Header *h) { - struct Header *h = NULL; - int cacheno; - short old_sort; - -#ifdef USE_HCACHE - idata->hcache = imap_hcache_open(idata, NULL); -#endif - - old_sort = Sort; - Sort = SORT_ORDER; - mutt_sort_headers(idata->ctx, 0); + struct ImapHeaderData *hd = (struct ImapHeaderData *) h->data; - for (int i = 0; i < idata->ctx->msgcount; i++) - { - h = idata->ctx->hdrs[i]; + if (h->read != hd->read) + return true; + if (h->old != hd->old) + return true; + if (h->flagged != hd->flagged) + return true; + if (h->replied != hd->replied) + return true; - if (h->index == INT_MAX) - { - mutt_debug(2, "Expunging message UID %d.\n", HEADER_DATA(h)->uid); + return false; +} - h->active = false; - idata->ctx->size -= h->content->length; +static int sync_helper(struct ImapData *idata, int right, int flag, const char *name) +{ + int count = 0; + int rc; + char buf[LONG_STRING]; - imap_cache_del(idata, h); -#ifdef USE_HCACHE - imap_hcache_del(idata, HEADER_DATA(h)->uid); -#endif + if (!idata->ctx) + return -1; - /* free cached body from disk, if necessary */ - cacheno = HEADER_DATA(h)->uid % IMAP_CACHE_LEN; - if (idata->cache[cacheno].uid == HEADER_DATA(h)->uid && - idata->cache[cacheno].path) - { - unlink(idata->cache[cacheno].path); - FREE(&idata->cache[cacheno].path); - } + if (!mutt_bit_isset(idata->ctx->rights, right)) + return 0; - int_hash_delete(idata->uid_hash, HEADER_DATA(h)->uid, h, NULL); + if (right == MUTT_ACL_WRITE && !imap_has_flag(&idata->flags, name)) + return 0; - imap_free_header_data((struct ImapHeaderData **) &h->data); - } - else - { - h->index = i; - /* Mutt has several places where it turns off h->active as a - * hack. For example to avoid FLAG updates, or to exclude from - * imap_exec_msgset. - * - * Unfortunately, when a reopen is allowed and the IMAP_EXPUNGE_PENDING - * flag becomes set (e.g. a flag update to a modified header), - * this function will be called by imap_cmd_finish(). - * - * The mx_update_tables() will free and remove these "inactive" headers, - * despite that an EXPUNGE was not received for them. - * This would result in memory leaks and segfaults due to dangling - * pointers in the msn_index and uid_hash. - * - * So this is another hack to work around the hacks. We don't want to - * remove the messages, so make sure active is on. - */ - h->active = true; - } - } + snprintf(buf, sizeof(buf), "+FLAGS.SILENT (%s)", name); + rc = imap_exec_msgset(idata, "UID STORE", buf, flag, 1, 0); + if (rc < 0) + return rc; + count += rc; -#ifdef USE_HCACHE - imap_hcache_close(idata); -#endif + buf[0] = '-'; + rc = imap_exec_msgset(idata, "UID STORE", buf, flag, 1, 1); + if (rc < 0) + return rc; + count += rc; - /* We may be called on to expunge at any time. We can't rely on the caller - * to always know to rethread */ - mx_update_tables(idata->ctx, false); - Sort = old_sort; - mutt_sort_headers(idata->ctx, 1); + return count; } /** - * imap_check_capabilities - Make sure we can log in to this server + * imap_get_mailbox - split path into (idata,mailbox name) */ -static int imap_check_capabilities(struct ImapData *idata) +static int imap_get_mailbox(const char *path, struct ImapData **hidata, char *buf, size_t blen) { - if (imap_exec(idata, "CAPABILITY", 0) != 0) + struct ImapMbox mx; + + if (imap_parse_path(path, &mx)) { - imap_error("imap_check_capabilities", idata->buf); + mutt_debug(1, "imap_get_mailbox: Error parsing %s\n", path); return -1; } - - if (!(mutt_bit_isset(idata->capabilities, IMAP4) || - mutt_bit_isset(idata->capabilities, IMAP4REV1))) + if (!(*hidata = imap_conn_find(&(mx.account), option(OPT_IMAP_PASSIVE) ? MUTT_IMAP_CONN_NONEW : 0)) || + (*hidata)->state < IMAP_AUTHENTICATED) { - mutt_error( - _("This IMAP server is ancient. NeoMutt does not work with it.")); - mutt_sleep(2); /* pause a moment to let the user see the error */ - + FREE(&mx.mbox); return -1; } + imap_fix_path(*hidata, mx.mbox, buf, blen); + if (!*buf) + strfcpy(buf, "INBOX", blen); + FREE(&mx.mbox); + return 0; } /** - * imap_conn_find - Find an open IMAP connection + * do_search - Perform a search of messages * - * Find an open IMAP connection matching account, or open a new one if none can - * be found. + * returns number of patterns in the search that should be done server-side + * (eg are full-text) */ -struct ImapData *imap_conn_find(const struct Account *account, int flags) +static int do_search(const struct Pattern *search, int allpats) { - struct Connection *conn = NULL; - struct Account *creds = NULL; - struct ImapData *idata = NULL; - bool new = false; + int rc = 0; + const struct Pattern *pat = NULL; - while ((conn = mutt_conn_find(conn, account))) + for (pat = search; pat; pat = pat->next) { - if (!creds) - creds = &conn->account; - else - memcpy(&conn->account, creds, sizeof(struct Account)); - - idata = conn->data; - if (flags & MUTT_IMAP_CONN_NONEW) + switch (pat->op) { - if (!idata) - { - /* This should only happen if we've come to the end of the list */ - mutt_socket_free(conn); - return NULL; - } - else if (idata->state < IMAP_AUTHENTICATED) - continue; + case MUTT_BODY: + case MUTT_HEADER: + case MUTT_WHOLE_MSG: + if (pat->stringmatch) + rc++; + break; + case MUTT_SERVERSEARCH: + rc++; + break; + default: + if (pat->child && do_search(pat->child, 1)) + rc++; } - if (flags & MUTT_IMAP_CONN_NOSELECT && idata && idata->state >= IMAP_SELECTED) - continue; - if (idata && idata->status == IMAP_FATAL) - continue; - break; + + if (!allpats) + break; } - if (!conn) - return NULL; /* this happens when the initial connection fails */ - if (!idata) - { - /* The current connection is a new connection */ - idata = imap_new_idata(); - if (!idata) - { - mutt_socket_free(conn); - return NULL; - } + return rc; +} - conn->data = idata; - idata->conn = conn; - new = true; - } +/** + * imap_compile_search - Convert NeoMutt pattern to IMAP search + * + * Convert neomutt Pattern to IMAP SEARCH command containing only elements + * that require full-text search (neomutt already has what it needs for most + * match types, and does a better job (eg server doesn't support regexes). + */ +static int imap_compile_search(struct Context *ctx, const struct Pattern *pat, + struct Buffer *buf) +{ + if (!do_search(pat, 0)) + return 0; - if (idata->state == IMAP_DISCONNECTED) - imap_open_connection(idata); - if (idata->state == IMAP_CONNECTED) + if (pat->not) + mutt_buffer_addstr(buf, "NOT "); + + if (pat->child) { - if (!imap_authenticate(idata)) + int clauses; + + clauses = do_search(pat->child, 1); + if (clauses > 0) { - idata->state = IMAP_AUTHENTICATED; - FREE(&idata->capstr); - new = true; - if (idata->conn->ssf) - mutt_debug(2, "Communication encrypted at %d bits\n", idata->conn->ssf); - } - else - mutt_account_unsetpass(&idata->conn->account); - } - if (new && idata->state == IMAP_AUTHENTICATED) - { - /* capabilities may have changed */ - imap_exec(idata, "CAPABILITY", IMAP_CMD_QUEUE); - /* enable RFC6855, if the server supports that */ - if (mutt_bit_isset(idata->capabilities, ENABLE)) - imap_exec(idata, "ENABLE UTF8=ACCEPT", IMAP_CMD_QUEUE); - /* get root delimiter, '/' as default */ - idata->delim = '/'; - imap_exec(idata, "LIST \"\" \"\"", IMAP_CMD_QUEUE); - if (option(OPT_IMAP_CHECK_SUBSCRIBED)) - imap_exec(idata, "LSUB \"\" \"*\"", IMAP_CMD_QUEUE); - /* we may need the root delimiter before we open a mailbox */ - imap_exec(idata, NULL, IMAP_CMD_FAIL_OK); - } + const struct Pattern *clause = pat->child; - return idata; -} + mutt_buffer_addch(buf, '('); -int imap_open_connection(struct ImapData *idata) -{ - char buf[LONG_STRING]; + while (clauses) + { + if (do_search(clause, 0)) + { + if (pat->op == MUTT_OR && clauses > 1) + mutt_buffer_addstr(buf, "OR "); + clauses--; - if (mutt_socket_open(idata->conn) < 0) - return -1; + if (imap_compile_search(ctx, clause, buf) < 0) + return -1; - idata->state = IMAP_CONNECTED; + if (clauses) + mutt_buffer_addch(buf, ' '); + } + clause = clause->next; + } - if (imap_cmd_step(idata) != IMAP_CMD_OK) - { - imap_close_connection(idata); - return -1; + mutt_buffer_addch(buf, ')'); + } } - - if (mutt_strncasecmp("* OK", idata->buf, 4) == 0) + else { - if ((mutt_strncasecmp("* OK [CAPABILITY", idata->buf, 16) != 0) && - imap_check_capabilities(idata)) - goto bail; -#ifdef USE_SSL - /* Attempt STARTTLS if available and desired. */ - if (!idata->conn->ssf && - (option(OPT_SSL_FORCE_TLS) || mutt_bit_isset(idata->capabilities, STARTTLS))) + char term[STRING]; + char *delim = NULL; + + switch (pat->op) { - int rc; + case MUTT_HEADER: + mutt_buffer_addstr(buf, "HEADER "); - if (option(OPT_SSL_FORCE_TLS)) - rc = MUTT_YES; - else if ((rc = query_quadoption(OPT_SSL_STARTTLS, - _("Secure connection with TLS?"))) == MUTT_ABORT) - goto err_close_conn; - if (rc == MUTT_YES) + /* extract header name */ + delim = strchr(pat->p.str, ':'); + if (!delim) + { + mutt_error(_("Header search without header name: %s"), pat->p.str); + return -1; + } + *delim = '\0'; + imap_quote_string(term, sizeof(term), pat->p.str); + mutt_buffer_addstr(buf, term); + mutt_buffer_addch(buf, ' '); + + /* and field */ + *delim = ':'; + delim++; + SKIPWS(delim); + imap_quote_string(term, sizeof(term), delim); + mutt_buffer_addstr(buf, term); + break; + case MUTT_BODY: + mutt_buffer_addstr(buf, "BODY "); + imap_quote_string(term, sizeof(term), pat->p.str); + mutt_buffer_addstr(buf, term); + break; + case MUTT_WHOLE_MSG: + mutt_buffer_addstr(buf, "TEXT "); + imap_quote_string(term, sizeof(term), pat->p.str); + mutt_buffer_addstr(buf, term); + break; + case MUTT_SERVERSEARCH: { - rc = imap_exec(idata, "STARTTLS", IMAP_CMD_FAIL_OK); - if (rc == -1) - goto bail; - if (rc != -2) + struct ImapData *idata = ctx->data; + if (!mutt_bit_isset(idata->capabilities, X_GM_EXT1)) { - if (mutt_ssl_starttls(idata->conn)) - { - mutt_error(_("Could not negotiate TLS connection")); - mutt_sleep(1); - goto err_close_conn; - } - else - { - /* RFC2595 demands we recheck CAPABILITY after TLS completes. */ - if (imap_exec(idata, "CAPABILITY", 0)) - goto bail; - } + mutt_error(_("Server-side custom search not supported: %s"), pat->p.str); + return -1; } } + mutt_buffer_addstr(buf, "X-GM-RAW "); + imap_quote_string(term, sizeof(term), pat->p.str); + mutt_buffer_addstr(buf, term); + break; } - - if (option(OPT_SSL_FORCE_TLS) && !idata->conn->ssf) - { - mutt_error(_("Encrypted connection unavailable")); - mutt_sleep(1); - goto err_close_conn; - } -#endif - } - else if (mutt_strncasecmp("* PREAUTH", idata->buf, 9) == 0) - { - idata->state = IMAP_AUTHENTICATED; - if (imap_check_capabilities(idata) != 0) - goto bail; - FREE(&idata->capstr); - } - else - { - imap_error("imap_open_connection()", buf); - goto bail; } return 0; - -#ifdef USE_SSL -err_close_conn: - imap_close_connection(idata); -#endif -bail: - FREE(&idata->capstr); - return -1; } -void imap_close_connection(struct ImapData *idata) +/** + * longest_common_prefix - Find longest prefix common to two strings + * @param dest Destination buffer + * @param src Source buffer + * @param start Starting offset into string + * @param dlen Destination buffer length + * @retval n Length of the common string + * + * Trim dest to the length of the longest prefix it shares with src. + */ +static size_t longest_common_prefix(char *dest, const char *src, size_t start, size_t dlen) { - if (idata->state != IMAP_DISCONNECTED) - { - mutt_socket_close(idata->conn); - idata->state = IMAP_DISCONNECTED; - } - idata->seqno = idata->nextcmd = idata->lastcmd = idata->status = false; - memset(idata->cmds, 0, sizeof(struct ImapCommand) * idata->cmdslots); + size_t pos = start; + + while (pos < dlen && dest[pos] && dest[pos] == src[pos]) + pos++; + dest[pos] = '\0'; + + return pos; } /** - * imap_get_flags - Make a simple list out of a FLAGS response + * imap_complete_hosts - Look for completion matches for mailboxes * - * return stream following FLAGS response + * look for IMAP URLs to complete from defined mailboxes. Could be extended to + * complete over open connections and account/folder hooks too. */ -static char *imap_get_flags(struct ListHead *hflags, char *s) +static int imap_complete_hosts(char *dest, size_t len) { - char *flag_word = NULL; - char ctmp; + struct Buffy *mailbox = NULL; + struct Connection *conn = NULL; + int rc = -1; + size_t matchlen; - /* sanity-check string */ - if (mutt_strncasecmp("FLAGS", s, 5) != 0) - { - mutt_debug(1, "imap_get_flags: not a FLAGS response: %s\n", s); - return NULL; - } - s += 5; - SKIPWS(s); - if (*s != '(') + matchlen = mutt_strlen(dest); + for (mailbox = Incoming; mailbox; mailbox = mailbox->next) { - mutt_debug(1, "imap_get_flags: bogus FLAGS response: %s\n", s); - return NULL; + if (mutt_strncmp(dest, mailbox->path, matchlen) == 0) + { + if (rc) + { + strfcpy(dest, mailbox->path, len); + rc = 0; + } + else + longest_common_prefix(dest, mailbox->path, matchlen, len); + } } - /* update caller's flags handle */ - while (*s && *s != ')') + TAILQ_FOREACH(conn, mutt_socket_head(), entries) { - s++; - SKIPWS(s); - flag_word = s; - while (*s && (*s != ')') && !ISSPACE(*s)) - s++; - ctmp = *s; - *s = '\0'; - if (*flag_word) - mutt_list_insert_tail(hflags, safe_strdup(flag_word)); - *s = ctmp; - } + struct Url url; + char urlstr[LONG_STRING]; - /* note bad flags response */ - if (*s != ')') - { - mutt_debug(1, "imap_get_flags: Unterminated FLAGS response: %s\n", s); - mutt_list_free(hflags); + if (conn->account.type != MUTT_ACCT_TYPE_IMAP) + continue; - return NULL; + mutt_account_tourl(&conn->account, &url); + /* FIXME: how to handle multiple users on the same host? */ + url.user = NULL; + url.path = NULL; + url_tostring(&url, urlstr, sizeof(urlstr), 0); + if (mutt_strncmp(dest, urlstr, matchlen) == 0) + { + if (rc) + { + strfcpy(dest, urlstr, len); + rc = 0; + } + else + longest_common_prefix(dest, urlstr, matchlen, len); + } } - s++; - - return s; + return rc; } -static int imap_open_mailbox(struct Context *ctx) +/** + * imap_access - Check permissions on an IMAP mailbox + * + * TODO: ACL checks. Right now we assume if it exists we can mess with it. + */ +int imap_access(const char *path) { struct ImapData *idata = NULL; - struct ImapStatus *status = NULL; + struct ImapMbox mx; char buf[LONG_STRING]; - char bufout[LONG_STRING]; - int count = 0; - struct ImapMbox mx, pmx; + char mailbox[LONG_STRING]; + char mbox[LONG_STRING]; int rc; - if (imap_parse_path(ctx->path, &mx)) - { - mutt_error(_("%s is an invalid IMAP path"), ctx->path); + if (imap_parse_path(path, &mx)) return -1; - } - /* we require a connection which isn't currently in IMAP_SELECTED state */ - idata = imap_conn_find(&(mx.account), MUTT_IMAP_CONN_NOSELECT); + idata = imap_conn_find(&mx.account, option(OPT_IMAP_PASSIVE) ? MUTT_IMAP_CONN_NONEW : 0); if (!idata) - goto fail_noidata; - if (idata->state < IMAP_AUTHENTICATED) - goto fail; - - /* once again the context is new */ - ctx->data = idata; - - /* Clean up path and replace the one in the ctx */ - imap_fix_path(idata, mx.mbox, buf, sizeof(buf)); - if (!*buf) - strfcpy(buf, "INBOX", sizeof(buf)); - FREE(&(idata->mailbox)); - idata->mailbox = safe_strdup(buf); - imap_qualify_path(buf, sizeof(buf), &mx, idata->mailbox); - - FREE(&(ctx->path)); - FREE(&(ctx->realpath)); - ctx->path = safe_strdup(buf); - ctx->realpath = safe_strdup(ctx->path); - - idata->ctx = ctx; - - /* clear mailbox status */ - idata->status = false; - memset(idata->ctx->rights, 0, sizeof(idata->ctx->rights)); - idata->new_mail_count = 0; - idata->max_msn = 0; - - mutt_message(_("Selecting %s..."), idata->mailbox); - imap_munge_mbox_name(idata, buf, sizeof(buf), idata->mailbox); - - /* pipeline ACL test */ - if (mutt_bit_isset(idata->capabilities, ACL)) - { - snprintf(bufout, sizeof(bufout), "MYRIGHTS %s", buf); - imap_exec(idata, bufout, IMAP_CMD_QUEUE); - } - /* assume we have all rights if ACL is unavailable */ - else { - mutt_bit_set(idata->ctx->rights, MUTT_ACL_LOOKUP); - mutt_bit_set(idata->ctx->rights, MUTT_ACL_READ); - mutt_bit_set(idata->ctx->rights, MUTT_ACL_SEEN); - mutt_bit_set(idata->ctx->rights, MUTT_ACL_WRITE); - mutt_bit_set(idata->ctx->rights, MUTT_ACL_INSERT); - mutt_bit_set(idata->ctx->rights, MUTT_ACL_POST); - mutt_bit_set(idata->ctx->rights, MUTT_ACL_CREATE); - mutt_bit_set(idata->ctx->rights, MUTT_ACL_DELETE); + FREE(&mx.mbox); + return -1; } - /* pipeline the postponed count if possible */ - pmx.mbox = NULL; - if (mx_is_imap(Postponed) && !imap_parse_path(Postponed, &pmx) && - mutt_account_match(&pmx.account, &mx.account)) - imap_status(Postponed, 1); - FREE(&pmx.mbox); - - snprintf(bufout, sizeof(bufout), "%s %s", - ctx->readonly ? "EXAMINE" : "SELECT", buf); - - idata->state = IMAP_SELECTED; - - imap_cmd_start(idata, bufout); - status = imap_mboxcache_get(idata, idata->mailbox, 1); + imap_fix_path(idata, mx.mbox, mailbox, sizeof(mailbox)); + if (!*mailbox) + strfcpy(mailbox, "INBOX", sizeof(mailbox)); - do + /* we may already be in the folder we're checking */ + if (mutt_strcmp(idata->mailbox, mx.mbox) == 0) { - char *pc = NULL; - - rc = imap_cmd_step(idata); - if (rc != IMAP_CMD_CONTINUE) - break; - - pc = idata->buf + 2; - - /* Obtain list of available flags here, may be overridden by a - * PERMANENTFLAGS tag in the OK response */ - if (mutt_strncasecmp("FLAGS", pc, 5) == 0) - { - /* don't override PERMANENTFLAGS */ - if (STAILQ_EMPTY(&idata->flags)) - { - mutt_debug(3, "Getting mailbox FLAGS\n"); - pc = imap_get_flags(&idata->flags, pc); - if (!pc) - goto fail; - } - } - /* PERMANENTFLAGS are massaged to look like FLAGS, then override FLAGS */ - else if (mutt_strncasecmp("OK [PERMANENTFLAGS", pc, 18) == 0) - { - mutt_debug(3, "Getting mailbox PERMANENTFLAGS\n"); - /* safe to call on NULL */ - mutt_list_free(&idata->flags); - /* skip "OK [PERMANENT" so syntax is the same as FLAGS */ - pc += 13; - pc = imap_get_flags(&(idata->flags), pc); - if (!pc) - goto fail; - } - /* save UIDVALIDITY for the header cache */ - else if (mutt_strncasecmp("OK [UIDVALIDITY", pc, 14) == 0) - { - mutt_debug(3, "Getting mailbox UIDVALIDITY\n"); - pc += 3; - pc = imap_next_word(pc); - idata->uid_validity = strtol(pc, NULL, 10); - status->uidvalidity = idata->uid_validity; - } - else if (mutt_strncasecmp("OK [UIDNEXT", pc, 11) == 0) - { - mutt_debug(3, "Getting mailbox UIDNEXT\n"); - pc += 3; - pc = imap_next_word(pc); - idata->uidnext = strtol(pc, NULL, 10); - status->uidnext = idata->uidnext; - } - else - { - pc = imap_next_word(pc); - if (mutt_strncasecmp("EXISTS", pc, 6) == 0) - { - count = idata->new_mail_count; - idata->new_mail_count = 0; - } - } - } while (rc == IMAP_CMD_CONTINUE); + FREE(&mx.mbox); + return 0; + } + FREE(&mx.mbox); - if (rc == IMAP_CMD_NO) + if (imap_mboxcache_get(idata, mailbox, 0)) { - char *s = NULL; - s = imap_next_word(idata->buf); /* skip seq */ - s = imap_next_word(s); /* Skip response */ - mutt_error("%s", s); - mutt_sleep(2); - goto fail; + mutt_debug(3, "imap_access: found %s in cache\n", mailbox); + return 0; } - if (rc != IMAP_CMD_OK) - goto fail; + imap_munge_mbox_name(idata, mbox, sizeof(mbox), mailbox); - /* check for READ-ONLY notification */ - if ((mutt_strncasecmp(imap_get_qualifier(idata->buf), "[READ-ONLY]", 11) == 0) && - !mutt_bit_isset(idata->capabilities, ACL)) + if (mutt_bit_isset(idata->capabilities, IMAP4REV1)) + snprintf(buf, sizeof(buf), "STATUS %s (UIDVALIDITY)", mbox); + else if (mutt_bit_isset(idata->capabilities, STATUS)) + snprintf(buf, sizeof(buf), "STATUS %s (UID-VALIDITY)", mbox); + else { - mutt_debug(2, "Mailbox is read-only.\n"); - ctx->readonly = true; + mutt_debug(2, "imap_access: STATUS not supported?\n"); + return -1; } -#ifdef DEBUG - /* dump the mailbox flags we've found */ - if (debuglevel > 2) + rc = imap_exec(idata, buf, IMAP_CMD_FAIL_OK); + if (rc < 0) { - if (STAILQ_EMPTY(&idata->flags)) - mutt_debug(3, "No folder flags found\n"); - else - { - struct ListNode *np; - struct Buffer flag_buffer; - mutt_buffer_init(&flag_buffer); - mutt_buffer_printf(&flag_buffer, "Mailbox flags: "); - STAILQ_FOREACH(np, &idata->flags, entries) - { - mutt_buffer_printf(&flag_buffer, "[%s] ", np->data); - } - mutt_debug(3, "%s\n", flag_buffer.data); - FREE(&flag_buffer.data); - } + mutt_debug(1, "imap_access: Can't check STATUS of %s\n", mbox); + return rc; } -#endif - if (!(mutt_bit_isset(idata->ctx->rights, MUTT_ACL_DELETE) || - mutt_bit_isset(idata->ctx->rights, MUTT_ACL_SEEN) || - mutt_bit_isset(idata->ctx->rights, MUTT_ACL_WRITE) || - mutt_bit_isset(idata->ctx->rights, MUTT_ACL_INSERT))) - ctx->readonly = true; + return 0; +} - ctx->hdrmax = count; - ctx->hdrs = safe_calloc(count, sizeof(struct Header *)); - ctx->v2r = safe_calloc(count, sizeof(int)); - ctx->msgcount = 0; +int imap_create_mailbox(struct ImapData *idata, char *mailbox) +{ + char buf[LONG_STRING], mbox[LONG_STRING]; - if (count && (imap_read_headers(idata, 1, count) < 0)) + imap_munge_mbox_name(idata, mbox, sizeof(mbox), mailbox); + snprintf(buf, sizeof(buf), "CREATE %s", mbox); + + if (imap_exec(idata, buf, 0) != 0) { - mutt_error(_("Error opening mailbox")); - mutt_sleep(1); - goto fail; + mutt_error(_("CREATE failed: %s"), imap_cmd_trailer(idata)); + return -1; } - mutt_debug(2, "imap_open_mailbox: msgcount is %d\n", ctx->msgcount); - FREE(&mx.mbox); return 0; - -fail: - if (idata->state == IMAP_SELECTED) - idata->state = IMAP_AUTHENTICATED; -fail_noidata: - FREE(&mx.mbox); - return -1; } -static int imap_open_mailbox_append(struct Context *ctx, int flags) +int imap_rename_mailbox(struct ImapData *idata, struct ImapMbox *mx, const char *newname) { - struct ImapData *idata = NULL; + char oldmbox[LONG_STRING]; + char newmbox[LONG_STRING]; char buf[LONG_STRING]; - char mailbox[LONG_STRING]; - struct ImapMbox mx; - int rc; - if (imap_parse_path(ctx->path, &mx)) - return -1; + imap_munge_mbox_name(idata, oldmbox, sizeof(oldmbox), mx->mbox); + imap_munge_mbox_name(idata, newmbox, sizeof(newmbox), newname); - /* in APPEND mode, we appear to hijack an existing IMAP connection - - * ctx is brand new and mostly empty */ + snprintf(buf, sizeof(buf), "RENAME %s %s", oldmbox, newmbox); - idata = imap_conn_find(&(mx.account), 0); - if (!idata) - { - FREE(&mx.mbox); - return -1; - } - - ctx->data = idata; - - imap_fix_path(idata, mx.mbox, mailbox, sizeof(mailbox)); - if (!*mailbox) - strfcpy(mailbox, "INBOX", sizeof(mailbox)); - FREE(&mx.mbox); - - rc = imap_access(ctx->path); - if (rc == 0) - return 0; - - if (rc == -1) - return -1; - - snprintf(buf, sizeof(buf), _("Create %s?"), mailbox); - if (option(OPT_CONFIRMCREATE) && mutt_yesorno(buf, 1) != MUTT_YES) - return -1; - - if (imap_create_mailbox(idata, mailbox) < 0) + if (imap_exec(idata, buf, 0) != 0) return -1; return 0; } -/** - * imap_logout - Gracefully log out of server - */ -void imap_logout(struct ImapData **idata) +int imap_delete_mailbox(struct Context *ctx, struct ImapMbox *mx) { - /* we set status here to let imap_handle_untagged know we _expect_ to - * receive a bye response (so it doesn't freak out and close the conn) */ - (*idata)->status = IMAP_BYE; - imap_cmd_start(*idata, "LOGOUT"); - if (ImapPollTimeout <= 0 || mutt_socket_poll((*idata)->conn, ImapPollTimeout) != 0) + char buf[LONG_STRING], mbox[LONG_STRING]; + struct ImapData *idata = NULL; + + if (!ctx || !ctx->data) { - while (imap_cmd_step(*idata) == IMAP_CMD_CONTINUE) - ; + idata = imap_conn_find(&mx->account, + option(OPT_IMAP_PASSIVE) ? MUTT_IMAP_CONN_NONEW : 0); + if (!idata) + { + FREE(&mx->mbox); + return -1; + } + } + else + { + idata = ctx->data; } - mutt_socket_close((*idata)->conn); - imap_free_idata(idata); -} - -static int imap_open_new_message(struct Message *msg, struct Context *dest, struct Header *hdr) -{ - char tmp[_POSIX_PATH_MAX]; + imap_munge_mbox_name(idata, mbox, sizeof(mbox), mx->mbox); + snprintf(buf, sizeof(buf), "DELETE %s", mbox); - mutt_mktemp(tmp, sizeof(tmp)); - msg->fp = safe_fopen(tmp, "w"); - if (!msg->fp) - { - mutt_perror(tmp); + if (imap_exec(idata, buf, 0) != 0) return -1; - } - msg->path = safe_strdup(tmp); + return 0; } /** - * imap_set_flag - append str to flags if we currently have permission + * imap_logout_all - close all open connections * - * according to aclbit + * Quick and dirty until we can make sure we've got all the context we need. */ -static void imap_set_flag(struct ImapData *idata, int aclbit, int flag, - const char *str, char *flags, size_t flsize) +void imap_logout_all(void) { - if (mutt_bit_isset(idata->ctx->rights, aclbit)) - if (flag && imap_has_flag(&idata->flags, str)) - safe_strcat(flags, flsize, str); + struct ConnectionList *head = mutt_socket_head(); + struct Connection *np, *tmp; + TAILQ_FOREACH_SAFE(np, head, entries, tmp) + { + if (np->account.type == MUTT_ACCT_TYPE_IMAP && np->fd >= 0) + { + TAILQ_REMOVE(head, np, entries); + mutt_message(_("Closing connection to %s..."), np->account.host); + imap_logout((struct ImapData **) (void *) &np->data); + mutt_clear_error(); + mutt_socket_free(np); + } + } } /** - * imap_has_flag - Does the flag exist in the list - * @retval boolean + * imap_read_literal - Read bytes bytes from server into file * - * Do a caseless comparison of the flag against a flag list, return true if - * found or flag list has '\*'. + * Not explicitly buffered, relies on FILE buffering. NOTE: strips `\r` from + * `\r\n`. Apparently even literals use `\r\n`-terminated strings ?! */ -bool imap_has_flag(struct ListHead *flag_list, const char *flag) +int imap_read_literal(FILE *fp, struct ImapData *idata, long bytes, struct Progress *pbar) { - if (STAILQ_EMPTY(flag_list)) - return false; + char c; + bool r = false; - struct ListNode *np; - STAILQ_FOREACH(np, flag_list, entries) + mutt_debug(2, "imap_read_literal: reading %ld bytes\n", bytes); + + for (long pos = 0; pos < bytes; pos++) { - if (mutt_strncasecmp(np->data, flag, strlen(np->data)) == 0) - return true; + if (mutt_socket_readchar(idata->conn, &c) != 1) + { + mutt_debug(1, "imap_read_literal: error during read, %ld bytes read\n", pos); + idata->status = IMAP_FATAL; - if (mutt_strncmp(np->data, "\\*", strlen(np->data)) == 0) - return true; + return -1; + } + + if (r && c != '\n') + fputc('\r', fp); + + if (c == '\r') + { + r = true; + continue; + } + else + r = false; + + fputc(c, fp); + + if (pbar && !(pos % 1024)) + mutt_progress_update(pbar, pos, -1); +#ifdef DEBUG + if (debuglevel >= IMAP_LOG_LTRL) + fputc(c, debugfile); +#endif } - return false; + return 0; } /** - * imap_make_msg_set - Make a message set + * imap_expunge_mailbox - Purge messages from the server * - * Note: headers must be in SORT_ORDER. See imap_exec_msgset for args. - * Pos is an opaque pointer a la strtok. It should be 0 at first call. + * Purge IMAP portion of expunged messages from the context. Must not be done + * while something has a handle on any headers (eg inside pager or editor). + * That is, check IMAP_REOPEN_ALLOW. */ -static int imap_make_msg_set(struct ImapData *idata, struct Buffer *buf, - int flag, bool changed, bool invert, int *pos) +void imap_expunge_mailbox(struct ImapData *idata) { - struct Header **hdrs = idata->ctx->hdrs; - int count = 0; /* number of messages in message set */ - bool match = false; /* whether current message matches flag condition */ - unsigned int setstart = 0; /* start of current message range */ - int n; - bool started = false; + struct Header *h = NULL; + int cacheno; + short old_sort; - hdrs = idata->ctx->hdrs; +#ifdef USE_HCACHE + idata->hcache = imap_hcache_open(idata, NULL); +#endif - for (n = *pos; n < idata->ctx->msgcount && buf->dptr - buf->data < IMAP_MAX_CMDLEN; n++) - { - match = false; - /* don't include pending expunged messages */ - if (hdrs[n]->active) - switch (flag) - { - case MUTT_DELETED: - if (hdrs[n]->deleted != HEADER_DATA(hdrs[n])->deleted) - match = invert ^ hdrs[n]->deleted; - break; - case MUTT_FLAG: - if (hdrs[n]->flagged != HEADER_DATA(hdrs[n])->flagged) - match = invert ^ hdrs[n]->flagged; - break; - case MUTT_OLD: - if (hdrs[n]->old != HEADER_DATA(hdrs[n])->old) - match = invert ^ hdrs[n]->old; - break; - case MUTT_READ: - if (hdrs[n]->read != HEADER_DATA(hdrs[n])->read) - match = invert ^ hdrs[n]->read; - break; - case MUTT_REPLIED: - if (hdrs[n]->replied != HEADER_DATA(hdrs[n])->replied) - match = invert ^ hdrs[n]->replied; - break; + old_sort = Sort; + Sort = SORT_ORDER; + mutt_sort_headers(idata->ctx, 0); - case MUTT_TAG: - if (hdrs[n]->tagged) - match = true; - break; - case MUTT_TRASH: - if (hdrs[n]->deleted && !hdrs[n]->purge) - match = true; - break; - } + for (int i = 0; i < idata->ctx->msgcount; i++) + { + h = idata->ctx->hdrs[i]; - if (match && (!changed || hdrs[n]->changed)) + if (h->index == INT_MAX) { - count++; - if (setstart == 0) + mutt_debug(2, "Expunging message UID %d.\n", HEADER_DATA(h)->uid); + + h->active = false; + idata->ctx->size -= h->content->length; + + imap_cache_del(idata, h); +#ifdef USE_HCACHE + imap_hcache_del(idata, HEADER_DATA(h)->uid); +#endif + + /* free cached body from disk, if necessary */ + cacheno = HEADER_DATA(h)->uid % IMAP_CACHE_LEN; + if (idata->cache[cacheno].uid == HEADER_DATA(h)->uid && + idata->cache[cacheno].path) { - setstart = HEADER_DATA(hdrs[n])->uid; - if (!started) - { - mutt_buffer_printf(buf, "%u", HEADER_DATA(hdrs[n])->uid); - started = true; - } - else - mutt_buffer_printf(buf, ",%u", HEADER_DATA(hdrs[n])->uid); + unlink(idata->cache[cacheno].path); + FREE(&idata->cache[cacheno].path); } - /* tie up if the last message also matches */ - else if (n == idata->ctx->msgcount - 1) - mutt_buffer_printf(buf, ":%u", HEADER_DATA(hdrs[n])->uid); - } - /* End current set if message doesn't match or we've reached the end - * of the mailbox via inactive messages following the last match. */ - else if (setstart && (hdrs[n]->active || n == idata->ctx->msgcount - 1)) - { - if (HEADER_DATA(hdrs[n - 1])->uid > setstart) - mutt_buffer_printf(buf, ":%u", HEADER_DATA(hdrs[n - 1])->uid); - setstart = 0; - } - } - - *pos = n; - return count; -} + int_hash_delete(idata->uid_hash, HEADER_DATA(h)->uid, h, NULL); -/** - * imap_exec_msgset - Prepare commands for all messages matching conditions - * @param idata ImapData containing context containing header set - * @param pre prefix commands - * @param post postfix commands - * @param flag flag type on which to filter, e.g. MUTT_REPLIED + imap_free_header_data((struct ImapHeaderData **) &h->data); + } + else + { + h->index = i; + /* Mutt has several places where it turns off h->active as a + * hack. For example to avoid FLAG updates, or to exclude from + * imap_exec_msgset. + * + * Unfortunately, when a reopen is allowed and the IMAP_EXPUNGE_PENDING + * flag becomes set (e.g. a flag update to a modified header), + * this function will be called by imap_cmd_finish(). + * + * The mx_update_tables() will free and remove these "inactive" headers, + * despite that an EXPUNGE was not received for them. + * This would result in memory leaks and segfaults due to dangling + * pointers in the msn_index and uid_hash. + * + * So this is another hack to work around the hacks. We don't want to + * remove the messages, so make sure active is on. + */ + h->active = true; + } + } + +#ifdef USE_HCACHE + imap_hcache_close(idata); +#endif + + /* We may be called on to expunge at any time. We can't rely on the caller + * to always know to rethread */ + mx_update_tables(idata->ctx, false); + Sort = old_sort; + mutt_sort_headers(idata->ctx, 1); +} + +/** + * imap_conn_find - Find an open IMAP connection + * + * Find an open IMAP connection matching account, or open a new one if none can + * be found. + */ +struct ImapData *imap_conn_find(const struct Account *account, int flags) +{ + struct Connection *conn = NULL; + struct Account *creds = NULL; + struct ImapData *idata = NULL; + bool new = false; + + while ((conn = mutt_conn_find(conn, account))) + { + if (!creds) + creds = &conn->account; + else + memcpy(&conn->account, creds, sizeof(struct Account)); + + idata = conn->data; + if (flags & MUTT_IMAP_CONN_NONEW) + { + if (!idata) + { + /* This should only happen if we've come to the end of the list */ + mutt_socket_free(conn); + return NULL; + } + else if (idata->state < IMAP_AUTHENTICATED) + continue; + } + if (flags & MUTT_IMAP_CONN_NOSELECT && idata && idata->state >= IMAP_SELECTED) + continue; + if (idata && idata->status == IMAP_FATAL) + continue; + break; + } + if (!conn) + return NULL; /* this happens when the initial connection fails */ + + if (!idata) + { + /* The current connection is a new connection */ + idata = imap_new_idata(); + if (!idata) + { + mutt_socket_free(conn); + return NULL; + } + + conn->data = idata; + idata->conn = conn; + new = true; + } + + if (idata->state == IMAP_DISCONNECTED) + imap_open_connection(idata); + if (idata->state == IMAP_CONNECTED) + { + if (!imap_authenticate(idata)) + { + idata->state = IMAP_AUTHENTICATED; + FREE(&idata->capstr); + new = true; + if (idata->conn->ssf) + mutt_debug(2, "Communication encrypted at %d bits\n", idata->conn->ssf); + } + else + mutt_account_unsetpass(&idata->conn->account); + } + if (new && idata->state == IMAP_AUTHENTICATED) + { + /* capabilities may have changed */ + imap_exec(idata, "CAPABILITY", IMAP_CMD_QUEUE); + /* enable RFC6855, if the server supports that */ + if (mutt_bit_isset(idata->capabilities, ENABLE)) + imap_exec(idata, "ENABLE UTF8=ACCEPT", IMAP_CMD_QUEUE); + /* get root delimiter, '/' as default */ + idata->delim = '/'; + imap_exec(idata, "LIST \"\" \"\"", IMAP_CMD_QUEUE); + if (option(OPT_IMAP_CHECK_SUBSCRIBED)) + imap_exec(idata, "LSUB \"\" \"*\"", IMAP_CMD_QUEUE); + /* we may need the root delimiter before we open a mailbox */ + imap_exec(idata, NULL, IMAP_CMD_FAIL_OK); + } + + return idata; +} + +int imap_open_connection(struct ImapData *idata) +{ + char buf[LONG_STRING]; + + if (mutt_socket_open(idata->conn) < 0) + return -1; + + idata->state = IMAP_CONNECTED; + + if (imap_cmd_step(idata) != IMAP_CMD_OK) + { + imap_close_connection(idata); + return -1; + } + + if (mutt_strncasecmp("* OK", idata->buf, 4) == 0) + { + if ((mutt_strncasecmp("* OK [CAPABILITY", idata->buf, 16) != 0) && + imap_check_capabilities(idata)) + goto bail; +#ifdef USE_SSL + /* Attempt STARTTLS if available and desired. */ + if (!idata->conn->ssf && + (option(OPT_SSL_FORCE_TLS) || mutt_bit_isset(idata->capabilities, STARTTLS))) + { + int rc; + + if (option(OPT_SSL_FORCE_TLS)) + rc = MUTT_YES; + else if ((rc = query_quadoption(OPT_SSL_STARTTLS, + _("Secure connection with TLS?"))) == MUTT_ABORT) + goto err_close_conn; + if (rc == MUTT_YES) + { + rc = imap_exec(idata, "STARTTLS", IMAP_CMD_FAIL_OK); + if (rc == -1) + goto bail; + if (rc != -2) + { + if (mutt_ssl_starttls(idata->conn)) + { + mutt_error(_("Could not negotiate TLS connection")); + mutt_sleep(1); + goto err_close_conn; + } + else + { + /* RFC2595 demands we recheck CAPABILITY after TLS completes. */ + if (imap_exec(idata, "CAPABILITY", 0)) + goto bail; + } + } + } + } + + if (option(OPT_SSL_FORCE_TLS) && !idata->conn->ssf) + { + mutt_error(_("Encrypted connection unavailable")); + mutt_sleep(1); + goto err_close_conn; + } +#endif + } + else if (mutt_strncasecmp("* PREAUTH", idata->buf, 9) == 0) + { + idata->state = IMAP_AUTHENTICATED; + if (imap_check_capabilities(idata) != 0) + goto bail; + FREE(&idata->capstr); + } + else + { + imap_error("imap_open_connection()", buf); + goto bail; + } + + return 0; + +#ifdef USE_SSL +err_close_conn: + imap_close_connection(idata); +#endif +bail: + FREE(&idata->capstr); + return -1; +} + +void imap_close_connection(struct ImapData *idata) +{ + if (idata->state != IMAP_DISCONNECTED) + { + mutt_socket_close(idata->conn); + idata->state = IMAP_DISCONNECTED; + } + idata->seqno = idata->nextcmd = idata->lastcmd = idata->status = false; + memset(idata->cmds, 0, sizeof(struct ImapCommand) * idata->cmdslots); +} + +/** + * imap_logout - Gracefully log out of server + */ +void imap_logout(struct ImapData **idata) +{ + /* we set status here to let imap_handle_untagged know we _expect_ to + * receive a bye response (so it doesn't freak out and close the conn) */ + (*idata)->status = IMAP_BYE; + imap_cmd_start(*idata, "LOGOUT"); + if (ImapPollTimeout <= 0 || mutt_socket_poll((*idata)->conn, ImapPollTimeout) != 0) + { + while (imap_cmd_step(*idata) == IMAP_CMD_CONTINUE) + ; + } + + mutt_socket_close((*idata)->conn); + imap_free_idata(idata); +} + +/** + * imap_has_flag - Does the flag exist in the list + * @retval boolean + * + * Do a caseless comparison of the flag against a flag list, return true if + * found or flag list has '\*'. + */ +bool imap_has_flag(struct ListHead *flag_list, const char *flag) +{ + if (STAILQ_EMPTY(flag_list)) + return false; + + struct ListNode *np; + STAILQ_FOREACH(np, flag_list, entries) + { + if (mutt_strncasecmp(np->data, flag, strlen(np->data)) == 0) + return true; + + if (mutt_strncmp(np->data, "\\*", strlen(np->data)) == 0) + return true; + } + + return false; +} + +/** + * imap_exec_msgset - Prepare commands for all messages matching conditions + * @param idata ImapData containing context containing header set + * @param pre prefix commands + * @param post postfix commands + * @param flag flag type on which to filter, e.g. MUTT_REPLIED * @param changed include only changed messages in message set * @param invert invert sense of flag, eg MUTT_READ matches unread messages * @retval n Number of matched messages @@ -1113,32 +1123,11 @@ out: if (oldsort != Sort) { Sort = oldsort; - FREE(&idata->ctx->hdrs); - idata->ctx->hdrs = hdrs; - } - - return rc; -} - -/** - * compare_flags_for_copy - Compare local flags against the server - * @retval 0 if neomutt's flags match cached server flags - * EXCLUDING the deleted flag. - */ -static bool compare_flags_for_copy(struct Header *h) -{ - struct ImapHeaderData *hd = (struct ImapHeaderData *) h->data; - - if (h->read != hd->read) - return true; - if (h->old != hd->old) - return true; - if (h->flagged != hd->flagged) - return true; - if (h->replied != hd->replied) - return true; + FREE(&idata->ctx->hdrs); + idata->ctx->hdrs = hdrs; + } - return false; + return rc; } /** @@ -1245,1314 +1234,1319 @@ int imap_sync_message_for_copy(struct ImapData *idata, struct Header *hdr, return 0; } -static int sync_helper(struct ImapData *idata, int right, int flag, const char *name) +/** + * imap_check_mailbox - use the NOOP or IDLE command to poll for new mail + * @param ctx Context + * @param force Don't wait + * @retval #MUTT_REOPENED mailbox has been externally modified + * @retval #MUTT_NEW_MAIL new mail has arrived + * @retval 0 no change + * @retval -1 error + */ +int imap_check_mailbox(struct Context *ctx, int force) { - int count = 0; - int rc; - char buf[LONG_STRING]; - - if (!idata->ctx) - return -1; - - if (!mutt_bit_isset(idata->ctx->rights, right)) - return 0; - - if (right == MUTT_ACL_WRITE && !imap_has_flag(&idata->flags, name)) - return 0; - - snprintf(buf, sizeof(buf), "+FLAGS.SILENT (%s)", name); - rc = imap_exec_msgset(idata, "UID STORE", buf, flag, 1, 0); - if (rc < 0) - return rc; - count += rc; - - buf[0] = '-'; - rc = imap_exec_msgset(idata, "UID STORE", buf, flag, 1, 1); - if (rc < 0) - return rc; - count += rc; - - return count; + return imap_check(ctx->data, force); } -/** - * imap_edit_message_tags - Prompt and validate new messages tags - * - * @retval -1: error - * @retval 0: no valid user input - * @retval 1: buf set - */ -static int imap_edit_message_tags(struct Context *ctx, const char *tags, char *buf, size_t buflen) +int imap_check(struct ImapData *idata, int force) { - char *new = NULL; - char *checker = NULL; - struct ImapData *idata = (struct ImapData *) ctx->data; + /* overload keyboard timeout to avoid many mailbox checks in a row. + * Most users don't like having to wait exactly when they press a key. */ + int result = 0; - /* Check for \* flags capability */ - if (!imap_has_flag(&idata->flags, NULL)) + /* try IDLE first, unless force is set */ + if (!force && option(OPT_IMAP_IDLE) && mutt_bit_isset(idata->capabilities, IDLE) && + (idata->state != IMAP_IDLE || time(NULL) >= idata->lastread + ImapKeepalive)) { - mutt_error(_("IMAP server doesn't support custom flags")); - return -1; + if (imap_cmd_idle(idata) < 0) + return -1; } - - *buf = '\0'; - if (tags) - strncpy(buf, tags, buflen); - - if (mutt_get_field("Tags: ", buf, buflen, 0) != 0) - return -1; - - /* each keyword must be atom defined by rfc822 as: - * - * atom = 1* - * CHAR = ( 0.-127. ) - * specials = "(" / ")" / "<" / ">" / "@" - * / "," / ";" / ":" / "\" / <"> - * / "." / "[" / "]" - * SPACE = ( 32. ) - * CTLS = ( 0.-31., 127.) - * - * And must be separated by one space. - */ - - new = buf; - checker = buf; - SKIPWS(checker); - while (*checker != '\0') + if (idata->state == IMAP_IDLE) { - if (*checker < 32 || *checker >= 127 || // We allow space because it's the separator - *checker == 40 || // ( - *checker == 41 || // ) - *checker == 60 || // < - *checker == 62 || // > - *checker == 64 || // @ - *checker == 44 || // , - *checker == 59 || // ; - *checker == 58 || // : - *checker == 92 || // backslash - *checker == 34 || // " - *checker == 46 || // . - *checker == 91 || // [ - *checker == 93) // ] + while ((result = mutt_socket_poll(idata->conn, 0)) > 0) { - mutt_error(_("Invalid IMAP flags")); - mutt_sleep(2); - return 0; + if (imap_cmd_step(idata) != IMAP_CMD_CONTINUE) + { + mutt_debug(1, "Error reading IDLE response\n"); + return -1; + } + } + if (result < 0) + { + mutt_debug(1, "Poll failed, disabling IDLE\n"); + mutt_bit_unset(idata->capabilities, IDLE); } + } - /* Skip duplicate space */ - while (*checker == ' ' && *(checker + 1) == ' ') - checker++; + if ((force || (idata->state != IMAP_IDLE && time(NULL) >= idata->lastread + Timeout)) && + imap_exec(idata, "NOOP", IMAP_CMD_POLL) != 0) + return -1; - /* copy char to new and go the next one */ - *new ++ = *checker++; - } - *new = '\0'; - new = buf; /* rewind */ - mutt_remove_trailing_ws(new); + /* We call this even when we haven't run NOOP in case we have pending + * changes to process, since we can reopen here. */ + imap_cmd_finish(idata); - if (mutt_strcmp(tags, buf) == 0) - return 0; - return 1; + if (idata->check_status & IMAP_EXPUNGE_PENDING) + result = MUTT_REOPENED; + else if (idata->check_status & IMAP_NEWMAIL_PENDING) + result = MUTT_NEW_MAIL; + else if (idata->check_status & IMAP_FLAGS_PENDING) + result = MUTT_FLAGS; + + idata->check_status = 0; + + return result; } /** - * imap_commit_message_tags - Add/Change/Remove flags from headers - * @param ctx Context - * @param h Header - * @param tags List of tags - * @retval 0 Success - * @retval -1 Error - * - * This method update the server flags on the server by - * removing the last know custom flags of a header - * and adds the local flags - * - * If everything success we push the local flags to the - * last know custom flags (flags_remote). + * imap_buffy_check - Check for new mail in subscribed folders * - * Also this method check that each flags is support by the server - * first and remove unsupported one. + * Given a list of mailboxes rather than called once for each so that it can + * batch the commands and save on round trips. Returns number of mailboxes with + * new mail. */ -static int imap_commit_message_tags(struct Context *ctx, struct Header *h, char *tags) +int imap_buffy_check(int force, int check_stats) { struct ImapData *idata = NULL; - struct Buffer *cmd = NULL; - char uid[11]; - - idata = ctx->data; - - if (*tags == '\0') - tags = NULL; + struct ImapData *lastdata = NULL; + struct Buffy *mailbox = NULL; + char name[LONG_STRING]; + char command[LONG_STRING]; + char munged[LONG_STRING]; + int buffies = 0; - if (!mutt_bit_isset(idata->ctx->rights, MUTT_ACL_WRITE)) - return 0; + for (mailbox = Incoming; mailbox; mailbox = mailbox->next) + { + /* Init newly-added mailboxes */ + if (!mailbox->magic) + { + if (mx_is_imap(mailbox->path)) + mailbox->magic = MUTT_IMAP; + } - snprintf(uid, sizeof(uid), "%u", HEADER_DATA(h)->uid); + if (mailbox->magic != MUTT_IMAP) + continue; - /* Remove old custom flags */ - if (HEADER_DATA(h)->flags_remote) - { - cmd = mutt_buffer_new(); - if (!cmd) + if (imap_get_mailbox(mailbox->path, &idata, name, sizeof(name)) < 0) { - mutt_debug(1, "imap_commit_message_tags: unable to allocate buffer\n"); - return -1; + mailbox->new = false; + continue; } - cmd->dptr = cmd->data; - mutt_buffer_addstr(cmd, "UID STORE "); - mutt_buffer_addstr(cmd, uid); - mutt_buffer_addstr(cmd, " -FLAGS.SILENT ("); - mutt_buffer_addstr(cmd, HEADER_DATA(h)->flags_remote); - mutt_buffer_addstr(cmd, ")"); - /* Should we return here, or we are fine and we could - * continue to add new flags * - */ - if (imap_exec(idata, cmd->data, 0) != 0) + /* Don't issue STATUS on the selected mailbox, it will be NOOPed or + * IDLEd elsewhere. + * idata->mailbox may be NULL for connections other than the current + * mailbox's, and shouldn't expand to INBOX in that case. #3216. */ + if (idata->mailbox && (imap_mxcmp(name, idata->mailbox) == 0)) { - mutt_buffer_free(&cmd); - return -1; + mailbox->new = false; + continue; } - mutt_buffer_free(&cmd); - } + if (!mutt_bit_isset(idata->capabilities, IMAP4REV1) && + !mutt_bit_isset(idata->capabilities, STATUS)) + { + mutt_debug(2, "Server doesn't support STATUS\n"); + continue; + } - /* Add new custom flags */ - if (tags) - { - cmd = mutt_buffer_new(); - if (!cmd) + if (lastdata && idata != lastdata) { - mutt_debug(1, "imap_commit_message_tags: fail to remove old flags\n"); - return -1; + /* Send commands to previous server. Sorting the buffy list + * may prevent some infelicitous interleavings */ + if (imap_exec(lastdata, NULL, IMAP_CMD_FAIL_OK | IMAP_CMD_POLL) == -1) + mutt_debug(1, "Error polling mailboxes\n"); + + lastdata = NULL; } - cmd->dptr = cmd->data; - mutt_buffer_addstr(cmd, "UID STORE "); - mutt_buffer_addstr(cmd, uid); - mutt_buffer_addstr(cmd, " +FLAGS.SILENT ("); - mutt_buffer_addstr(cmd, tags); - mutt_buffer_addstr(cmd, ")"); - if (imap_exec(idata, cmd->data, 0) != 0) + if (!lastdata) + lastdata = idata; + + imap_munge_mbox_name(idata, munged, sizeof(munged), name); + if (check_stats) + snprintf(command, sizeof(command), + "STATUS %s (UIDNEXT UIDVALIDITY UNSEEN RECENT MESSAGES)", munged); + else + snprintf(command, sizeof(command), + "STATUS %s (UIDNEXT UIDVALIDITY UNSEEN RECENT)", munged); + + if (imap_exec(idata, command, IMAP_CMD_QUEUE | IMAP_CMD_POLL) < 0) { - mutt_debug(1, "imap_commit_message_tags: fail to add new flags\n"); - mutt_buffer_free(&cmd); - return -1; + mutt_debug(1, "Error queueing command\n"); + return 0; } + } - mutt_buffer_free(&cmd); + if (lastdata && (imap_exec(lastdata, NULL, IMAP_CMD_FAIL_OK | IMAP_CMD_POLL) == -1)) + { + mutt_debug(1, "Error polling mailboxes\n"); + return 0; } - /* We are good sync them */ - mutt_debug(1, "NEW TAGS: %d\n", tags); - driver_tags_replace(&h->tags, tags); - FREE(&HEADER_DATA(h)->flags_remote); - HEADER_DATA(h)->flags_remote = driver_tags_get_with_hidden(&h->tags); - return 0; + /* collect results */ + for (mailbox = Incoming; mailbox; mailbox = mailbox->next) + { + if (mailbox->magic == MUTT_IMAP && mailbox->new) + buffies++; + } + + return buffies; } -/* - * imap_sync_mailbox - Sync all the changes to the server - * @param ctx the current context - * @param expunge 0 or 1 - do expunge? - * @retval 0 on success +/** + * imap_status - Get the status of a mailbox * @retval -1 on error + * @retval >=0 count of messages in mailbox + * + * if queue != 0, queue the command and expect it to have been run + * on the next call (for pipelining the postponed count) */ -int imap_sync_mailbox(struct Context *ctx, int expunge) +int imap_status(char *path, int queue) { - struct ImapData *idata = NULL; - struct Context *appendctx = NULL; - struct Header *h = NULL; - struct Header **hdrs = NULL; - int oldsort; - int rc; + static int queued = 0; - idata = ctx->data; + struct ImapData *idata = NULL; + char buf[LONG_STRING]; + char mbox[LONG_STRING]; + struct ImapStatus *status = NULL; - if (idata->state < IMAP_SELECTED) - { - mutt_debug(2, "imap_sync_mailbox: no mailbox selected\n"); + if (imap_get_mailbox(path, &idata, buf, sizeof(buf)) < 0) return -1; - } - - /* This function is only called when the calling code expects the context - * to be changed. */ - imap_allow_reopen(ctx); - - rc = imap_check(idata, 0); - if (rc != 0) - return rc; - /* if we are expunging anyway, we can do deleted messages very quickly... */ - if (expunge && mutt_bit_isset(ctx->rights, MUTT_ACL_DELETE)) + /* We are in the folder we're polling - just return the mailbox count. + * + * Note that imap_mxcmp() converts NULL to "INBOX", so we need to + * make sure the idata really is open to a folder. */ + if (idata->ctx && !imap_mxcmp(buf, idata->mailbox)) + return idata->ctx->msgcount; + else if (mutt_bit_isset(idata->capabilities, IMAP4REV1) || + mutt_bit_isset(idata->capabilities, STATUS)) { - if ((rc = imap_exec_msgset(idata, "UID STORE", "+FLAGS.SILENT (\\Deleted)", - MUTT_DELETED, 1, 0)) < 0) - { - mutt_error(_("Expunge failed")); - mutt_sleep(1); - goto out; - } - - if (rc > 0) - { - /* mark these messages as unchanged so second pass ignores them. Done - * here so BOGUS UW-IMAP 4.7 SILENT FLAGS updates are ignored. */ - for (int i = 0; i < ctx->msgcount; i++) - if (ctx->hdrs[i]->deleted && ctx->hdrs[i]->changed) - ctx->hdrs[i]->active = false; - mutt_message(_("Marking %d messages deleted..."), rc); - } + imap_munge_mbox_name(idata, mbox, sizeof(mbox), buf); + snprintf(buf, sizeof(buf), "STATUS %s (%s)", mbox, "MESSAGES"); + imap_unmunge_mbox_name(idata, mbox); } + else + /* Server does not support STATUS, and this is not the current mailbox. + * There is no lightweight way to check recent arrivals */ + return -1; -#ifdef USE_HCACHE - idata->hcache = imap_hcache_open(idata, NULL); -#endif - - /* save messages with real (non-flag) changes */ - for (int i = 0; i < ctx->msgcount; i++) + if (queue) { - h = ctx->hdrs[i]; + imap_exec(idata, buf, IMAP_CMD_QUEUE); + queued = 1; + return 0; + } + else if (!queued) + imap_exec(idata, buf, 0); - if (h->deleted) - { - imap_cache_del(idata, h); -#ifdef USE_HCACHE - imap_hcache_del(idata, HEADER_DATA(h)->uid); -#endif - } + queued = 0; + if ((status = imap_mboxcache_get(idata, mbox, 0))) + return status->messages; - if (h->active && h->changed) - { -#ifdef USE_HCACHE - imap_hcache_put(idata, h); -#endif - /* if the message has been rethreaded or attachments have been deleted - * we delete the message and reupload it. - * This works better if we're expunging, of course. */ - if ((h->env && (h->env->refs_changed || h->env->irt_changed)) || - h->attach_del || h->xlabel_changed) - { - mutt_message(_("Saving changed messages... [%d/%d]"), i + 1, ctx->msgcount); - if (!appendctx) - appendctx = mx_open_mailbox(ctx->path, MUTT_APPEND | MUTT_QUIET, NULL); - if (!appendctx) - mutt_debug( - 1, "imap_sync_mailbox: Error opening mailbox in append mode\n"); - else - _mutt_save_message(h, appendctx, 1, 0, 0); - h->xlabel_changed = false; - } - } - } + return 0; +} +/** + * imap_mboxcache_get - Open an hcache for a mailbox + * + * return cached mailbox stats or NULL if create is 0 + */ +struct ImapStatus *imap_mboxcache_get(struct ImapData *idata, const char *mbox, int create) +{ + struct ImapStatus *status = NULL; #ifdef USE_HCACHE - imap_hcache_close(idata); + header_cache_t *hc = NULL; + void *uidvalidity = NULL; + void *uidnext = NULL; #endif - /* presort here to avoid doing 10 resorts in imap_exec_msgset */ - oldsort = Sort; - if (Sort != SORT_ORDER) + struct ListNode *np; + STAILQ_FOREACH(np, &idata->mboxcache, entries) { - hdrs = ctx->hdrs; - ctx->hdrs = safe_malloc(ctx->msgcount * sizeof(struct Header *)); - memcpy(ctx->hdrs, hdrs, ctx->msgcount * sizeof(struct Header *)); + status = (struct ImapStatus *) np->data; - Sort = SORT_ORDER; - qsort(ctx->hdrs, ctx->msgcount, sizeof(struct Header *), mutt_get_sort_func(SORT_ORDER)); + if (imap_mxcmp(mbox, status->name) == 0) + return status; } + status = NULL; - rc = sync_helper(idata, MUTT_ACL_DELETE, MUTT_DELETED, "\\Deleted"); - if (rc >= 0) - rc |= sync_helper(idata, MUTT_ACL_WRITE, MUTT_FLAG, "\\Flagged"); - if (rc >= 0) - rc |= sync_helper(idata, MUTT_ACL_WRITE, MUTT_OLD, "Old"); - if (rc >= 0) - rc |= sync_helper(idata, MUTT_ACL_SEEN, MUTT_READ, "\\Seen"); - if (rc >= 0) - rc |= sync_helper(idata, MUTT_ACL_WRITE, MUTT_REPLIED, "\\Answered"); - - if (oldsort != Sort) + /* lame */ + if (create) { - Sort = oldsort; - FREE(&ctx->hdrs); - ctx->hdrs = hdrs; + struct ImapStatus *scache = safe_calloc(1, sizeof(struct ImapStatus)); + scache->name = (char *) mbox; + mutt_list_insert_tail(&idata->mboxcache, (char *) scache); + status = imap_mboxcache_get(idata, mbox, 0); + status->name = safe_strdup(mbox); } - /* Flush the queued flags if any were changed in sync_helper. */ - if (rc > 0) - if (imap_exec(idata, NULL, 0) != IMAP_CMD_OK) - rc = -1; - - if (rc < 0) +#ifdef USE_HCACHE + hc = imap_hcache_open(idata, mbox); + if (hc) { - if (ctx->closing) + uidvalidity = mutt_hcache_fetch_raw(hc, "/UIDVALIDITY", 12); + uidnext = mutt_hcache_fetch_raw(hc, "/UIDNEXT", 8); + if (uidvalidity) { - if (mutt_yesorno(_("Error saving flags. Close anyway?"), 0) == MUTT_YES) + if (!status) { - rc = 0; - idata->state = IMAP_AUTHENTICATED; - goto out; - } - } - else - mutt_error(_("Error saving flags")); - rc = -1; - goto out; - } - - /* Update local record of server state to reflect the synchronization just - * completed. imap_read_headers always overwrites hcache-origin flags, so - * there is no need to mutate the hcache after flag-only changes. */ - for (int i = 0; i < ctx->msgcount; i++) - { - HEADER_DATA(ctx->hdrs[i])->deleted = ctx->hdrs[i]->deleted; - HEADER_DATA(ctx->hdrs[i])->flagged = ctx->hdrs[i]->flagged; - HEADER_DATA(ctx->hdrs[i])->old = ctx->hdrs[i]->old; - HEADER_DATA(ctx->hdrs[i])->read = ctx->hdrs[i]->read; - HEADER_DATA(ctx->hdrs[i])->replied = ctx->hdrs[i]->replied; - ctx->hdrs[i]->changed = false; - } - ctx->changed = false; - - /* We must send an EXPUNGE command if we're not closing. */ - if (expunge && !(ctx->closing) && mutt_bit_isset(ctx->rights, MUTT_ACL_DELETE)) - { - mutt_message(_("Expunging messages from server...")); - /* Set expunge bit so we don't get spurious reopened messages */ - idata->reopen |= IMAP_EXPUNGE_EXPECTED; - if (imap_exec(idata, "EXPUNGE", 0) != 0) - { - idata->reopen &= ~IMAP_EXPUNGE_EXPECTED; - imap_error(_("imap_sync_mailbox: EXPUNGE failed"), idata->buf); - rc = -1; - goto out; + mutt_hcache_free(hc, &uidvalidity); + mutt_hcache_free(hc, &uidnext); + mutt_hcache_close(hc); + return imap_mboxcache_get(idata, mbox, 1); + } + status->uidvalidity = *(unsigned int *) uidvalidity; + status->uidnext = uidnext ? *(unsigned int *) uidnext : 0; + mutt_debug(3, "mboxcache: hcache uidvalidity %d, uidnext %d\n", + status->uidvalidity, status->uidnext); } - idata->reopen &= ~IMAP_EXPUNGE_EXPECTED; - } - - if (expunge && ctx->closing) - { - imap_exec(idata, "CLOSE", IMAP_CMD_QUEUE); - idata->state = IMAP_AUTHENTICATED; + mutt_hcache_free(hc, &uidvalidity); + mutt_hcache_free(hc, &uidnext); + mutt_hcache_close(hc); } +#endif - if (option(OPT_MESSAGE_CACHE_CLEAN)) - imap_cache_clean(idata); + return status; +} - rc = 0; +void imap_mboxcache_free(struct ImapData *idata) +{ + struct ImapStatus *status = NULL; -out: - if (appendctx) + struct ListNode *np; + STAILQ_FOREACH(np, &idata->mboxcache, entries) { - mx_fastclose_mailbox(appendctx); - FREE(&appendctx); + status = (struct ImapStatus *) np->data; + FREE(&status->name); } - return rc; + + mutt_list_free(&idata->mboxcache); } -/** - * imap_close_mailbox - Clean up IMAP data in Context - */ -int imap_close_mailbox(struct Context *ctx) +int imap_search(struct Context *ctx, const struct Pattern *pat) { - struct ImapData *idata = NULL; + struct Buffer buf; + struct ImapData *idata = ctx->data; + for (int i = 0; i < ctx->msgcount; i++) + ctx->hdrs[i]->matched = false; - idata = ctx->data; - /* Check to see if the mailbox is actually open */ - if (!idata) + if (!do_search(pat, 1)) return 0; - /* imap_open_mailbox_append() borrows the struct ImapData temporarily, - * just for the connection, but does not set idata->ctx to the - * open-append ctx. - * - * So when these are equal, it means we are actually closing the - * mailbox and should clean up idata. Otherwise, we don't want to - * touch idata - it's still being used. - */ - if (ctx == idata->ctx) + mutt_buffer_init(&buf); + mutt_buffer_addstr(&buf, "UID SEARCH "); + if (imap_compile_search(ctx, pat, &buf) < 0) { - if (idata->status != IMAP_FATAL && idata->state >= IMAP_SELECTED) - { - /* mx_close_mailbox won't sync if there are no deleted messages - * and the mailbox is unchanged, so we may have to close here */ - if (!ctx->deleted) - imap_exec(idata, "CLOSE", IMAP_CMD_QUEUE); - idata->state = IMAP_AUTHENTICATED; - } - - idata->reopen &= IMAP_REOPEN_ALLOW; - FREE(&(idata->mailbox)); - mutt_list_free(&idata->flags); - idata->ctx = NULL; + FREE(&buf.data); + return -1; + } + if (imap_exec(idata, buf.data, 0) < 0) + { + FREE(&buf.data); + return -1; + } - hash_destroy(&idata->uid_hash, NULL); - FREE(&idata->msn_index); - idata->msn_index_size = 0; - idata->max_msn = 0; + FREE(&buf.data); + return 0; +} - for (int i = 0; i < IMAP_CACHE_LEN; i++) - { - if (idata->cache[i].path) - { - unlink(idata->cache[i].path); - FREE(&idata->cache[i].path); - } - } +int imap_subscribe(char *path, int subscribe) +{ + struct ImapData *idata = NULL; + char buf[LONG_STRING]; + char mbox[LONG_STRING]; + char errstr[STRING]; + struct Buffer err, token; + struct ImapMbox mx; - mutt_bcache_close(&idata->bcache); + if (!mx_is_imap(path) || imap_parse_path(path, &mx) || !mx.mbox) + { + mutt_error(_("Bad mailbox name")); + return -1; } + idata = imap_conn_find(&(mx.account), 0); + if (!idata) + goto fail; - /* free IMAP part of headers */ - for (int i = 0; i < ctx->msgcount; i++) + imap_fix_path(idata, mx.mbox, buf, sizeof(buf)); + if (!*buf) + strfcpy(buf, "INBOX", sizeof(buf)); + + if (option(OPT_IMAP_CHECK_SUBSCRIBED)) { - /* mailbox may not have fully loaded */ - if (ctx->hdrs[i] && ctx->hdrs[i]->data) - imap_free_header_data((struct ImapHeaderData **) &(ctx->hdrs[i]->data)); + mutt_buffer_init(&token); + mutt_buffer_init(&err); + err.data = errstr; + err.dsize = sizeof(errstr); + snprintf(mbox, sizeof(mbox), "%smailboxes \"%s\"", subscribe ? "" : "un", path); + if (mutt_parse_rc_line(mbox, &token, &err)) + mutt_debug(1, "Error adding subscribed mailbox: %s\n", errstr); + FREE(&token.data); } + if (subscribe) + mutt_message(_("Subscribing to %s..."), buf); + else + mutt_message(_("Unsubscribing from %s..."), buf); + imap_munge_mbox_name(idata, mbox, sizeof(mbox), buf); + + snprintf(buf, sizeof(buf), "%sSUBSCRIBE %s", subscribe ? "" : "UN", mbox); + + if (imap_exec(idata, buf, 0) < 0) + goto fail; + + imap_unmunge_mbox_name(idata, mx.mbox); + if (subscribe) + mutt_message(_("Subscribed to %s"), mx.mbox); + else + mutt_message(_("Unsubscribed from %s"), mx.mbox); + FREE(&mx.mbox); return 0; + +fail: + FREE(&mx.mbox); + return -1; } /** - * imap_check_mailbox - use the NOOP or IDLE command to poll for new mail - * @param ctx Context - * @param force Don't wait - * @retval #MUTT_REOPENED mailbox has been externally modified - * @retval #MUTT_NEW_MAIL new mail has arrived - * @retval 0 no change - * @retval -1 error + * imap_complete - Try to complete an IMAP folder path + * + * Given a partial IMAP folder path, return a string which adds as much to the + * path as is unique */ -int imap_check_mailbox(struct Context *ctx, int force) -{ - return imap_check(ctx->data, force); -} - -int imap_check(struct ImapData *idata, int force) +int imap_complete(char *dest, size_t dlen, char *path) { - /* overload keyboard timeout to avoid many mailbox checks in a row. - * Most users don't like having to wait exactly when they press a key. */ - int result = 0; + struct ImapData *idata = NULL; + char list[LONG_STRING]; + char buf[LONG_STRING]; + struct ImapList listresp; + char completion[LONG_STRING]; + int clen; + size_t matchlen = 0; + int completions = 0; + struct ImapMbox mx; + int rc; - /* try IDLE first, unless force is set */ - if (!force && option(OPT_IMAP_IDLE) && mutt_bit_isset(idata->capabilities, IDLE) && - (idata->state != IMAP_IDLE || time(NULL) >= idata->lastread + ImapKeepalive)) + if (imap_parse_path(path, &mx)) { - if (imap_cmd_idle(idata) < 0) - return -1; + strfcpy(dest, path, dlen); + return imap_complete_hosts(dest, dlen); } - if (idata->state == IMAP_IDLE) + + /* don't open a new socket just for completion. Instead complete over + * known mailboxes/hooks/etc */ + idata = imap_conn_find(&(mx.account), MUTT_IMAP_CONN_NONEW); + if (!idata) { - while ((result = mutt_socket_poll(idata->conn, 0)) > 0) - { - if (imap_cmd_step(idata) != IMAP_CMD_CONTINUE) - { - mutt_debug(1, "Error reading IDLE response\n"); - return -1; - } - } - if (result < 0) - { - mutt_debug(1, "Poll failed, disabling IDLE\n"); - mutt_bit_unset(idata->capabilities, IDLE); - } + FREE(&mx.mbox); + strfcpy(dest, path, dlen); + return imap_complete_hosts(dest, dlen); } - if ((force || (idata->state != IMAP_IDLE && time(NULL) >= idata->lastread + Timeout)) && - imap_exec(idata, "NOOP", IMAP_CMD_POLL) != 0) - return -1; + /* reformat path for IMAP list, and append wildcard */ + /* don't use INBOX in place of "" */ + if (mx.mbox && mx.mbox[0]) + imap_fix_path(idata, mx.mbox, list, sizeof(list)); + else + list[0] = '\0'; - /* We call this even when we haven't run NOOP in case we have pending - * changes to process, since we can reopen here. */ - imap_cmd_finish(idata); + /* fire off command */ + snprintf(buf, sizeof(buf), "%s \"\" \"%s%%\"", + option(OPT_IMAP_LIST_SUBSCRIBED) ? "LSUB" : "LIST", list); - if (idata->check_status & IMAP_EXPUNGE_PENDING) - result = MUTT_REOPENED; - else if (idata->check_status & IMAP_NEWMAIL_PENDING) - result = MUTT_NEW_MAIL; - else if (idata->check_status & IMAP_FLAGS_PENDING) - result = MUTT_FLAGS; + imap_cmd_start(idata, buf); - idata->check_status = 0; + /* and see what the results are */ + strfcpy(completion, NONULL(mx.mbox), sizeof(completion)); + idata->cmdtype = IMAP_CT_LIST; + idata->cmddata = &listresp; + do + { + listresp.name = NULL; + rc = imap_cmd_step(idata); + + if (rc == IMAP_CMD_CONTINUE && listresp.name) + { + /* if the folder isn't selectable, append delimiter to force browse + * to enter it on second tab. */ + if (listresp.noselect) + { + clen = strlen(listresp.name); + listresp.name[clen++] = listresp.delim; + listresp.name[clen] = '\0'; + } + /* copy in first word */ + if (!completions) + { + strfcpy(completion, listresp.name, sizeof(completion)); + matchlen = strlen(completion); + completions++; + continue; + } - return result; -} + matchlen = longest_common_prefix(completion, listresp.name, 0, matchlen); + completions++; + } + } while (rc == IMAP_CMD_CONTINUE); + idata->cmddata = NULL; -static int imap_check_mailbox_reopen(struct Context *ctx, int *index_hint) -{ - int rc; - (void) index_hint; + if (completions) + { + /* reformat output */ + imap_qualify_path(dest, dlen, &mx, completion); + mutt_pretty_mailbox(dest, dlen); - imap_allow_reopen(ctx); - rc = imap_check(ctx->data, 0); - imap_disallow_reopen(ctx); + FREE(&mx.mbox); + return 0; + } - return rc; + return -1; } /** - * imap_get_mailbox - split path into (idata,mailbox name) + * imap_fast_trash - Use server COPY command to copy deleted messages to trash + * @retval -1 error + * @retval 0 success + * @retval 1 non-fatal error - try fetch/append */ -static int imap_get_mailbox(const char *path, struct ImapData **hidata, char *buf, size_t blen) +int imap_fast_trash(struct Context *ctx, char *dest) { + struct ImapData *idata = NULL; + char mbox[LONG_STRING]; + char mmbox[LONG_STRING]; + char prompt[LONG_STRING]; + int rc; struct ImapMbox mx; + bool triedcreate = false; + struct Buffer *sync_cmd = NULL; + int err_continue = MUTT_NO; - if (imap_parse_path(path, &mx)) + idata = ctx->data; + + if (imap_parse_path(dest, &mx)) { - mutt_debug(1, "imap_get_mailbox: Error parsing %s\n", path); + mutt_debug(1, "imap_fast_trash: bad destination %s\n", dest); return -1; } - if (!(*hidata = imap_conn_find(&(mx.account), option(OPT_IMAP_PASSIVE) ? MUTT_IMAP_CONN_NONEW : 0)) || - (*hidata)->state < IMAP_AUTHENTICATED) + + /* check that the save-to folder is in the same account */ + if (!mutt_account_match(&(idata->conn->account), &(mx.account))) { - FREE(&mx.mbox); - return -1; + mutt_debug(3, "imap_fast_trash: %s not same server as %s\n", dest, ctx->path); + return 1; } - imap_fix_path(*hidata, mx.mbox, buf, blen); - if (!*buf) - strfcpy(buf, "INBOX", blen); - FREE(&mx.mbox); - - return 0; -} - -/** - * imap_buffy_check - Check for new mail in subscribed folders - * - * Given a list of mailboxes rather than called once for each so that it can - * batch the commands and save on round trips. Returns number of mailboxes with - * new mail. - */ -int imap_buffy_check(int force, int check_stats) -{ - struct ImapData *idata = NULL; - struct ImapData *lastdata = NULL; - struct Buffy *mailbox = NULL; - char name[LONG_STRING]; - char command[LONG_STRING]; - char munged[LONG_STRING]; - int buffies = 0; + imap_fix_path(idata, mx.mbox, mbox, sizeof(mbox)); + if (!*mbox) + strfcpy(mbox, "INBOX", sizeof(mbox)); + imap_munge_mbox_name(idata, mmbox, sizeof(mmbox), mbox); - for (mailbox = Incoming; mailbox; mailbox = mailbox->next) + sync_cmd = mutt_buffer_new(); + for (int i = 0; i < ctx->msgcount; i++) { - /* Init newly-added mailboxes */ - if (!mailbox->magic) + if (ctx->hdrs[i]->active && ctx->hdrs[i]->changed && + ctx->hdrs[i]->deleted && !ctx->hdrs[i]->purge) { - if (mx_is_imap(mailbox->path)) - mailbox->magic = MUTT_IMAP; + rc = imap_sync_message_for_copy(idata, ctx->hdrs[i], sync_cmd, &err_continue); + if (rc < 0) + { + mutt_debug(1, "imap_fast_trash: could not sync\n"); + goto out; + } } + } - if (mailbox->magic != MUTT_IMAP) - continue; - - if (imap_get_mailbox(mailbox->path, &idata, name, sizeof(name)) < 0) + /* loop in case of TRYCREATE */ + do + { + rc = imap_exec_msgset(idata, "UID COPY", mmbox, MUTT_TRASH, 0, 0); + if (!rc) { - mailbox->new = false; - continue; + mutt_debug(1, "imap_fast_trash: No messages to trash\n"); + rc = -1; + goto out; } - - /* Don't issue STATUS on the selected mailbox, it will be NOOPed or - * IDLEd elsewhere. - * idata->mailbox may be NULL for connections other than the current - * mailbox's, and shouldn't expand to INBOX in that case. #3216. */ - if (idata->mailbox && (imap_mxcmp(name, idata->mailbox) == 0)) + else if (rc < 0) { - mailbox->new = false; - continue; + mutt_debug(1, "could not queue copy\n"); + goto out; } + else + mutt_message(_("Copying %d messages to %s..."), rc, mbox); - if (!mutt_bit_isset(idata->capabilities, IMAP4REV1) && - !mutt_bit_isset(idata->capabilities, STATUS)) + /* let's get it on */ + rc = imap_exec(idata, NULL, IMAP_CMD_FAIL_OK); + if (rc == -2) { - mutt_debug(2, "Server doesn't support STATUS\n"); - continue; + if (triedcreate) + { + mutt_debug(1, "Already tried to create mailbox %s\n", mbox); + break; + } + /* bail out if command failed for reasons other than nonexistent target */ + if (mutt_strncasecmp(imap_get_qualifier(idata->buf), "[TRYCREATE]", 11) != 0) + break; + mutt_debug(3, "imap_fast_trash: server suggests TRYCREATE\n"); + snprintf(prompt, sizeof(prompt), _("Create %s?"), mbox); + if (option(OPT_CONFIRMCREATE) && mutt_yesorno(prompt, 1) != MUTT_YES) + { + mutt_clear_error(); + goto out; + } + if (imap_create_mailbox(idata, mbox) < 0) + break; + triedcreate = true; } + } while (rc == -2); - if (lastdata && idata != lastdata) - { - /* Send commands to previous server. Sorting the buffy list - * may prevent some infelicitous interleavings */ - if (imap_exec(lastdata, NULL, IMAP_CMD_FAIL_OK | IMAP_CMD_POLL) == -1) - mutt_debug(1, "Error polling mailboxes\n"); + if (rc != 0) + { + imap_error("imap_fast_trash", idata->buf); + goto out; + } - lastdata = NULL; - } + rc = 0; - if (!lastdata) - lastdata = idata; +out: + mutt_buffer_free(&sync_cmd); + FREE(&mx.mbox); - imap_munge_mbox_name(idata, munged, sizeof(munged), name); - if (check_stats) - snprintf(command, sizeof(command), - "STATUS %s (UIDNEXT UIDVALIDITY UNSEEN RECENT MESSAGES)", munged); - else - snprintf(command, sizeof(command), - "STATUS %s (UIDNEXT UIDVALIDITY UNSEEN RECENT)", munged); + return rc < 0 ? -1 : rc; +} - if (imap_exec(idata, command, IMAP_CMD_QUEUE | IMAP_CMD_POLL) < 0) - { - mutt_debug(1, "Error queueing command\n"); - return 0; - } - } +static int imap_open_mailbox(struct Context *ctx) +{ + struct ImapData *idata = NULL; + struct ImapStatus *status = NULL; + char buf[LONG_STRING]; + char bufout[LONG_STRING]; + int count = 0; + struct ImapMbox mx, pmx; + int rc; - if (lastdata && (imap_exec(lastdata, NULL, IMAP_CMD_FAIL_OK | IMAP_CMD_POLL) == -1)) + if (imap_parse_path(ctx->path, &mx)) { - mutt_debug(1, "Error polling mailboxes\n"); - return 0; + mutt_error(_("%s is an invalid IMAP path"), ctx->path); + return -1; } - /* collect results */ - for (mailbox = Incoming; mailbox; mailbox = mailbox->next) - { - if (mailbox->magic == MUTT_IMAP && mailbox->new) - buffies++; - } + /* we require a connection which isn't currently in IMAP_SELECTED state */ + idata = imap_conn_find(&(mx.account), MUTT_IMAP_CONN_NOSELECT); + if (!idata) + goto fail_noidata; + if (idata->state < IMAP_AUTHENTICATED) + goto fail; - return buffies; -} + /* once again the context is new */ + ctx->data = idata; -/** - * imap_status - Get the status of a mailbox - * @retval -1 on error - * @retval >=0 count of messages in mailbox - * - * if queue != 0, queue the command and expect it to have been run - * on the next call (for pipelining the postponed count) - */ -int imap_status(char *path, int queue) -{ - static int queued = 0; + /* Clean up path and replace the one in the ctx */ + imap_fix_path(idata, mx.mbox, buf, sizeof(buf)); + if (!*buf) + strfcpy(buf, "INBOX", sizeof(buf)); + FREE(&(idata->mailbox)); + idata->mailbox = safe_strdup(buf); + imap_qualify_path(buf, sizeof(buf), &mx, idata->mailbox); - struct ImapData *idata = NULL; - char buf[LONG_STRING]; - char mbox[LONG_STRING]; - struct ImapStatus *status = NULL; + FREE(&(ctx->path)); + FREE(&(ctx->realpath)); + ctx->path = safe_strdup(buf); + ctx->realpath = safe_strdup(ctx->path); + + idata->ctx = ctx; + + /* clear mailbox status */ + idata->status = false; + memset(idata->ctx->rights, 0, sizeof(idata->ctx->rights)); + idata->new_mail_count = 0; + idata->max_msn = 0; - if (imap_get_mailbox(path, &idata, buf, sizeof(buf)) < 0) - return -1; + mutt_message(_("Selecting %s..."), idata->mailbox); + imap_munge_mbox_name(idata, buf, sizeof(buf), idata->mailbox); - /* We are in the folder we're polling - just return the mailbox count. - * - * Note that imap_mxcmp() converts NULL to "INBOX", so we need to - * make sure the idata really is open to a folder. */ - if (idata->ctx && !imap_mxcmp(buf, idata->mailbox)) - return idata->ctx->msgcount; - else if (mutt_bit_isset(idata->capabilities, IMAP4REV1) || - mutt_bit_isset(idata->capabilities, STATUS)) + /* pipeline ACL test */ + if (mutt_bit_isset(idata->capabilities, ACL)) { - imap_munge_mbox_name(idata, mbox, sizeof(mbox), buf); - snprintf(buf, sizeof(buf), "STATUS %s (%s)", mbox, "MESSAGES"); - imap_unmunge_mbox_name(idata, mbox); + snprintf(bufout, sizeof(bufout), "MYRIGHTS %s", buf); + imap_exec(idata, bufout, IMAP_CMD_QUEUE); } + /* assume we have all rights if ACL is unavailable */ else - /* Server does not support STATUS, and this is not the current mailbox. - * There is no lightweight way to check recent arrivals */ - return -1; - - if (queue) { - imap_exec(idata, buf, IMAP_CMD_QUEUE); - queued = 1; - return 0; + mutt_bit_set(idata->ctx->rights, MUTT_ACL_LOOKUP); + mutt_bit_set(idata->ctx->rights, MUTT_ACL_READ); + mutt_bit_set(idata->ctx->rights, MUTT_ACL_SEEN); + mutt_bit_set(idata->ctx->rights, MUTT_ACL_WRITE); + mutt_bit_set(idata->ctx->rights, MUTT_ACL_INSERT); + mutt_bit_set(idata->ctx->rights, MUTT_ACL_POST); + mutt_bit_set(idata->ctx->rights, MUTT_ACL_CREATE); + mutt_bit_set(idata->ctx->rights, MUTT_ACL_DELETE); } - else if (!queued) - imap_exec(idata, buf, 0); + /* pipeline the postponed count if possible */ + pmx.mbox = NULL; + if (mx_is_imap(Postponed) && !imap_parse_path(Postponed, &pmx) && + mutt_account_match(&pmx.account, &mx.account)) + imap_status(Postponed, 1); + FREE(&pmx.mbox); - queued = 0; - if ((status = imap_mboxcache_get(idata, mbox, 0))) - return status->messages; + snprintf(bufout, sizeof(bufout), "%s %s", + ctx->readonly ? "EXAMINE" : "SELECT", buf); - return 0; -} + idata->state = IMAP_SELECTED; -/** - * imap_mboxcache_get - Open an hcache for a mailbox - * - * return cached mailbox stats or NULL if create is 0 - */ -struct ImapStatus *imap_mboxcache_get(struct ImapData *idata, const char *mbox, int create) -{ - struct ImapStatus *status = NULL; -#ifdef USE_HCACHE - header_cache_t *hc = NULL; - void *uidvalidity = NULL; - void *uidnext = NULL; -#endif + imap_cmd_start(idata, bufout); - struct ListNode *np; - STAILQ_FOREACH(np, &idata->mboxcache, entries) + status = imap_mboxcache_get(idata, idata->mailbox, 1); + + do { - status = (struct ImapStatus *) np->data; + char *pc = NULL; - if (imap_mxcmp(mbox, status->name) == 0) - return status; + rc = imap_cmd_step(idata); + if (rc != IMAP_CMD_CONTINUE) + break; + + pc = idata->buf + 2; + + /* Obtain list of available flags here, may be overridden by a + * PERMANENTFLAGS tag in the OK response */ + if (mutt_strncasecmp("FLAGS", pc, 5) == 0) + { + /* don't override PERMANENTFLAGS */ + if (STAILQ_EMPTY(&idata->flags)) + { + mutt_debug(3, "Getting mailbox FLAGS\n"); + pc = imap_get_flags(&idata->flags, pc); + if (!pc) + goto fail; + } + } + /* PERMANENTFLAGS are massaged to look like FLAGS, then override FLAGS */ + else if (mutt_strncasecmp("OK [PERMANENTFLAGS", pc, 18) == 0) + { + mutt_debug(3, "Getting mailbox PERMANENTFLAGS\n"); + /* safe to call on NULL */ + mutt_list_free(&idata->flags); + /* skip "OK [PERMANENT" so syntax is the same as FLAGS */ + pc += 13; + pc = imap_get_flags(&(idata->flags), pc); + if (!pc) + goto fail; + } + /* save UIDVALIDITY for the header cache */ + else if (mutt_strncasecmp("OK [UIDVALIDITY", pc, 14) == 0) + { + mutt_debug(3, "Getting mailbox UIDVALIDITY\n"); + pc += 3; + pc = imap_next_word(pc); + idata->uid_validity = strtol(pc, NULL, 10); + status->uidvalidity = idata->uid_validity; + } + else if (mutt_strncasecmp("OK [UIDNEXT", pc, 11) == 0) + { + mutt_debug(3, "Getting mailbox UIDNEXT\n"); + pc += 3; + pc = imap_next_word(pc); + idata->uidnext = strtol(pc, NULL, 10); + status->uidnext = idata->uidnext; + } + else + { + pc = imap_next_word(pc); + if (mutt_strncasecmp("EXISTS", pc, 6) == 0) + { + count = idata->new_mail_count; + idata->new_mail_count = 0; + } + } + } while (rc == IMAP_CMD_CONTINUE); + + if (rc == IMAP_CMD_NO) + { + char *s = NULL; + s = imap_next_word(idata->buf); /* skip seq */ + s = imap_next_word(s); /* Skip response */ + mutt_error("%s", s); + mutt_sleep(2); + goto fail; } - status = NULL; - /* lame */ - if (create) + if (rc != IMAP_CMD_OK) + goto fail; + + /* check for READ-ONLY notification */ + if ((mutt_strncasecmp(imap_get_qualifier(idata->buf), "[READ-ONLY]", 11) == 0) && + !mutt_bit_isset(idata->capabilities, ACL)) { - struct ImapStatus *scache = safe_calloc(1, sizeof(struct ImapStatus)); - scache->name = (char *) mbox; - mutt_list_insert_tail(&idata->mboxcache, (char *) scache); - status = imap_mboxcache_get(idata, mbox, 0); - status->name = safe_strdup(mbox); + mutt_debug(2, "Mailbox is read-only.\n"); + ctx->readonly = true; } -#ifdef USE_HCACHE - hc = imap_hcache_open(idata, mbox); - if (hc) +#ifdef DEBUG + /* dump the mailbox flags we've found */ + if (debuglevel > 2) { - uidvalidity = mutt_hcache_fetch_raw(hc, "/UIDVALIDITY", 12); - uidnext = mutt_hcache_fetch_raw(hc, "/UIDNEXT", 8); - if (uidvalidity) + if (STAILQ_EMPTY(&idata->flags)) + mutt_debug(3, "No folder flags found\n"); + else { - if (!status) + struct ListNode *np; + struct Buffer flag_buffer; + mutt_buffer_init(&flag_buffer); + mutt_buffer_printf(&flag_buffer, "Mailbox flags: "); + STAILQ_FOREACH(np, &idata->flags, entries) { - mutt_hcache_free(hc, &uidvalidity); - mutt_hcache_free(hc, &uidnext); - mutt_hcache_close(hc); - return imap_mboxcache_get(idata, mbox, 1); + mutt_buffer_printf(&flag_buffer, "[%s] ", np->data); } - status->uidvalidity = *(unsigned int *) uidvalidity; - status->uidnext = uidnext ? *(unsigned int *) uidnext : 0; - mutt_debug(3, "mboxcache: hcache uidvalidity %d, uidnext %d\n", - status->uidvalidity, status->uidnext); + mutt_debug(3, "%s\n", flag_buffer.data); + FREE(&flag_buffer.data); } - mutt_hcache_free(hc, &uidvalidity); - mutt_hcache_free(hc, &uidnext); - mutt_hcache_close(hc); } #endif - return status; + if (!(mutt_bit_isset(idata->ctx->rights, MUTT_ACL_DELETE) || + mutt_bit_isset(idata->ctx->rights, MUTT_ACL_SEEN) || + mutt_bit_isset(idata->ctx->rights, MUTT_ACL_WRITE) || + mutt_bit_isset(idata->ctx->rights, MUTT_ACL_INSERT))) + ctx->readonly = true; + + ctx->hdrmax = count; + ctx->hdrs = safe_calloc(count, sizeof(struct Header *)); + ctx->v2r = safe_calloc(count, sizeof(int)); + ctx->msgcount = 0; + + if (count && (imap_read_headers(idata, 1, count) < 0)) + { + mutt_error(_("Error opening mailbox")); + mutt_sleep(1); + goto fail; + } + + mutt_debug(2, "imap_open_mailbox: msgcount is %d\n", ctx->msgcount); + FREE(&mx.mbox); + return 0; + +fail: + if (idata->state == IMAP_SELECTED) + idata->state = IMAP_AUTHENTICATED; +fail_noidata: + FREE(&mx.mbox); + return -1; } -void imap_mboxcache_free(struct ImapData *idata) +static int imap_open_mailbox_append(struct Context *ctx, int flags) { - struct ImapStatus *status = NULL; + struct ImapData *idata = NULL; + char buf[LONG_STRING]; + char mailbox[LONG_STRING]; + struct ImapMbox mx; + int rc; - struct ListNode *np; - STAILQ_FOREACH(np, &idata->mboxcache, entries) + if (imap_parse_path(ctx->path, &mx)) + return -1; + + /* in APPEND mode, we appear to hijack an existing IMAP connection - + * ctx is brand new and mostly empty */ + + idata = imap_conn_find(&(mx.account), 0); + if (!idata) { - status = (struct ImapStatus *) np->data; - FREE(&status->name); + FREE(&mx.mbox); + return -1; } - mutt_list_free(&idata->mboxcache); -} + ctx->data = idata; + + imap_fix_path(idata, mx.mbox, mailbox, sizeof(mailbox)); + if (!*mailbox) + strfcpy(mailbox, "INBOX", sizeof(mailbox)); + FREE(&mx.mbox); + + rc = imap_access(ctx->path); + if (rc == 0) + return 0; -/** - * do_search - Perform a search of messages - * - * returns number of patterns in the search that should be done server-side - * (eg are full-text) - */ -static int do_search(const struct Pattern *search, int allpats) -{ - int rc = 0; - const struct Pattern *pat = NULL; + if (rc == -1) + return -1; - for (pat = search; pat; pat = pat->next) - { - switch (pat->op) - { - case MUTT_BODY: - case MUTT_HEADER: - case MUTT_WHOLE_MSG: - if (pat->stringmatch) - rc++; - break; - case MUTT_SERVERSEARCH: - rc++; - break; - default: - if (pat->child && do_search(pat->child, 1)) - rc++; - } + snprintf(buf, sizeof(buf), _("Create %s?"), mailbox); + if (option(OPT_CONFIRMCREATE) && mutt_yesorno(buf, 1) != MUTT_YES) + return -1; - if (!allpats) - break; - } + if (imap_create_mailbox(idata, mailbox) < 0) + return -1; - return rc; + return 0; } /** - * imap_compile_search - Convert NeoMutt pattern to IMAP search - * - * Convert neomutt Pattern to IMAP SEARCH command containing only elements - * that require full-text search (neomutt already has what it needs for most - * match types, and does a better job (eg server doesn't support regexes). + * imap_close_mailbox - Clean up IMAP data in Context */ -static int imap_compile_search(struct Context *ctx, const struct Pattern *pat, - struct Buffer *buf) +int imap_close_mailbox(struct Context *ctx) { - if (!do_search(pat, 0)) - return 0; + struct ImapData *idata = NULL; - if (pat->not) - mutt_buffer_addstr(buf, "NOT "); + idata = ctx->data; + /* Check to see if the mailbox is actually open */ + if (!idata) + return 0; - if (pat->child) + /* imap_open_mailbox_append() borrows the struct ImapData temporarily, + * just for the connection, but does not set idata->ctx to the + * open-append ctx. + * + * So when these are equal, it means we are actually closing the + * mailbox and should clean up idata. Otherwise, we don't want to + * touch idata - it's still being used. + */ + if (ctx == idata->ctx) { - int clauses; - - clauses = do_search(pat->child, 1); - if (clauses > 0) + if (idata->status != IMAP_FATAL && idata->state >= IMAP_SELECTED) { - const struct Pattern *clause = pat->child; - - mutt_buffer_addch(buf, '('); - - while (clauses) - { - if (do_search(clause, 0)) - { - if (pat->op == MUTT_OR && clauses > 1) - mutt_buffer_addstr(buf, "OR "); - clauses--; - - if (imap_compile_search(ctx, clause, buf) < 0) - return -1; - - if (clauses) - mutt_buffer_addch(buf, ' '); - } - clause = clause->next; - } - - mutt_buffer_addch(buf, ')'); + /* mx_close_mailbox won't sync if there are no deleted messages + * and the mailbox is unchanged, so we may have to close here */ + if (!ctx->deleted) + imap_exec(idata, "CLOSE", IMAP_CMD_QUEUE); + idata->state = IMAP_AUTHENTICATED; } - } - else - { - char term[STRING]; - char *delim = NULL; - switch (pat->op) - { - case MUTT_HEADER: - mutt_buffer_addstr(buf, "HEADER "); + idata->reopen &= IMAP_REOPEN_ALLOW; + FREE(&(idata->mailbox)); + mutt_list_free(&idata->flags); + idata->ctx = NULL; - /* extract header name */ - delim = strchr(pat->p.str, ':'); - if (!delim) - { - mutt_error(_("Header search without header name: %s"), pat->p.str); - return -1; - } - *delim = '\0'; - imap_quote_string(term, sizeof(term), pat->p.str); - mutt_buffer_addstr(buf, term); - mutt_buffer_addch(buf, ' '); + hash_destroy(&idata->uid_hash, NULL); + FREE(&idata->msn_index); + idata->msn_index_size = 0; + idata->max_msn = 0; - /* and field */ - *delim = ':'; - delim++; - SKIPWS(delim); - imap_quote_string(term, sizeof(term), delim); - mutt_buffer_addstr(buf, term); - break; - case MUTT_BODY: - mutt_buffer_addstr(buf, "BODY "); - imap_quote_string(term, sizeof(term), pat->p.str); - mutt_buffer_addstr(buf, term); - break; - case MUTT_WHOLE_MSG: - mutt_buffer_addstr(buf, "TEXT "); - imap_quote_string(term, sizeof(term), pat->p.str); - mutt_buffer_addstr(buf, term); - break; - case MUTT_SERVERSEARCH: + for (int i = 0; i < IMAP_CACHE_LEN; i++) + { + if (idata->cache[i].path) { - struct ImapData *idata = ctx->data; - if (!mutt_bit_isset(idata->capabilities, X_GM_EXT1)) - { - mutt_error(_("Server-side custom search not supported: %s"), pat->p.str); - return -1; - } + unlink(idata->cache[i].path); + FREE(&idata->cache[i].path); } - mutt_buffer_addstr(buf, "X-GM-RAW "); - imap_quote_string(term, sizeof(term), pat->p.str); - mutt_buffer_addstr(buf, term); - break; } + + mutt_bcache_close(&idata->bcache); + } + + /* free IMAP part of headers */ + for (int i = 0; i < ctx->msgcount; i++) + { + /* mailbox may not have fully loaded */ + if (ctx->hdrs[i] && ctx->hdrs[i]->data) + imap_free_header_data((struct ImapHeaderData **) &(ctx->hdrs[i]->data)); } return 0; } -int imap_search(struct Context *ctx, const struct Pattern *pat) +static int imap_open_new_message(struct Message *msg, struct Context *dest, struct Header *hdr) { - struct Buffer buf; - struct ImapData *idata = ctx->data; - for (int i = 0; i < ctx->msgcount; i++) - ctx->hdrs[i]->matched = false; - - if (!do_search(pat, 1)) - return 0; + char tmp[_POSIX_PATH_MAX]; - mutt_buffer_init(&buf); - mutt_buffer_addstr(&buf, "UID SEARCH "); - if (imap_compile_search(ctx, pat, &buf) < 0) - { - FREE(&buf.data); - return -1; - } - if (imap_exec(idata, buf.data, 0) < 0) + mutt_mktemp(tmp, sizeof(tmp)); + msg->fp = safe_fopen(tmp, "w"); + if (!msg->fp) { - FREE(&buf.data); + mutt_perror(tmp); return -1; } - - FREE(&buf.data); + msg->path = safe_strdup(tmp); return 0; } -int imap_subscribe(char *path, int subscribe) +static int imap_check_mailbox_reopen(struct Context *ctx, int *index_hint) +{ + int rc; + (void) index_hint; + + imap_allow_reopen(ctx); + rc = imap_check(ctx->data, 0); + imap_disallow_reopen(ctx); + + return rc; +} + +/* + * imap_sync_mailbox - Sync all the changes to the server + * @param ctx the current context + * @param expunge 0 or 1 - do expunge? + * @retval 0 on success + * @retval -1 on error + */ +int imap_sync_mailbox(struct Context *ctx, int expunge) { struct ImapData *idata = NULL; - char buf[LONG_STRING]; - char mbox[LONG_STRING]; - char errstr[STRING]; - struct Buffer err, token; - struct ImapMbox mx; + struct Context *appendctx = NULL; + struct Header *h = NULL; + struct Header **hdrs = NULL; + int oldsort; + int rc; - if (!mx_is_imap(path) || imap_parse_path(path, &mx) || !mx.mbox) + idata = ctx->data; + + if (idata->state < IMAP_SELECTED) { - mutt_error(_("Bad mailbox name")); + mutt_debug(2, "imap_sync_mailbox: no mailbox selected\n"); return -1; } - idata = imap_conn_find(&(mx.account), 0); - if (!idata) - goto fail; - imap_fix_path(idata, mx.mbox, buf, sizeof(buf)); - if (!*buf) - strfcpy(buf, "INBOX", sizeof(buf)); + /* This function is only called when the calling code expects the context + * to be changed. */ + imap_allow_reopen(ctx); + + rc = imap_check(idata, 0); + if (rc != 0) + return rc; + + /* if we are expunging anyway, we can do deleted messages very quickly... */ + if (expunge && mutt_bit_isset(ctx->rights, MUTT_ACL_DELETE)) + { + if ((rc = imap_exec_msgset(idata, "UID STORE", "+FLAGS.SILENT (\\Deleted)", + MUTT_DELETED, 1, 0)) < 0) + { + mutt_error(_("Expunge failed")); + mutt_sleep(1); + goto out; + } + + if (rc > 0) + { + /* mark these messages as unchanged so second pass ignores them. Done + * here so BOGUS UW-IMAP 4.7 SILENT FLAGS updates are ignored. */ + for (int i = 0; i < ctx->msgcount; i++) + if (ctx->hdrs[i]->deleted && ctx->hdrs[i]->changed) + ctx->hdrs[i]->active = false; + mutt_message(_("Marking %d messages deleted..."), rc); + } + } + +#ifdef USE_HCACHE + idata->hcache = imap_hcache_open(idata, NULL); +#endif - if (option(OPT_IMAP_CHECK_SUBSCRIBED)) + /* save messages with real (non-flag) changes */ + for (int i = 0; i < ctx->msgcount; i++) { - mutt_buffer_init(&token); - mutt_buffer_init(&err); - err.data = errstr; - err.dsize = sizeof(errstr); - snprintf(mbox, sizeof(mbox), "%smailboxes \"%s\"", subscribe ? "" : "un", path); - if (mutt_parse_rc_line(mbox, &token, &err)) - mutt_debug(1, "Error adding subscribed mailbox: %s\n", errstr); - FREE(&token.data); - } - - if (subscribe) - mutt_message(_("Subscribing to %s..."), buf); - else - mutt_message(_("Unsubscribing from %s..."), buf); - imap_munge_mbox_name(idata, mbox, sizeof(mbox), buf); + h = ctx->hdrs[i]; - snprintf(buf, sizeof(buf), "%sSUBSCRIBE %s", subscribe ? "" : "UN", mbox); + if (h->deleted) + { + imap_cache_del(idata, h); +#ifdef USE_HCACHE + imap_hcache_del(idata, HEADER_DATA(h)->uid); +#endif + } - if (imap_exec(idata, buf, 0) < 0) - goto fail; + if (h->active && h->changed) + { +#ifdef USE_HCACHE + imap_hcache_put(idata, h); +#endif + /* if the message has been rethreaded or attachments have been deleted + * we delete the message and reupload it. + * This works better if we're expunging, of course. */ + if ((h->env && (h->env->refs_changed || h->env->irt_changed)) || + h->attach_del || h->xlabel_changed) + { + mutt_message(_("Saving changed messages... [%d/%d]"), i + 1, ctx->msgcount); + if (!appendctx) + appendctx = mx_open_mailbox(ctx->path, MUTT_APPEND | MUTT_QUIET, NULL); + if (!appendctx) + mutt_debug( + 1, "imap_sync_mailbox: Error opening mailbox in append mode\n"); + else + _mutt_save_message(h, appendctx, 1, 0, 0); + h->xlabel_changed = false; + } + } + } - imap_unmunge_mbox_name(idata, mx.mbox); - if (subscribe) - mutt_message(_("Subscribed to %s"), mx.mbox); - else - mutt_message(_("Unsubscribed from %s"), mx.mbox); - FREE(&mx.mbox); - return 0; +#ifdef USE_HCACHE + imap_hcache_close(idata); +#endif -fail: - FREE(&mx.mbox); - return -1; -} + /* presort here to avoid doing 10 resorts in imap_exec_msgset */ + oldsort = Sort; + if (Sort != SORT_ORDER) + { + hdrs = ctx->hdrs; + ctx->hdrs = safe_malloc(ctx->msgcount * sizeof(struct Header *)); + memcpy(ctx->hdrs, hdrs, ctx->msgcount * sizeof(struct Header *)); -/** - * longest_common_prefix - Find longest prefix common to two strings - * @param dest Destination buffer - * @param src Source buffer - * @param start Starting offset into string - * @param dlen Destination buffer length - * @retval n Length of the common string - * - * Trim dest to the length of the longest prefix it shares with src. - */ -static size_t longest_common_prefix(char *dest, const char *src, size_t start, size_t dlen) -{ - size_t pos = start; + Sort = SORT_ORDER; + qsort(ctx->hdrs, ctx->msgcount, sizeof(struct Header *), mutt_get_sort_func(SORT_ORDER)); + } - while (pos < dlen && dest[pos] && dest[pos] == src[pos]) - pos++; - dest[pos] = '\0'; + rc = sync_helper(idata, MUTT_ACL_DELETE, MUTT_DELETED, "\\Deleted"); + if (rc >= 0) + rc |= sync_helper(idata, MUTT_ACL_WRITE, MUTT_FLAG, "\\Flagged"); + if (rc >= 0) + rc |= sync_helper(idata, MUTT_ACL_WRITE, MUTT_OLD, "Old"); + if (rc >= 0) + rc |= sync_helper(idata, MUTT_ACL_SEEN, MUTT_READ, "\\Seen"); + if (rc >= 0) + rc |= sync_helper(idata, MUTT_ACL_WRITE, MUTT_REPLIED, "\\Answered"); - return pos; -} + if (oldsort != Sort) + { + Sort = oldsort; + FREE(&ctx->hdrs); + ctx->hdrs = hdrs; + } -/** - * imap_complete_hosts - Look for completion matches for mailboxes - * - * look for IMAP URLs to complete from defined mailboxes. Could be extended to - * complete over open connections and account/folder hooks too. - */ -static int imap_complete_hosts(char *dest, size_t len) -{ - struct Buffy *mailbox = NULL; - struct Connection *conn = NULL; - int rc = -1; - size_t matchlen; + /* Flush the queued flags if any were changed in sync_helper. */ + if (rc > 0) + if (imap_exec(idata, NULL, 0) != IMAP_CMD_OK) + rc = -1; - matchlen = mutt_strlen(dest); - for (mailbox = Incoming; mailbox; mailbox = mailbox->next) + if (rc < 0) { - if (mutt_strncmp(dest, mailbox->path, matchlen) == 0) + if (ctx->closing) { - if (rc) + if (mutt_yesorno(_("Error saving flags. Close anyway?"), 0) == MUTT_YES) { - strfcpy(dest, mailbox->path, len); rc = 0; + idata->state = IMAP_AUTHENTICATED; + goto out; } - else - longest_common_prefix(dest, mailbox->path, matchlen, len); } + else + mutt_error(_("Error saving flags")); + rc = -1; + goto out; } - TAILQ_FOREACH(conn, mutt_socket_head(), entries) + /* Update local record of server state to reflect the synchronization just + * completed. imap_read_headers always overwrites hcache-origin flags, so + * there is no need to mutate the hcache after flag-only changes. */ + for (int i = 0; i < ctx->msgcount; i++) { - struct Url url; - char urlstr[LONG_STRING]; - - if (conn->account.type != MUTT_ACCT_TYPE_IMAP) - continue; + HEADER_DATA(ctx->hdrs[i])->deleted = ctx->hdrs[i]->deleted; + HEADER_DATA(ctx->hdrs[i])->flagged = ctx->hdrs[i]->flagged; + HEADER_DATA(ctx->hdrs[i])->old = ctx->hdrs[i]->old; + HEADER_DATA(ctx->hdrs[i])->read = ctx->hdrs[i]->read; + HEADER_DATA(ctx->hdrs[i])->replied = ctx->hdrs[i]->replied; + ctx->hdrs[i]->changed = false; + } + ctx->changed = false; - mutt_account_tourl(&conn->account, &url); - /* FIXME: how to handle multiple users on the same host? */ - url.user = NULL; - url.path = NULL; - url_tostring(&url, urlstr, sizeof(urlstr), 0); - if (mutt_strncmp(dest, urlstr, matchlen) == 0) + /* We must send an EXPUNGE command if we're not closing. */ + if (expunge && !(ctx->closing) && mutt_bit_isset(ctx->rights, MUTT_ACL_DELETE)) + { + mutt_message(_("Expunging messages from server...")); + /* Set expunge bit so we don't get spurious reopened messages */ + idata->reopen |= IMAP_EXPUNGE_EXPECTED; + if (imap_exec(idata, "EXPUNGE", 0) != 0) { - if (rc) - { - strfcpy(dest, urlstr, len); - rc = 0; - } - else - longest_common_prefix(dest, urlstr, matchlen, len); + idata->reopen &= ~IMAP_EXPUNGE_EXPECTED; + imap_error(_("imap_sync_mailbox: EXPUNGE failed"), idata->buf); + rc = -1; + goto out; } + idata->reopen &= ~IMAP_EXPUNGE_EXPECTED; + } + + if (expunge && ctx->closing) + { + imap_exec(idata, "CLOSE", IMAP_CMD_QUEUE); + idata->state = IMAP_AUTHENTICATED; } + if (option(OPT_MESSAGE_CACHE_CLEAN)) + imap_cache_clean(idata); + + rc = 0; + +out: + if (appendctx) + { + mx_fastclose_mailbox(appendctx); + FREE(&appendctx); + } return rc; } /** - * imap_complete - Try to complete an IMAP folder path + * imap_edit_message_tags - Prompt and validate new messages tags * - * Given a partial IMAP folder path, return a string which adds as much to the - * path as is unique + * @retval -1: error + * @retval 0: no valid user input + * @retval 1: buf set */ -int imap_complete(char *dest, size_t dlen, char *path) +static int imap_edit_message_tags(struct Context *ctx, const char *tags, char *buf, size_t buflen) { - struct ImapData *idata = NULL; - char list[LONG_STRING]; - char buf[LONG_STRING]; - struct ImapList listresp; - char completion[LONG_STRING]; - int clen; - size_t matchlen = 0; - int completions = 0; - struct ImapMbox mx; - int rc; - - if (imap_parse_path(path, &mx)) - { - strfcpy(dest, path, dlen); - return imap_complete_hosts(dest, dlen); - } + char *new = NULL; + char *checker = NULL; + struct ImapData *idata = (struct ImapData *) ctx->data; - /* don't open a new socket just for completion. Instead complete over - * known mailboxes/hooks/etc */ - idata = imap_conn_find(&(mx.account), MUTT_IMAP_CONN_NONEW); - if (!idata) - { - FREE(&mx.mbox); - strfcpy(dest, path, dlen); - return imap_complete_hosts(dest, dlen); + /* Check for \* flags capability */ + if (!imap_has_flag(&idata->flags, NULL)) + { + mutt_error(_("IMAP server doesn't support custom flags")); + return -1; } - /* reformat path for IMAP list, and append wildcard */ - /* don't use INBOX in place of "" */ - if (mx.mbox && mx.mbox[0]) - imap_fix_path(idata, mx.mbox, list, sizeof(list)); - else - list[0] = '\0'; + *buf = '\0'; + if (tags) + strncpy(buf, tags, buflen); - /* fire off command */ - snprintf(buf, sizeof(buf), "%s \"\" \"%s%%\"", - option(OPT_IMAP_LIST_SUBSCRIBED) ? "LSUB" : "LIST", list); + if (mutt_get_field("Tags: ", buf, buflen, 0) != 0) + return -1; - imap_cmd_start(idata, buf); + /* each keyword must be atom defined by rfc822 as: + * + * atom = 1* + * CHAR = ( 0.-127. ) + * specials = "(" / ")" / "<" / ">" / "@" + * / "," / ";" / ":" / "\" / <"> + * / "." / "[" / "]" + * SPACE = ( 32. ) + * CTLS = ( 0.-31., 127.) + * + * And must be separated by one space. + */ - /* and see what the results are */ - strfcpy(completion, NONULL(mx.mbox), sizeof(completion)); - idata->cmdtype = IMAP_CT_LIST; - idata->cmddata = &listresp; - do + new = buf; + checker = buf; + SKIPWS(checker); + while (*checker != '\0') { - listresp.name = NULL; - rc = imap_cmd_step(idata); - - if (rc == IMAP_CMD_CONTINUE && listresp.name) + if (*checker < 32 || *checker >= 127 || // We allow space because it's the separator + *checker == 40 || // ( + *checker == 41 || // ) + *checker == 60 || // < + *checker == 62 || // > + *checker == 64 || // @ + *checker == 44 || // , + *checker == 59 || // ; + *checker == 58 || // : + *checker == 92 || // backslash + *checker == 34 || // " + *checker == 46 || // . + *checker == 91 || // [ + *checker == 93) // ] { - /* if the folder isn't selectable, append delimiter to force browse - * to enter it on second tab. */ - if (listresp.noselect) - { - clen = strlen(listresp.name); - listresp.name[clen++] = listresp.delim; - listresp.name[clen] = '\0'; - } - /* copy in first word */ - if (!completions) - { - strfcpy(completion, listresp.name, sizeof(completion)); - matchlen = strlen(completion); - completions++; - continue; - } - - matchlen = longest_common_prefix(completion, listresp.name, 0, matchlen); - completions++; + mutt_error(_("Invalid IMAP flags")); + mutt_sleep(2); + return 0; } - } while (rc == IMAP_CMD_CONTINUE); - idata->cmddata = NULL; - if (completions) - { - /* reformat output */ - imap_qualify_path(dest, dlen, &mx, completion); - mutt_pretty_mailbox(dest, dlen); + /* Skip duplicate space */ + while (*checker == ' ' && *(checker + 1) == ' ') + checker++; - FREE(&mx.mbox); - return 0; + /* copy char to new and go the next one */ + *new ++ = *checker++; } + *new = '\0'; + new = buf; /* rewind */ + mutt_remove_trailing_ws(new); - return -1; + if (mutt_strcmp(tags, buf) == 0) + return 0; + return 1; } /** - * imap_fast_trash - Use server COPY command to copy deleted messages to trash - * @retval -1 error - * @retval 0 success - * @retval 1 non-fatal error - try fetch/append + * imap_commit_message_tags - Add/Change/Remove flags from headers + * @param ctx Context + * @param h Header + * @param tags List of tags + * @retval 0 Success + * @retval -1 Error + * + * This method update the server flags on the server by + * removing the last know custom flags of a header + * and adds the local flags + * + * If everything success we push the local flags to the + * last know custom flags (flags_remote). + * + * Also this method check that each flags is support by the server + * first and remove unsupported one. */ -int imap_fast_trash(struct Context *ctx, char *dest) +static int imap_commit_message_tags(struct Context *ctx, struct Header *h, char *tags) { struct ImapData *idata = NULL; - char mbox[LONG_STRING]; - char mmbox[LONG_STRING]; - char prompt[LONG_STRING]; - int rc; - struct ImapMbox mx; - bool triedcreate = false; - struct Buffer *sync_cmd = NULL; - int err_continue = MUTT_NO; + struct Buffer *cmd = NULL; + char uid[11]; idata = ctx->data; - if (imap_parse_path(dest, &mx)) - { - mutt_debug(1, "imap_fast_trash: bad destination %s\n", dest); - return -1; - } + if (*tags == '\0') + tags = NULL; - /* check that the save-to folder is in the same account */ - if (!mutt_account_match(&(idata->conn->account), &(mx.account))) - { - mutt_debug(3, "imap_fast_trash: %s not same server as %s\n", dest, ctx->path); - return 1; - } + if (!mutt_bit_isset(idata->ctx->rights, MUTT_ACL_WRITE)) + return 0; - imap_fix_path(idata, mx.mbox, mbox, sizeof(mbox)); - if (!*mbox) - strfcpy(mbox, "INBOX", sizeof(mbox)); - imap_munge_mbox_name(idata, mmbox, sizeof(mmbox), mbox); + snprintf(uid, sizeof(uid), "%u", HEADER_DATA(h)->uid); - sync_cmd = mutt_buffer_new(); - for (int i = 0; i < ctx->msgcount; i++) + /* Remove old custom flags */ + if (HEADER_DATA(h)->flags_remote) { - if (ctx->hdrs[i]->active && ctx->hdrs[i]->changed && - ctx->hdrs[i]->deleted && !ctx->hdrs[i]->purge) + cmd = mutt_buffer_new(); + if (!cmd) { - rc = imap_sync_message_for_copy(idata, ctx->hdrs[i], sync_cmd, &err_continue); - if (rc < 0) - { - mutt_debug(1, "imap_fast_trash: could not sync\n"); - goto out; - } + mutt_debug(1, "imap_commit_message_tags: unable to allocate buffer\n"); + return -1; } - } + cmd->dptr = cmd->data; + mutt_buffer_addstr(cmd, "UID STORE "); + mutt_buffer_addstr(cmd, uid); + mutt_buffer_addstr(cmd, " -FLAGS.SILENT ("); + mutt_buffer_addstr(cmd, HEADER_DATA(h)->flags_remote); + mutt_buffer_addstr(cmd, ")"); - /* loop in case of TRYCREATE */ - do - { - rc = imap_exec_msgset(idata, "UID COPY", mmbox, MUTT_TRASH, 0, 0); - if (!rc) + /* Should we return here, or we are fine and we could + * continue to add new flags * + */ + if (imap_exec(idata, cmd->data, 0) != 0) { - mutt_debug(1, "imap_fast_trash: No messages to trash\n"); - rc = -1; - goto out; + mutt_buffer_free(&cmd); + return -1; } - else if (rc < 0) + + mutt_buffer_free(&cmd); + } + + /* Add new custom flags */ + if (tags) + { + cmd = mutt_buffer_new(); + if (!cmd) { - mutt_debug(1, "could not queue copy\n"); - goto out; + mutt_debug(1, "imap_commit_message_tags: fail to remove old flags\n"); + return -1; } - else - mutt_message(_("Copying %d messages to %s..."), rc, mbox); + cmd->dptr = cmd->data; + mutt_buffer_addstr(cmd, "UID STORE "); + mutt_buffer_addstr(cmd, uid); + mutt_buffer_addstr(cmd, " +FLAGS.SILENT ("); + mutt_buffer_addstr(cmd, tags); + mutt_buffer_addstr(cmd, ")"); - /* let's get it on */ - rc = imap_exec(idata, NULL, IMAP_CMD_FAIL_OK); - if (rc == -2) + if (imap_exec(idata, cmd->data, 0) != 0) { - if (triedcreate) - { - mutt_debug(1, "Already tried to create mailbox %s\n", mbox); - break; - } - /* bail out if command failed for reasons other than nonexistent target */ - if (mutt_strncasecmp(imap_get_qualifier(idata->buf), "[TRYCREATE]", 11) != 0) - break; - mutt_debug(3, "imap_fast_trash: server suggests TRYCREATE\n"); - snprintf(prompt, sizeof(prompt), _("Create %s?"), mbox); - if (option(OPT_CONFIRMCREATE) && mutt_yesorno(prompt, 1) != MUTT_YES) - { - mutt_clear_error(); - goto out; - } - if (imap_create_mailbox(idata, mbox) < 0) - break; - triedcreate = true; + mutt_debug(1, "imap_commit_message_tags: fail to add new flags\n"); + mutt_buffer_free(&cmd); + return -1; } - } while (rc == -2); - if (rc != 0) - { - imap_error("imap_fast_trash", idata->buf); - goto out; + mutt_buffer_free(&cmd); } - rc = 0; - -out: - mutt_buffer_free(&sync_cmd); - FREE(&mx.mbox); - - return rc < 0 ? -1 : rc; + /* We are good sync them */ + mutt_debug(1, "NEW TAGS: %d\n", tags); + driver_tags_replace(&h->tags, tags); + FREE(&HEADER_DATA(h)->flags_remote); + HEADER_DATA(h)->flags_remote = driver_tags_get_with_hidden(&h->tags); + return 0; } struct MxOps mx_imap_ops = { diff --git a/imap/imap.h b/imap/imap.h index f9c660fa4..e5d890b8f 100644 --- a/imap/imap.h +++ b/imap/imap.h @@ -56,9 +56,6 @@ int imap_subscribe(char *path, int subscribe); int imap_complete(char *dest, size_t dlen, char *path); int imap_fast_trash(struct Context *ctx, char *dest); -void imap_allow_reopen(struct Context *ctx); -void imap_disallow_reopen(struct Context *ctx); - extern struct MxOps mx_imap_ops; /* browse.c */ @@ -67,7 +64,6 @@ int imap_mailbox_create(const char *folder); int imap_mailbox_rename(const char *mailbox); /* message.c */ -int imap_append_message(struct Context *ctx, struct Message *msg); int imap_copy_messages(struct Context *ctx, struct Header *h, char *dest, int delete); /* socket.c */ @@ -81,8 +77,6 @@ void imap_pretty_mailbox(char *path); int imap_wait_keepalive(pid_t pid); void imap_keepalive(void); -int imap_account_match(const struct Account *a1, const struct Account *a2); -void imap_get_parent(char *output, const char *mbox, size_t olen, char delim); void imap_get_parent_path(char *output, const char *path, size_t olen); void imap_clean_path(char *path, size_t plen); diff --git a/imap/imap_private.h b/imap/imap_private.h index 7a01ba43d..4cdaa3f4e 100644 --- a/imap/imap_private.h +++ b/imap/imap_private.h @@ -300,6 +300,7 @@ int imap_read_headers(struct ImapData *idata, unsigned int msn_begin, unsigned i char *imap_set_flags(struct ImapData *idata, struct Header *h, char *s, int *server_changes); int imap_cache_del(struct ImapData *idata, struct Header *h); int imap_cache_clean(struct ImapData *idata); +int imap_append_message(struct Context *ctx, struct Message *msg); int imap_fetch_message(struct Context *ctx, struct Message *msg, int msgno); int imap_close_message(struct Context *ctx, struct Message *msg); @@ -329,10 +330,14 @@ void imap_quote_string(char *dest, size_t slen, const char *src); void imap_unquote_string(char *s); void imap_munge_mbox_name(struct ImapData *idata, char *dest, size_t dlen, const char *src); void imap_unmunge_mbox_name(struct ImapData *idata, char *s); +int imap_account_match(const struct Account *a1, const struct Account *a2); +void imap_get_parent(char *output, const char *mbox, size_t olen, char delim); /* utf7.c */ void imap_utf_encode(struct ImapData *idata, char **s); void imap_utf_decode(struct ImapData *idata, char **s); +void imap_allow_reopen(struct Context *ctx); +void imap_disallow_reopen(struct Context *ctx); #ifdef USE_HCACHE #define imap_hcache_keylen mutt_strlen diff --git a/imap/message.c b/imap/message.c index 2c6530be3..486156bdc 100644 --- a/imap/message.c +++ b/imap/message.c @@ -478,6 +478,32 @@ static void imap_generate_seqset(struct Buffer *b, struct ImapData *idata, } } +/* Sets server_changes to 1 if a change to a flag is made, or in the + * case of local_changes, if a change to a flag _would_ have been + * made. */ +static void imap_set_changed_flag(struct Context *ctx, struct Header *h, + int local_changes, int *server_changes, int flag_name, + int old_hd_flag, int new_hd_flag, int h_flag) +{ + /* If there are local_changes, we only want to note if the server + * flags have changed, so we can set a reopen flag in + * cmd_parse_fetch(). We don't want to count a local modification + * to the header flag as a "change". + */ + if ((old_hd_flag != new_hd_flag) || (!local_changes)) + { + if (new_hd_flag != h_flag) + { + if (server_changes) + *server_changes = 1; + + /* Local changes have priority */ + if (!local_changes) + mutt_set_flag(ctx, h, flag_name, new_hd_flag); + } + } +} + /** * imap_read_headers - Read headers from the server * @@ -1435,32 +1461,6 @@ void imap_free_header_data(struct ImapHeaderData **data) } } -/* Sets server_changes to 1 if a change to a flag is made, or in the - * case of local_changes, if a change to a flag _would_ have been - * made. */ -static void imap_set_changed_flag(struct Context *ctx, struct Header *h, - int local_changes, int *server_changes, int flag_name, - int old_hd_flag, int new_hd_flag, int h_flag) -{ - /* If there are local_changes, we only want to note if the server - * flags have changed, so we can set a reopen flag in - * cmd_parse_fetch(). We don't want to count a local modification - * to the header flag as a "change". - */ - if ((old_hd_flag != new_hd_flag) || (!local_changes)) - { - if (new_hd_flag != h_flag) - { - if (server_changes) - *server_changes = 1; - - /* Local changes have priority */ - if (!local_changes) - mutt_set_flag(ctx, h, flag_name, new_hd_flag); - } - } -} - /** * imap_set_flags - fill the message header according to the server flags *