]> granicus.if.org Git - neomutt/commitdiff
reorg nntp functions
authorRichard Russon <rich@flatcap.org>
Tue, 11 Sep 2018 16:49:47 +0000 (17:49 +0100)
committerRichard Russon <rich@flatcap.org>
Thu, 13 Sep 2018 01:03:21 +0000 (02:03 +0100)
nntp/nntp.c

index f81e55f6a14f562a121c01473e59b48699c45731..f725b0509149c9ec0ebba4ccbf557f8cbec863fd 100644 (file)
@@ -75,6 +75,42 @@ bool ShowNewNews; ///< Config: (nntp) Check for new newsgroups when entering the
 
 struct NntpServer *CurrentNewsSrv;
 
+const char *OverviewFmt = "Subject:\0"
+                          "From:\0"
+                          "Date:\0"
+                          "Message-ID:\0"
+                          "References:\0"
+                          "Content-Length:\0"
+                          "Lines:\0"
+                          "\0";
+
+/**
+ * struct FetchCtx - Keep track when getting data from a server
+ */
+struct FetchCtx
+{
+  struct Context *ctx;
+  anum_t first;
+  anum_t last;
+  int restore;
+  unsigned char *messages;
+  struct Progress progress;
+#ifdef USE_HCACHE
+  header_cache_t *hc;
+#endif
+};
+
+/**
+ * struct ChildCtx - Keep track of the children of an article
+ */
+struct ChildCtx
+{
+  struct Mailbox *mailbox;
+  unsigned int num;
+  unsigned int max;
+  anum_t *child;
+};
+
 /**
  * nntp_connect_error - Signal a failed connection
  * @param nserv NNTP server
@@ -190,15 +226,6 @@ static int nntp_capabilities(struct NntpServer *nserv)
   return -1;
 }
 
-const char *OverviewFmt = "Subject:\0"
-                          "From:\0"
-                          "Date:\0"
-                          "Message-ID:\0"
-                          "References:\0"
-                          "Content-Length:\0"
-                          "Lines:\0"
-                          "\0";
-
 /**
  * nntp_attempt_features - Detect supported commands
  * @param nserv NNTP server
@@ -629,166 +656,6 @@ static int nntp_auth(struct NntpServer *nserv)
   return -1;
 }
 
-/**
- * nntp_open_connection - Connect to server, authenticate and get capabilities
- * @param nserv NNTP server
- * @retval  0 Success
- * @retval -1 Failure
- */
-int nntp_open_connection(struct NntpServer *nserv)
-{
-  struct Connection *conn = nserv->conn;
-  char buf[STRING];
-  int cap;
-  bool posting = false, auth = true;
-
-  if (nserv->status == NNTP_OK)
-    return 0;
-  if (nserv->status == NNTP_BYE)
-    return -1;
-  nserv->status = NNTP_NONE;
-
-  if (mutt_socket_open(conn) < 0)
-    return -1;
-
-  if (mutt_socket_readln(buf, sizeof(buf), conn) < 0)
-    return nntp_connect_error(nserv);
-
-  if (mutt_str_strncmp("200", buf, 3) == 0)
-    posting = true;
-  else if (mutt_str_strncmp("201", buf, 3) != 0)
-  {
-    mutt_socket_close(conn);
-    mutt_str_remove_trailing_ws(buf);
-    mutt_error("%s", buf);
-    return -1;
-  }
-
-  /* get initial capabilities */
-  cap = nntp_capabilities(nserv);
-  if (cap < 0)
-    return -1;
-
-  /* tell news server to switch to mode reader if it isn't so */
-  if (cap > 0)
-  {
-    if (mutt_socket_send(conn, "MODE READER\r\n") < 0 ||
-        mutt_socket_readln(buf, sizeof(buf), conn) < 0)
-    {
-      return nntp_connect_error(nserv);
-    }
-
-    if (mutt_str_strncmp("200", buf, 3) == 0)
-      posting = true;
-    else if (mutt_str_strncmp("201", buf, 3) == 0)
-      posting = false;
-    /* error if has capabilities, ignore result if no capabilities */
-    else if (nserv->hasCAPABILITIES)
-    {
-      mutt_socket_close(conn);
-      mutt_error(_("Could not switch to reader mode"));
-      return -1;
-    }
-
-    /* recheck capabilities after MODE READER */
-    if (nserv->hasCAPABILITIES)
-    {
-      cap = nntp_capabilities(nserv);
-      if (cap < 0)
-        return -1;
-    }
-  }
-
-  mutt_message(_("Connected to %s. %s"), conn->account.host,
-               posting ? _("Posting is ok") : _("Posting is NOT ok"));
-  mutt_sleep(1);
-
-#ifdef USE_SSL
-  /* Attempt STARTTLS if available and desired. */
-  if (nserv->use_tls != 1 && (nserv->hasSTARTTLS || SslForceTls))
-  {
-    if (nserv->use_tls == 0)
-    {
-      nserv->use_tls =
-          SslForceTls || query_quadoption(SslStarttls,
-                                          _("Secure connection with TLS?")) == MUTT_YES ?
-              2 :
-              1;
-    }
-    if (nserv->use_tls == 2)
-    {
-      if (mutt_socket_send(conn, "STARTTLS\r\n") < 0 ||
-          mutt_socket_readln(buf, sizeof(buf), conn) < 0)
-      {
-        return nntp_connect_error(nserv);
-      }
-      if (mutt_str_strncmp("382", buf, 3) != 0)
-      {
-        nserv->use_tls = 0;
-        mutt_error("STARTTLS: %s", buf);
-      }
-      else if (mutt_ssl_starttls(conn))
-      {
-        nserv->use_tls = 0;
-        nserv->status = NNTP_NONE;
-        mutt_socket_close(nserv->conn);
-        mutt_error(_("Could not negotiate TLS connection"));
-        return -1;
-      }
-      else
-      {
-        /* recheck capabilities after STARTTLS */
-        cap = nntp_capabilities(nserv);
-        if (cap < 0)
-          return -1;
-      }
-    }
-  }
-#endif
-
-  /* authentication required? */
-  if (conn->account.flags & MUTT_ACCT_USER)
-  {
-    if (!conn->account.user[0])
-      auth = false;
-  }
-  else
-  {
-    if (mutt_socket_send(conn, "STAT\r\n") < 0 ||
-        mutt_socket_readln(buf, sizeof(buf), conn) < 0)
-    {
-      return nntp_connect_error(nserv);
-    }
-    if (mutt_str_strncmp("480", buf, 3) != 0)
-      auth = false;
-  }
-
-  /* authenticate */
-  if (auth && nntp_auth(nserv) < 0)
-    return -1;
-
-  /* get final capabilities after authentication */
-  if (nserv->hasCAPABILITIES && (auth || cap > 0))
-  {
-    cap = nntp_capabilities(nserv);
-    if (cap < 0)
-      return -1;
-    if (cap > 0)
-    {
-      mutt_socket_close(conn);
-      mutt_error(_("Could not switch to reader mode"));
-      return -1;
-    }
-  }
-
-  /* attempt features */
-  if (nntp_attempt_features(nserv) < 0)
-    return -1;
-
-  nserv->status = NNTP_OK;
-  return 0;
-}
-
 /**
  * nntp_query - Send data from buffer and receive answer to same buffer
  * @param nntp_data NNTP server data
@@ -1075,22 +942,6 @@ static int fetch_tempfile(char *line, void *data)
   return 0;
 }
 
-/**
- * struct FetchCtx - Keep track when getting data from a server
- */
-struct FetchCtx
-{
-  struct Context *ctx;
-  anum_t first;
-  anum_t last;
-  int restore;
-  unsigned char *messages;
-  struct Progress progress;
-#ifdef USE_HCACHE
-  header_cache_t *hc;
-#endif
-};
-
 /**
  * fetch_numbers - Parse article number
  * @param line Article number
@@ -1505,274 +1356,495 @@ static int nntp_fetch_headers(struct Context *ctx, void *hc, anum_t first,
 }
 
 /**
- * nntp_mbox_open - Implements MxOps::mbox_open()
+ * nntp_group_poll - Check newsgroup for new articles
+ * @param nntp_data   NNTP server data
+ * @param update_stat Update the stats?
+ * @retval  1 New articles found
+ * @retval  0 No change
+ * @retval -1 Lost connection
  */
-static int nntp_mbox_open(struct Context *ctx)
+static int nntp_group_poll(struct NntpData *nntp_data, int update_stat)
 {
-  struct NntpServer *nserv = NULL;
-  struct NntpData *nntp_data = NULL;
-  char buf[HUGE_STRING];
-  char server[LONG_STRING];
-  char *group = NULL;
-  int rc;
-  void *hc = NULL;
-  anum_t first, last, count = 0;
-  struct Url url;
+  char buf[LONG_STRING] = "";
+  anum_t count, first, last;
 
-  mutt_str_strfcpy(buf, ctx->mailbox->path, sizeof(buf));
-  if (url_parse(&url, buf) < 0 || !url.host || !url.path ||
-      !(url.scheme == U_NNTP || url.scheme == U_NNTPS))
-  {
-    url_free(&url);
-    mutt_error(_("%s is an invalid newsgroup specification"), ctx->mailbox->path);
+  /* use GROUP command to poll newsgroup */
+  if (nntp_query(nntp_data, buf, sizeof(buf)) < 0)
     return -1;
-  }
+  if (sscanf(buf, "211 " ANUM " " ANUM " " ANUM, &count, &first, &last) != 3)
+    return 0;
+  if (first == nntp_data->first_message && last == nntp_data->last_message)
+    return 0;
 
-  group = url.path;
-  url.path = strchr(url.path, '\0');
-  url_tostring(&url, server, sizeof(server), 0);
-  nserv = nntp_select_server(ctx->mailbox, server, true);
-  url_free(&url);
-  if (!nserv)
-    return -1;
-  CurrentNewsSrv = nserv;
-
-  /* find news group data structure */
-  nntp_data = mutt_hash_find(nserv->groups_hash, group);
-  if (!nntp_data)
+  /* articles have been renumbered */
+  if (last < nntp_data->last_message)
   {
-    nntp_newsrc_close(nserv);
-    mutt_error(_("Newsgroup %s not found on the server"), group);
-    return -1;
+    nntp_data->last_cached = 0;
+    if (nntp_data->newsrc_len)
+    {
+      mutt_mem_realloc(&nntp_data->newsrc_ent, sizeof(struct NewsrcEntry));
+      nntp_data->newsrc_len = 1;
+      nntp_data->newsrc_ent[0].first = 1;
+      nntp_data->newsrc_ent[0].last = 0;
+    }
   }
+  nntp_data->first_message = first;
+  nntp_data->last_message = last;
+  if (!update_stat)
+    return 1;
 
-  mutt_bit_unset(ctx->mailbox->rights, MUTT_ACL_INSERT);
-  if (!nntp_data->newsrc_ent && !nntp_data->subscribed && !SaveUnsubscribed)
-    ctx->mailbox->readonly = true;
+  /* update counters */
+  else if (!last || (!nntp_data->newsrc_ent && !nntp_data->last_cached))
+    nntp_data->unread = count;
+  else
+    nntp_group_unread_stat(nntp_data);
+  return 1;
+}
 
-  /* select newsgroup */
-  mutt_message(_("Selecting %s..."), group);
-  buf[0] = '\0';
-  if (nntp_query(nntp_data, buf, sizeof(buf)) < 0)
+/**
+ * check_mailbox - Check current newsgroup for new articles
+ * @param ctx Mailbox
+ * @retval #MUTT_REOPENED Articles have been renumbered or removed from server
+ * @retval #MUTT_NEW_MAIL New articles found
+ * @retval  0             No change
+ * @retval -1             Lost connection
+ *
+ * Leave newsrc locked
+ */
+static int check_mailbox(struct Context *ctx)
+{
+  struct NntpData *nntp_data = ctx->mailbox->data;
+  struct NntpServer *nserv = nntp_data->nserv;
+  time_t now = time(NULL);
+  int rc, ret = 0;
+  void *hc = NULL;
+
+  if (nserv->check_time + NntpPoll > now)
+    return 0;
+
+  mutt_message(_("Checking for new messages..."));
+  if (nntp_newsrc_parse(nserv) < 0)
+    return -1;
+
+  nserv->check_time = now;
+  rc = nntp_group_poll(nntp_data, 0);
+  if (rc < 0)
   {
     nntp_newsrc_close(nserv);
     return -1;
   }
+  if (rc)
+    nntp_active_save_cache(nserv);
 
-  /* newsgroup not found, remove it */
-  if (mutt_str_strncmp("411", buf, 3) == 0)
+  /* articles have been renumbered, remove all headers */
+  if (nntp_data->last_message < nntp_data->last_loaded)
   {
-    mutt_error(_("Newsgroup %s has been removed from the server"), nntp_data->group);
-    if (!nntp_data->deleted)
-    {
-      nntp_data->deleted = true;
-      nntp_active_save_cache(nserv);
-    }
-    if (nntp_data->newsrc_ent && !nntp_data->subscribed && !SaveUnsubscribed)
+    for (int i = 0; i < ctx->mailbox->msg_count; i++)
+      mutt_header_free(&ctx->mailbox->hdrs[i]);
+    ctx->mailbox->msg_count = 0;
+    ctx->tagged = 0;
+
+    if (nntp_data->last_message < nntp_data->last_loaded)
     {
-      FREE(&nntp_data->newsrc_ent);
-      nntp_data->newsrc_len = 0;
-      nntp_delete_group_cache(nntp_data);
-      nntp_newsrc_update(nserv);
+      nntp_data->last_loaded = nntp_data->first_message - 1;
+      if (NntpContext && nntp_data->last_message - nntp_data->last_loaded > NntpContext)
+        nntp_data->last_loaded = nntp_data->last_message - NntpContext;
     }
+    ret = MUTT_REOPENED;
   }
 
-  /* parse newsgroup info */
-  else
+  /* .newsrc has been externally modified */
+  if (nserv->newsrc_modified)
   {
-    if (sscanf(buf, "211 " ANUM " " ANUM " " ANUM, &count, &first, &last) != 3)
+#ifdef USE_HCACHE
+    unsigned char *messages = NULL;
+    char buf[16];
+    void *hdata = NULL;
+    struct Header *hdr = NULL;
+    anum_t first = nntp_data->first_message;
+
+    if (NntpContext && nntp_data->last_message - first + 1 > NntpContext)
+      first = nntp_data->last_message - NntpContext + 1;
+    messages = mutt_mem_calloc(nntp_data->last_loaded - first + 1, sizeof(unsigned char));
+    hc = nntp_hcache_open(nntp_data);
+    nntp_hcache_update(nntp_data, hc);
+#endif
+
+    /* update flags according to .newsrc */
+    int j = 0;
+    anum_t anum;
+    for (int i = 0; i < ctx->mailbox->msg_count; i++)
     {
-      nntp_newsrc_close(nserv);
-      mutt_error("GROUP: %s", buf);
-      return -1;
+      bool flagged = false;
+      anum = NHDR(ctx->mailbox->hdrs[i])->article_num;
+
+#ifdef USE_HCACHE
+      /* check hcache for flagged and deleted flags */
+      if (hc)
+      {
+        if (anum >= first && anum <= nntp_data->last_loaded)
+          messages[anum - first] = 1;
+
+        snprintf(buf, sizeof(buf), "%u", anum);
+        hdata = mutt_hcache_fetch(hc, buf, strlen(buf));
+        if (hdata)
+        {
+          bool deleted;
+
+          mutt_debug(2, "#1 mutt_hcache_fetch %s\n", buf);
+          hdr = mutt_hcache_restore(hdata);
+          mutt_hcache_free(hc, &hdata);
+          hdr->data = NULL;
+          deleted = hdr->deleted;
+          flagged = hdr->flagged;
+          mutt_header_free(&hdr);
+
+          /* header marked as deleted, removing from context */
+          if (deleted)
+          {
+            mutt_set_flag(ctx, ctx->mailbox->hdrs[i], MUTT_TAG, 0);
+            mutt_header_free(&ctx->mailbox->hdrs[i]);
+            continue;
+          }
+        }
+      }
+#endif
+
+      if (!ctx->mailbox->hdrs[i]->changed)
+      {
+        ctx->mailbox->hdrs[i]->flagged = flagged;
+        ctx->mailbox->hdrs[i]->read = false;
+        ctx->mailbox->hdrs[i]->old = false;
+        nntp_article_status(ctx->mailbox, ctx->mailbox->hdrs[i], NULL, anum);
+        if (!ctx->mailbox->hdrs[i]->read)
+          nntp_parse_xref(ctx->mailbox, ctx->mailbox->hdrs[i]);
+      }
+      ctx->mailbox->hdrs[j++] = ctx->mailbox->hdrs[i];
     }
-    nntp_data->first_message = first;
-    nntp_data->last_message = last;
-    nntp_data->deleted = false;
 
-    /* get description if empty */
-    if (NntpLoadDescription && !nntp_data->desc)
+#ifdef USE_HCACHE
+    ctx->mailbox->msg_count = j;
+
+    /* restore headers without "deleted" flag */
+    for (anum = first; anum <= nntp_data->last_loaded; anum++)
     {
-      if (get_description(nntp_data, NULL, NULL) < 0)
+      if (messages[anum - first])
+        continue;
+
+      snprintf(buf, sizeof(buf), "%u", anum);
+      hdata = mutt_hcache_fetch(hc, buf, strlen(buf));
+      if (hdata)
       {
-        nntp_newsrc_close(nserv);
-        return -1;
+        mutt_debug(2, "#2 mutt_hcache_fetch %s\n", buf);
+        if (ctx->mailbox->msg_count >= ctx->mailbox->hdrmax)
+          mx_alloc_memory(ctx->mailbox);
+
+        hdr = mutt_hcache_restore(hdata);
+        ctx->mailbox->hdrs[ctx->mailbox->msg_count] = hdr;
+        mutt_hcache_free(hc, &hdata);
+        hdr->data = NULL;
+        if (hdr->deleted)
+        {
+          mutt_header_free(&hdr);
+          if (nntp_data->bcache)
+          {
+            mutt_debug(2, "mutt_bcache_del %s\n", buf);
+            mutt_bcache_del(nntp_data->bcache, buf);
+          }
+          continue;
+        }
+
+        ctx->mailbox->msg_count++;
+        hdr->read = false;
+        hdr->old = false;
+        hdr->data = mutt_mem_calloc(1, sizeof(struct NntpHeaderData));
+        NHDR(hdr)->article_num = anum;
+        nntp_article_status(ctx->mailbox, hdr, NULL, anum);
+        if (!hdr->read)
+          nntp_parse_xref(ctx->mailbox, hdr);
       }
-      if (nntp_data->desc)
-        nntp_active_save_cache(nserv);
     }
+    FREE(&messages);
+#endif
+
+    nserv->newsrc_modified = false;
+    ret = MUTT_REOPENED;
   }
 
-  time(&nserv->check_time);
-  ctx->mailbox->data = nntp_data;
-  if (!nntp_data->bcache && (nntp_data->newsrc_ent || nntp_data->subscribed || SaveUnsubscribed))
-    nntp_data->bcache = mutt_bcache_open(&nserv->conn->account, nntp_data->group);
+  /* some headers were removed, context must be updated */
+  if (ret == MUTT_REOPENED)
+  {
+    if (ctx->mailbox->subj_hash)
+      mutt_hash_destroy(&ctx->mailbox->subj_hash);
+    if (ctx->mailbox->id_hash)
+      mutt_hash_destroy(&ctx->mailbox->id_hash);
+    mutt_clear_threads(ctx);
 
-  /* strip off extra articles if adding context is greater than $nntp_context */
-  first = nntp_data->first_message;
-  if (NntpContext && nntp_data->last_message - first + 1 > NntpContext)
-    first = nntp_data->last_message - NntpContext + 1;
-  nntp_data->last_loaded = first ? first - 1 : 0;
-  count = nntp_data->first_message;
-  nntp_data->first_message = first;
-  nntp_bcache_update(nntp_data);
-  nntp_data->first_message = count;
+    ctx->mailbox->vcount = 0;
+    ctx->deleted = 0;
+    ctx->new = 0;
+    ctx->mailbox->msg_unread = 0;
+    ctx->mailbox->msg_flagged = 0;
+    ctx->mailbox->changed = false;
+    ctx->mailbox->id_hash = NULL;
+    ctx->mailbox->subj_hash = NULL;
+    mx_update_context(ctx, ctx->mailbox->msg_count);
+  }
+
+  /* fetch headers of new articles */
+  if (nntp_data->last_message > nntp_data->last_loaded)
+  {
+    int oldmsgcount = ctx->mailbox->msg_count;
+    bool quiet = ctx->mailbox->quiet;
+    ctx->mailbox->quiet = true;
 #ifdef USE_HCACHE
-  hc = nntp_hcache_open(nntp_data);
-  nntp_hcache_update(nntp_data, hc);
+    if (!hc)
+    {
+      hc = nntp_hcache_open(nntp_data);
+      nntp_hcache_update(nntp_data, hc);
+    }
 #endif
-  if (!hc)
-  {
-    mutt_bit_unset(ctx->mailbox->rights, MUTT_ACL_WRITE);
-    mutt_bit_unset(ctx->mailbox->rights, MUTT_ACL_DELETE);
+    rc = nntp_fetch_headers(ctx, hc, nntp_data->last_loaded + 1, nntp_data->last_message, 0);
+    ctx->mailbox->quiet = quiet;
+    if (rc >= 0)
+      nntp_data->last_loaded = nntp_data->last_message;
+    if (ret == 0 && ctx->mailbox->msg_count > oldmsgcount)
+      ret = MUTT_NEW_MAIL;
   }
-  nntp_newsrc_close(nserv);
-  rc = nntp_fetch_headers(ctx, hc, first, nntp_data->last_message, 0);
+
 #ifdef USE_HCACHE
   mutt_hcache_close(hc);
 #endif
-  if (rc < 0)
-    return -1;
-  nntp_data->last_loaded = nntp_data->last_message;
-  nserv->newsrc_modified = false;
-  return 0;
+  if (ret)
+    nntp_newsrc_close(nserv);
+  mutt_clear_error();
+  return ret;
 }
 
 /**
- * nntp_msg_open - Implements MxOps::msg_open()
+ * nntp_date - Get date and time from server
+ * @param nserv NNTP server
+ * @param now   Server time
+ * @retval  0 Success
+ * @retval -1 Failure
  */
-static int nntp_msg_open(struct Context *ctx, struct Message *msg, int msgno)
+static int nntp_date(struct NntpServer *nserv, time_t *now)
 {
-  struct NntpData *nntp_data = ctx->mailbox->data;
-  struct Header *hdr = ctx->mailbox->hdrs[msgno];
-  char article[16];
-
-  /* try to get article from cache */
-  struct NntpAcache *acache = &nntp_data->acache[hdr->index % NNTP_ACACHE_LEN];
-  if (acache->path)
+  if (nserv->hasDATE)
   {
-    if (acache->index == hdr->index)
+    struct NntpData nntp_data;
+    char buf[LONG_STRING];
+    struct tm tm;
+    memset(&tm, 0, sizeof(tm));
+
+    nntp_data.nserv = nserv;
+    nntp_data.group = NULL;
+    mutt_str_strfcpy(buf, "DATE\r\n", sizeof(buf));
+    if (nntp_query(&nntp_data, buf, sizeof(buf)) < 0)
+      return -1;
+
+    if (sscanf(buf, "111 %4d%2d%2d%2d%2d%2d%*s", &tm.tm_year, &tm.tm_mon,
+               &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6)
     {
-      msg->fp = mutt_file_fopen(acache->path, "r");
-      if (msg->fp)
+      tm.tm_year -= 1900;
+      tm.tm_mon--;
+      *now = timegm(&tm);
+      if (*now >= 0)
+      {
+        mutt_debug(1, "server time is %lu\n", *now);
         return 0;
-    }
-    /* clear previous entry */
-    else
-    {
-      unlink(acache->path);
-      FREE(&acache->path);
+      }
     }
   }
-  snprintf(article, sizeof(article), "%d", NHDR(hdr)->article_num);
-  msg->fp = mutt_bcache_get(nntp_data->bcache, article);
-  if (msg->fp)
-  {
-    if (NHDR(hdr)->parsed)
+  time(now);
+  return 0;
+}
+
+/**
+ * fetch_children - Parse XPAT line
+ * @param line String to parse
+ * @param data ChildCtx
+ * @retval 0 Always
+ */
+static int fetch_children(char *line, void *data)
+{
+  struct ChildCtx *cc = data;
+  anum_t anum;
+
+  if (!line || sscanf(line, ANUM, &anum) != 1)
+    return 0;
+  for (unsigned int i = 0; i < cc->mailbox->msg_count; i++)
+    if (NHDR(cc->mailbox->hdrs[i])->article_num == anum)
       return 0;
+  if (cc->num >= cc->max)
+  {
+    cc->max *= 2;
+    mutt_mem_realloc(&cc->child, sizeof(anum_t) * cc->max);
   }
-  else
+  cc->child[cc->num++] = anum;
+  return 0;
+}
+
+/**
+ * nntp_open_connection - Connect to server, authenticate and get capabilities
+ * @param nserv NNTP server
+ * @retval  0 Success
+ * @retval -1 Failure
+ */
+int nntp_open_connection(struct NntpServer *nserv)
+{
+  struct Connection *conn = nserv->conn;
+  char buf[STRING];
+  int cap;
+  bool posting = false, auth = true;
+
+  if (nserv->status == NNTP_OK)
+    return 0;
+  if (nserv->status == NNTP_BYE)
+    return -1;
+  nserv->status = NNTP_NONE;
+
+  if (mutt_socket_open(conn) < 0)
+    return -1;
+
+  if (mutt_socket_readln(buf, sizeof(buf), conn) < 0)
+    return nntp_connect_error(nserv);
+
+  if (mutt_str_strncmp("200", buf, 3) == 0)
+    posting = true;
+  else if (mutt_str_strncmp("201", buf, 3) != 0)
   {
-    char buf[PATH_MAX];
-    /* don't try to fetch article from removed newsgroup */
-    if (nntp_data->deleted)
+    mutt_socket_close(conn);
+    mutt_str_remove_trailing_ws(buf);
+    mutt_error("%s", buf);
+    return -1;
+  }
+
+  /* get initial capabilities */
+  cap = nntp_capabilities(nserv);
+  if (cap < 0)
+    return -1;
+
+  /* tell news server to switch to mode reader if it isn't so */
+  if (cap > 0)
+  {
+    if (mutt_socket_send(conn, "MODE READER\r\n") < 0 ||
+        mutt_socket_readln(buf, sizeof(buf), conn) < 0)
+    {
+      return nntp_connect_error(nserv);
+    }
+
+    if (mutt_str_strncmp("200", buf, 3) == 0)
+      posting = true;
+    else if (mutt_str_strncmp("201", buf, 3) == 0)
+      posting = false;
+    /* error if has capabilities, ignore result if no capabilities */
+    else if (nserv->hasCAPABILITIES)
+    {
+      mutt_socket_close(conn);
+      mutt_error(_("Could not switch to reader mode"));
       return -1;
+    }
 
-    /* create new cache file */
-    const char *fetch_msg = _("Fetching message...");
-    mutt_message(fetch_msg);
-    msg->fp = mutt_bcache_put(nntp_data->bcache, article);
-    if (!msg->fp)
+    /* recheck capabilities after MODE READER */
+    if (nserv->hasCAPABILITIES)
     {
-      mutt_mktemp(buf, sizeof(buf));
-      acache->path = mutt_str_strdup(buf);
-      acache->index = hdr->index;
-      msg->fp = mutt_file_fopen(acache->path, "w+");
-      if (!msg->fp)
-      {
-        mutt_perror(acache->path);
-        unlink(acache->path);
-        FREE(&acache->path);
+      cap = nntp_capabilities(nserv);
+      if (cap < 0)
         return -1;
+    }
+  }
+
+  mutt_message(_("Connected to %s. %s"), conn->account.host,
+               posting ? _("Posting is ok") : _("Posting is NOT ok"));
+  mutt_sleep(1);
+
+#ifdef USE_SSL
+  /* Attempt STARTTLS if available and desired. */
+  if (nserv->use_tls != 1 && (nserv->hasSTARTTLS || SslForceTls))
+  {
+    if (nserv->use_tls == 0)
+    {
+      nserv->use_tls =
+          SslForceTls || query_quadoption(SslStarttls,
+                                          _("Secure connection with TLS?")) == MUTT_YES ?
+              2 :
+              1;
+    }
+    if (nserv->use_tls == 2)
+    {
+      if (mutt_socket_send(conn, "STARTTLS\r\n") < 0 ||
+          mutt_socket_readln(buf, sizeof(buf), conn) < 0)
+      {
+        return nntp_connect_error(nserv);
+      }
+      if (mutt_str_strncmp("382", buf, 3) != 0)
+      {
+        nserv->use_tls = 0;
+        mutt_error("STARTTLS: %s", buf);
       }
-    }
-
-    /* fetch message to cache file */
-    snprintf(buf, sizeof(buf), "ARTICLE %s\r\n",
-             NHDR(hdr)->article_num ? article : hdr->env->message_id);
-    const int rc = nntp_fetch_lines(nntp_data, buf, sizeof(buf), fetch_msg,
-                                    fetch_tempfile, msg->fp);
-    if (rc)
-    {
-      mutt_file_fclose(&msg->fp);
-      if (acache->path)
+      else if (mutt_ssl_starttls(conn))
       {
-        unlink(acache->path);
-        FREE(&acache->path);
+        nserv->use_tls = 0;
+        nserv->status = NNTP_NONE;
+        mutt_socket_close(nserv->conn);
+        mutt_error(_("Could not negotiate TLS connection"));
+        return -1;
       }
-      if (rc > 0)
+      else
       {
-        if (mutt_str_strncmp(NHDR(hdr)->article_num ? "423" : "430", buf, 3) == 0)
-        {
-          mutt_error(_("Article %d not found on the server"),
-                     NHDR(hdr)->article_num ? article : hdr->env->message_id);
-        }
-        else
-          mutt_error("ARTICLE: %s", buf);
+        /* recheck capabilities after STARTTLS */
+        cap = nntp_capabilities(nserv);
+        if (cap < 0)
+          return -1;
       }
-      return -1;
     }
-
-    if (!acache->path)
-      mutt_bcache_commit(nntp_data->bcache, article);
   }
+#endif
 
-  /* replace envelope with new one
-   * hash elements must be updated because pointers will be changed */
-  if (ctx->mailbox->id_hash && hdr->env->message_id)
-    mutt_hash_delete(ctx->mailbox->id_hash, hdr->env->message_id, hdr);
-  if (ctx->mailbox->subj_hash && hdr->env->real_subj)
-    mutt_hash_delete(ctx->mailbox->subj_hash, hdr->env->real_subj, hdr);
-
-  mutt_env_free(&hdr->env);
-  hdr->env = mutt_rfc822_read_header(msg->fp, hdr, false, false);
-
-  if (ctx->mailbox->id_hash && hdr->env->message_id)
-    mutt_hash_insert(ctx->mailbox->id_hash, hdr->env->message_id, hdr);
-  if (ctx->mailbox->subj_hash && hdr->env->real_subj)
-    mutt_hash_insert(ctx->mailbox->subj_hash, hdr->env->real_subj, hdr);
+  /* authentication required? */
+  if (conn->account.flags & MUTT_ACCT_USER)
+  {
+    if (!conn->account.user[0])
+      auth = false;
+  }
+  else
+  {
+    if (mutt_socket_send(conn, "STAT\r\n") < 0 ||
+        mutt_socket_readln(buf, sizeof(buf), conn) < 0)
+    {
+      return nntp_connect_error(nserv);
+    }
+    if (mutt_str_strncmp("480", buf, 3) != 0)
+      auth = false;
+  }
 
-  /* fix content length */
-  fseek(msg->fp, 0, SEEK_END);
-  hdr->content->length = ftell(msg->fp) - hdr->content->offset;
+  /* authenticate */
+  if (auth && nntp_auth(nserv) < 0)
+    return -1;
 
-  /* this is called in neomutt before the open which fetches the message,
-   * which is probably wrong, but we just call it again here to handle
-   * the problem instead of fixing it */
-  NHDR(hdr)->parsed = true;
-  mutt_parse_mime_message(ctx, hdr);
+  /* get final capabilities after authentication */
+  if (nserv->hasCAPABILITIES && (auth || cap > 0))
+  {
+    cap = nntp_capabilities(nserv);
+    if (cap < 0)
+      return -1;
+    if (cap > 0)
+    {
+      mutt_socket_close(conn);
+      mutt_error(_("Could not switch to reader mode"));
+      return -1;
+    }
+  }
 
-  /* these would normally be updated in mx_update_context(), but the
-   * full headers aren't parsed with overview, so the information wasn't
-   * available then */
-  if (WithCrypto)
-    hdr->security = crypt_query(hdr->content);
+  /* attempt features */
+  if (nntp_attempt_features(nserv) < 0)
+    return -1;
 
-  rewind(msg->fp);
-  mutt_clear_error();
+  nserv->status = NNTP_OK;
   return 0;
 }
 
-/**
- * nntp_msg_close - Implements MxOps::msg_close()
- *
- * @note May also return EOF Failure, see errno
- */
-static int nntp_msg_close(struct Context *ctx, struct Message *msg)
-{
-  return mutt_file_fclose(&msg->fp);
-}
-
 /**
  * nntp_post - Post article
  * @param mailbox Mailbox
@@ -1856,777 +1928,705 @@ int nntp_post(struct Mailbox *mailbox, const char *msg)
 }
 
 /**
- * nntp_group_poll - Check newsgroup for new articles
- * @param nntp_data   NNTP server data
- * @param update_stat Update the stats?
- * @retval  1 New articles found
- * @retval  0 No change
- * @retval -1 Lost connection
+ * nntp_active_fetch - Fetch list of all newsgroups from server
+ * @param nserv NNTP server
+ * @param new   Mark the groups as new
+ * @retval  0 Success
+ * @retval -1 Failure
  */
-static int nntp_group_poll(struct NntpData *nntp_data, int update_stat)
+int nntp_active_fetch(struct NntpServer *nserv, bool new)
 {
-  char buf[LONG_STRING] = "";
-  anum_t count, first, last;
+  struct NntpData nntp_data;
+  char msg[STRING];
+  char buf[LONG_STRING];
+  unsigned int i;
+  int rc;
 
-  /* use GROUP command to poll newsgroup */
-  if (nntp_query(nntp_data, buf, sizeof(buf)) < 0)
+  snprintf(msg, sizeof(msg), _("Loading list of groups from server %s..."),
+           nserv->conn->account.host);
+  mutt_message(msg);
+  if (nntp_date(nserv, &nserv->newgroups_time) < 0)
     return -1;
-  if (sscanf(buf, "211 " ANUM " " ANUM " " ANUM, &count, &first, &last) != 3)
-    return 0;
-  if (first == nntp_data->first_message && last == nntp_data->last_message)
-    return 0;
 
-  /* articles have been renumbered */
-  if (last < nntp_data->last_message)
+  nntp_data.nserv = nserv;
+  nntp_data.group = NULL;
+  i = nserv->groups_num;
+  mutt_str_strfcpy(buf, "LIST\r\n", sizeof(buf));
+  rc = nntp_fetch_lines(&nntp_data, buf, sizeof(buf), msg, nntp_add_group, nserv);
+  if (rc)
   {
-    nntp_data->last_cached = 0;
-    if (nntp_data->newsrc_len)
+    if (rc > 0)
     {
-      mutt_mem_realloc(&nntp_data->newsrc_ent, sizeof(struct NewsrcEntry));
-      nntp_data->newsrc_len = 1;
-      nntp_data->newsrc_ent[0].first = 1;
-      nntp_data->newsrc_ent[0].last = 0;
+      mutt_error("LIST: %s", buf);
     }
+    return -1;
   }
-  nntp_data->first_message = first;
-  nntp_data->last_message = last;
-  if (!update_stat)
-    return 1;
 
-  /* update counters */
-  else if (!last || (!nntp_data->newsrc_ent && !nntp_data->last_cached))
-    nntp_data->unread = count;
-  else
-    nntp_group_unread_stat(nntp_data);
-  return 1;
+  if (new)
+  {
+    for (; i < nserv->groups_num; i++)
+    {
+      struct NntpData *data = nserv->groups_list[i];
+      data->new = true;
+    }
+  }
+
+  for (i = 0; i < nserv->groups_num; i++)
+  {
+    struct NntpData *data = nserv->groups_list[i];
+
+    if (data && data->deleted && !data->newsrc_ent)
+    {
+      nntp_delete_group_cache(data);
+      mutt_hash_delete(nserv->groups_hash, data->group, NULL);
+      nserv->groups_list[i] = NULL;
+    }
+  }
+
+  if (NntpLoadDescription)
+    rc = get_description(&nntp_data, "*", _("Loading descriptions..."));
+
+  nntp_active_save_cache(nserv);
+  if (rc < 0)
+    return -1;
+  mutt_clear_error();
+  return 0;
 }
 
 /**
- * check_mailbox - Check current newsgroup for new articles
- * @param ctx Mailbox
- * @retval #MUTT_REOPENED Articles have been renumbered or removed from server
- * @retval #MUTT_NEW_MAIL New articles found
- * @retval  0             No change
- * @retval -1             Lost connection
- *
- * Leave newsrc locked
+ * nntp_check_new_groups - Check for new groups/articles in subscribed groups
+ * @param mailbox Mailbox
+ * @param nserv   NNTP server
+ * @retval  1 New groups found
+ * @retval  0 No new groups
+ * @retval -1 Error
  */
-static int check_mailbox(struct Context *ctx)
+int nntp_check_new_groups(struct Mailbox *mailbox, struct NntpServer *nserv)
 {
-  struct NntpData *nntp_data = ctx->mailbox->data;
-  struct NntpServer *nserv = nntp_data->nserv;
-  time_t now = time(NULL);
-  int rc, ret = 0;
-  void *hc = NULL;
+  struct NntpData nntp_data;
+  time_t now;
+  struct tm *tm = NULL;
+  char buf[LONG_STRING];
+  char *msg = _("Checking for new newsgroups...");
+  unsigned int i;
+  int rc, update_active = false;
 
-  if (nserv->check_time + NntpPoll > now)
+  if (!nserv || !nserv->newgroups_time)
+    return -1;
+
+  /* check subscribed newsgroups for new articles */
+  if (ShowNewNews)
+  {
+    mutt_message(_("Checking for new messages..."));
+    for (i = 0; i < nserv->groups_num; i++)
+    {
+      struct NntpData *data = nserv->groups_list[i];
+
+      if (data && data->subscribed)
+      {
+        rc = nntp_group_poll(data, 1);
+        if (rc < 0)
+          return -1;
+        if (rc > 0)
+          update_active = true;
+      }
+    }
+    /* select current newsgroup */
+    if (mailbox && (mailbox->magic == MUTT_NNTP))
+    {
+      buf[0] = '\0';
+      if (nntp_query(mailbox->data, buf, sizeof(buf)) < 0)
+        return -1;
+    }
+  }
+  else if (nserv->newgroups_time)
     return 0;
 
-  mutt_message(_("Checking for new messages..."));
-  if (nntp_newsrc_parse(nserv) < 0)
-    return -1;
-
-  nserv->check_time = now;
-  rc = nntp_group_poll(nntp_data, 0);
-  if (rc < 0)
-  {
-    nntp_newsrc_close(nserv);
+  /* get list of new groups */
+  mutt_message(msg);
+  if (nntp_date(nserv, &now) < 0)
     return -1;
-  }
+  nntp_data.nserv = nserv;
+  if (mailbox && (mailbox->magic == MUTT_NNTP))
+    nntp_data.group = ((struct NntpData *) mailbox->data)->group;
+  else
+    nntp_data.group = NULL;
+  i = nserv->groups_num;
+  tm = gmtime(&nserv->newgroups_time);
+  snprintf(buf, sizeof(buf), "NEWGROUPS %02d%02d%02d %02d%02d%02d GMT\r\n",
+           tm->tm_year % 100, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour,
+           tm->tm_min, tm->tm_sec);
+  rc = nntp_fetch_lines(&nntp_data, buf, sizeof(buf), msg, nntp_add_group, nserv);
   if (rc)
-    nntp_active_save_cache(nserv);
-
-  /* articles have been renumbered, remove all headers */
-  if (nntp_data->last_message < nntp_data->last_loaded)
   {
-    for (int i = 0; i < ctx->mailbox->msg_count; i++)
-      mutt_header_free(&ctx->mailbox->hdrs[i]);
-    ctx->mailbox->msg_count = 0;
-    ctx->tagged = 0;
-
-    if (nntp_data->last_message < nntp_data->last_loaded)
+    if (rc > 0)
     {
-      nntp_data->last_loaded = nntp_data->first_message - 1;
-      if (NntpContext && nntp_data->last_message - nntp_data->last_loaded > NntpContext)
-        nntp_data->last_loaded = nntp_data->last_message - NntpContext;
+      mutt_error("NEWGROUPS: %s", buf);
     }
-    ret = MUTT_REOPENED;
+    return -1;
   }
 
-  /* .newsrc has been externally modified */
-  if (nserv->newsrc_modified)
+  /* new groups found */
+  rc = 0;
+  if (nserv->groups_num != i)
   {
-#ifdef USE_HCACHE
-    unsigned char *messages = NULL;
-    char buf[16];
-    void *hdata = NULL;
-    struct Header *hdr = NULL;
-    anum_t first = nntp_data->first_message;
-
-    if (NntpContext && nntp_data->last_message - first + 1 > NntpContext)
-      first = nntp_data->last_message - NntpContext + 1;
-    messages = mutt_mem_calloc(nntp_data->last_loaded - first + 1, sizeof(unsigned char));
-    hc = nntp_hcache_open(nntp_data);
-    nntp_hcache_update(nntp_data, hc);
-#endif
+    int groups_num = i;
 
-    /* update flags according to .newsrc */
-    int j = 0;
-    anum_t anum;
-    for (int i = 0; i < ctx->mailbox->msg_count; i++)
+    nserv->newgroups_time = now;
+    for (; i < nserv->groups_num; i++)
     {
-      bool flagged = false;
-      anum = NHDR(ctx->mailbox->hdrs[i])->article_num;
-
-#ifdef USE_HCACHE
-      /* check hcache for flagged and deleted flags */
-      if (hc)
-      {
-        if (anum >= first && anum <= nntp_data->last_loaded)
-          messages[anum - first] = 1;
-
-        snprintf(buf, sizeof(buf), "%u", anum);
-        hdata = mutt_hcache_fetch(hc, buf, strlen(buf));
-        if (hdata)
-        {
-          bool deleted;
-
-          mutt_debug(2, "#1 mutt_hcache_fetch %s\n", buf);
-          hdr = mutt_hcache_restore(hdata);
-          mutt_hcache_free(hc, &hdata);
-          hdr->data = NULL;
-          deleted = hdr->deleted;
-          flagged = hdr->flagged;
-          mutt_header_free(&hdr);
-
-          /* header marked as deleted, removing from context */
-          if (deleted)
-          {
-            mutt_set_flag(ctx, ctx->mailbox->hdrs[i], MUTT_TAG, 0);
-            mutt_header_free(&ctx->mailbox->hdrs[i]);
-            continue;
-          }
-        }
-      }
-#endif
-
-      if (!ctx->mailbox->hdrs[i]->changed)
-      {
-        ctx->mailbox->hdrs[i]->flagged = flagged;
-        ctx->mailbox->hdrs[i]->read = false;
-        ctx->mailbox->hdrs[i]->old = false;
-        nntp_article_status(ctx->mailbox, ctx->mailbox->hdrs[i], NULL, anum);
-        if (!ctx->mailbox->hdrs[i]->read)
-          nntp_parse_xref(ctx->mailbox, ctx->mailbox->hdrs[i]);
-      }
-      ctx->mailbox->hdrs[j++] = ctx->mailbox->hdrs[i];
+      struct NntpData *data = nserv->groups_list[i];
+      data->new = true;
     }
 
-#ifdef USE_HCACHE
-    ctx->mailbox->msg_count = j;
-
-    /* restore headers without "deleted" flag */
-    for (anum = first; anum <= nntp_data->last_loaded; anum++)
+    /* loading descriptions */
+    if (NntpLoadDescription)
     {
-      if (messages[anum - first])
-        continue;
+      unsigned int count = 0;
+      struct Progress progress;
 
-      snprintf(buf, sizeof(buf), "%u", anum);
-      hdata = mutt_hcache_fetch(hc, buf, strlen(buf));
-      if (hdata)
+      mutt_progress_init(&progress, _("Loading descriptions..."),
+                         MUTT_PROGRESS_MSG, ReadInc, nserv->groups_num - i);
+      for (i = groups_num; i < nserv->groups_num; i++)
       {
-        mutt_debug(2, "#2 mutt_hcache_fetch %s\n", buf);
-        if (ctx->mailbox->msg_count >= ctx->mailbox->hdrmax)
-          mx_alloc_memory(ctx->mailbox);
-
-        hdr = mutt_hcache_restore(hdata);
-        ctx->mailbox->hdrs[ctx->mailbox->msg_count] = hdr;
-        mutt_hcache_free(hc, &hdata);
-        hdr->data = NULL;
-        if (hdr->deleted)
-        {
-          mutt_header_free(&hdr);
-          if (nntp_data->bcache)
-          {
-            mutt_debug(2, "mutt_bcache_del %s\n", buf);
-            mutt_bcache_del(nntp_data->bcache, buf);
-          }
-          continue;
-        }
+        struct NntpData *data = nserv->groups_list[i];
 
-        ctx->mailbox->msg_count++;
-        hdr->read = false;
-        hdr->old = false;
-        hdr->data = mutt_mem_calloc(1, sizeof(struct NntpHeaderData));
-        NHDR(hdr)->article_num = anum;
-        nntp_article_status(ctx->mailbox, hdr, NULL, anum);
-        if (!hdr->read)
-          nntp_parse_xref(ctx->mailbox, hdr);
+        if (get_description(data, NULL, NULL) < 0)
+          return -1;
+        mutt_progress_update(&progress, ++count, -1);
       }
     }
-    FREE(&messages);
-#endif
-
-    nserv->newsrc_modified = false;
-    ret = MUTT_REOPENED;
-  }
-
-  /* some headers were removed, context must be updated */
-  if (ret == MUTT_REOPENED)
-  {
-    if (ctx->mailbox->subj_hash)
-      mutt_hash_destroy(&ctx->mailbox->subj_hash);
-    if (ctx->mailbox->id_hash)
-      mutt_hash_destroy(&ctx->mailbox->id_hash);
-    mutt_clear_threads(ctx);
-
-    ctx->mailbox->vcount = 0;
-    ctx->deleted = 0;
-    ctx->new = 0;
-    ctx->mailbox->msg_unread = 0;
-    ctx->mailbox->msg_flagged = 0;
-    ctx->mailbox->changed = false;
-    ctx->mailbox->id_hash = NULL;
-    ctx->mailbox->subj_hash = NULL;
-    mx_update_context(ctx, ctx->mailbox->msg_count);
-  }
-
-  /* fetch headers of new articles */
-  if (nntp_data->last_message > nntp_data->last_loaded)
-  {
-    int oldmsgcount = ctx->mailbox->msg_count;
-    bool quiet = ctx->mailbox->quiet;
-    ctx->mailbox->quiet = true;
-#ifdef USE_HCACHE
-    if (!hc)
-    {
-      hc = nntp_hcache_open(nntp_data);
-      nntp_hcache_update(nntp_data, hc);
-    }
-#endif
-    rc = nntp_fetch_headers(ctx, hc, nntp_data->last_loaded + 1, nntp_data->last_message, 0);
-    ctx->mailbox->quiet = quiet;
-    if (rc >= 0)
-      nntp_data->last_loaded = nntp_data->last_message;
-    if (ret == 0 && ctx->mailbox->msg_count > oldmsgcount)
-      ret = MUTT_NEW_MAIL;
+    update_active = true;
+    rc = 1;
   }
-
-#ifdef USE_HCACHE
-  mutt_hcache_close(hc);
-#endif
-  if (ret)
-    nntp_newsrc_close(nserv);
+  if (update_active)
+    nntp_active_save_cache(nserv);
   mutt_clear_error();
-  return ret;
+  return rc;
 }
 
 /**
- * nntp_mbox_check - Implements MxOps::mbox_check()
- * @param ctx        Mailbox
- * @param index_hint Current message (UNUSED)
- * @retval #MUTT_REOPENED Articles have been renumbered or removed from server
- * @retval #MUTT_NEW_MAIL New articles found
- * @retval  0             No change
- * @retval -1             Lost connection
+ * nntp_check_msgid - Fetch article by Message-ID
+ * @param ctx   Mailbox
+ * @param msgid Message ID
+ * @retval  0 Success
+ * @retval  1 No such article
+ * @retval -1 Error
  */
-static int nntp_mbox_check(struct Context *ctx, int *index_hint)
+int nntp_check_msgid(struct Context *ctx, const char *msgid)
 {
-  int ret = check_mailbox(ctx);
-  if (ret == 0)
+  struct NntpData *nntp_data = ctx->mailbox->data;
+  char buf[LONG_STRING];
+
+  FILE *fp = mutt_file_mkstemp();
+  if (!fp)
+  {
+    mutt_perror(_("Can't create temporary file"));
+    return -1;
+  }
+
+  snprintf(buf, sizeof(buf), "HEAD %s\r\n", msgid);
+  int rc = nntp_fetch_lines(nntp_data, buf, sizeof(buf), NULL, fetch_tempfile, fp);
+  if (rc)
+  {
+    mutt_file_fclose(&fp);
+    if (rc < 0)
+      return -1;
+    if (mutt_str_strncmp("430", buf, 3) == 0)
+      return 1;
+    mutt_error("HEAD: %s", buf);
+    return -1;
+  }
+
+  /* parse header */
+  if (ctx->mailbox->msg_count == ctx->mailbox->hdrmax)
+    mx_alloc_memory(ctx->mailbox);
+  ctx->mailbox->hdrs[ctx->mailbox->msg_count] = mutt_header_new();
+  struct Header *hdr = ctx->mailbox->hdrs[ctx->mailbox->msg_count];
+  hdr->data = mutt_mem_calloc(1, sizeof(struct NntpHeaderData));
+  hdr->env = mutt_rfc822_read_header(fp, hdr, false, false);
+  mutt_file_fclose(&fp);
+
+  /* get article number */
+  if (hdr->env->xref)
+    nntp_parse_xref(ctx->mailbox, hdr);
+  else
   {
-    struct NntpData *nntp_data = ctx->mailbox->data;
-    struct NntpServer *nserv = nntp_data->nserv;
-    nntp_newsrc_close(nserv);
+    snprintf(buf, sizeof(buf), "STAT %s\r\n", msgid);
+    if (nntp_query(nntp_data, buf, sizeof(buf)) < 0)
+    {
+      mutt_header_free(&hdr);
+      return -1;
+    }
+    sscanf(buf + 4, ANUM, &NHDR(hdr)->article_num);
   }
-  return ret;
+
+  /* reset flags */
+  hdr->read = false;
+  hdr->old = false;
+  hdr->deleted = false;
+  hdr->changed = true;
+  hdr->received = hdr->date_sent;
+  hdr->index = ctx->mailbox->msg_count++;
+  mx_update_context(ctx, 1);
+  return 0;
 }
 
 /**
- * nntp_mbox_sync - Implements MxOps::mbox_sync()
- *
- * @note May also return values from check_mailbox()
+ * nntp_check_children - Fetch children of article with the Message-ID
+ * @param ctx   Mailbox
+ * @param msgid Message ID to find
+ * @retval  0 Success
+ * @retval -1 Failure
  */
-static int nntp_mbox_sync(struct Context *ctx, int *index_hint)
+int nntp_check_children(struct Context *ctx, const char *msgid)
 {
   struct NntpData *nntp_data = ctx->mailbox->data;
+  struct ChildCtx cc;
+  char buf[STRING];
   int rc;
-#ifdef USE_HCACHE
-  header_cache_t *hc = NULL;
-#endif
+  bool quiet;
+  void *hc = NULL;
 
-  /* check for new articles */
-  nntp_data->nserv->check_time = 0;
-  rc = check_mailbox(ctx);
-  if (rc)
-    return rc;
+  if (!nntp_data || !nntp_data->nserv)
+    return -1;
+  if (nntp_data->first_message > nntp_data->last_loaded)
+    return 0;
 
-#ifdef USE_HCACHE
-  nntp_data->last_cached = 0;
-  hc = nntp_hcache_open(nntp_data);
-#endif
+  /* init context */
+  cc.mailbox = ctx->mailbox;
+  cc.num = 0;
+  cc.max = 10;
+  cc.child = mutt_mem_malloc(sizeof(anum_t) * cc.max);
 
-  for (int i = 0; i < ctx->mailbox->msg_count; i++)
+  /* fetch numbers of child messages */
+  snprintf(buf, sizeof(buf), "XPAT References %u-%u *%s*\r\n",
+           nntp_data->first_message, nntp_data->last_loaded, msgid);
+  rc = nntp_fetch_lines(nntp_data, buf, sizeof(buf), NULL, fetch_children, &cc);
+  if (rc)
   {
-    struct Header *hdr = ctx->mailbox->hdrs[i];
-    char buf[16];
-
-    snprintf(buf, sizeof(buf), "%d", NHDR(hdr)->article_num);
-    if (nntp_data->bcache && hdr->deleted)
-    {
-      mutt_debug(2, "mutt_bcache_del %s\n", buf);
-      mutt_bcache_del(nntp_data->bcache, buf);
-    }
-
-#ifdef USE_HCACHE
-    if (hc && (hdr->changed || hdr->deleted))
+    FREE(&cc.child);
+    if (rc > 0)
     {
-      if (hdr->deleted && !hdr->read)
-        nntp_data->unread--;
-      mutt_debug(2, "mutt_hcache_store %s\n", buf);
-      mutt_hcache_store(hc, buf, strlen(buf), hdr, 0);
+      if (mutt_str_strncmp("500", buf, 3) != 0)
+        mutt_error("XPAT: %s", buf);
+      else
+      {
+        mutt_error(_("Unable to find child articles because server does not "
+                     "support XPAT command"));
+      }
     }
-#endif
+    return -1;
   }
 
+  /* fetch all found messages */
+  quiet = ctx->mailbox->quiet;
+  ctx->mailbox->quiet = true;
 #ifdef USE_HCACHE
-  if (hc)
+  hc = nntp_hcache_open(nntp_data);
+#endif
+  for (int i = 0; i < cc.num; i++)
   {
-    mutt_hcache_close(hc);
-    nntp_data->last_cached = nntp_data->last_loaded;
+    rc = nntp_fetch_headers(ctx, hc, cc.child[i], cc.child[i], 1);
+    if (rc < 0)
+      break;
   }
+#ifdef USE_HCACHE
+  mutt_hcache_close(hc);
 #endif
-
-  /* save .newsrc entries */
-  nntp_newsrc_gen_entries(ctx);
-  nntp_newsrc_update(nntp_data->nserv);
-  nntp_newsrc_close(nntp_data->nserv);
-  return 0;
+  ctx->mailbox->quiet = quiet;
+  FREE(&cc.child);
+  return (rc < 0) ? -1 : 0;
 }
 
 /**
- * nntp_mbox_close - Implements MxOps::mbox_close()
- * @retval 0 Always
+ * nntp_compare_order - Sort to mailbox order - Implements ::sort_t
  */
-static int nntp_mbox_close(struct Context *ctx)
+int nntp_compare_order(const void *a, const void *b)
 {
-  struct NntpData *nntp_data = ctx->mailbox->data, *nntp_tmp = NULL;
-
-  if (!nntp_data)
-    return 0;
-
-  nntp_data->unread = ctx->mailbox->msg_unread;
-
-  nntp_acache_free(nntp_data);
-  if (!nntp_data->nserv || !nntp_data->nserv->groups_hash || !nntp_data->group)
-    return 0;
+  struct Header **ha = (struct Header **) a;
+  struct Header **hb = (struct Header **) b;
 
-  nntp_tmp = mutt_hash_find(nntp_data->nserv->groups_hash, nntp_data->group);
-  if (!nntp_tmp || nntp_tmp != nntp_data)
-    nntp_data_free(nntp_data);
-  return 0;
+  anum_t na = NHDR(*ha)->article_num;
+  anum_t nb = NHDR(*hb)->article_num;
+  int result = (na == nb) ? 0 : (na > nb) ? 1 : -1;
+  result = perform_auxsort(result, a, b);
+  return SORTCODE(result);
 }
 
 /**
- * nntp_date - Get date and time from server
- * @param nserv NNTP server
- * @param now   Server time
- * @retval  0 Success
- * @retval -1 Failure
+ * nntp_mbox_open - Implements MxOps::mbox_open()
  */
-static int nntp_date(struct NntpServer *nserv, time_t *now)
+static int nntp_mbox_open(struct Context *ctx)
 {
-  if (nserv->hasDATE)
-  {
-    struct NntpData nntp_data;
-    char buf[LONG_STRING];
-    struct tm tm;
-    memset(&tm, 0, sizeof(tm));
-
-    nntp_data.nserv = nserv;
-    nntp_data.group = NULL;
-    mutt_str_strfcpy(buf, "DATE\r\n", sizeof(buf));
-    if (nntp_query(&nntp_data, buf, sizeof(buf)) < 0)
-      return -1;
+  struct NntpServer *nserv = NULL;
+  struct NntpData *nntp_data = NULL;
+  char buf[HUGE_STRING];
+  char server[LONG_STRING];
+  char *group = NULL;
+  int rc;
+  void *hc = NULL;
+  anum_t first, last, count = 0;
+  struct Url url;
 
-    if (sscanf(buf, "111 %4d%2d%2d%2d%2d%2d%*s", &tm.tm_year, &tm.tm_mon,
-               &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6)
-    {
-      tm.tm_year -= 1900;
-      tm.tm_mon--;
-      *now = timegm(&tm);
-      if (*now >= 0)
-      {
-        mutt_debug(1, "server time is %lu\n", *now);
-        return 0;
-      }
-    }
+  mutt_str_strfcpy(buf, ctx->mailbox->path, sizeof(buf));
+  if (url_parse(&url, buf) < 0 || !url.host || !url.path ||
+      !(url.scheme == U_NNTP || url.scheme == U_NNTPS))
+  {
+    url_free(&url);
+    mutt_error(_("%s is an invalid newsgroup specification"), ctx->mailbox->path);
+    return -1;
   }
-  time(now);
-  return 0;
-}
-
-/**
- * nntp_active_fetch - Fetch list of all newsgroups from server
- * @param nserv NNTP server
- * @param new   Mark the groups as new
- * @retval  0 Success
- * @retval -1 Failure
- */
-int nntp_active_fetch(struct NntpServer *nserv, bool new)
-{
-  struct NntpData nntp_data;
-  char msg[STRING];
-  char buf[LONG_STRING];
-  unsigned int i;
-  int rc;
 
-  snprintf(msg, sizeof(msg), _("Loading list of groups from server %s..."),
-           nserv->conn->account.host);
-  mutt_message(msg);
-  if (nntp_date(nserv, &nserv->newgroups_time) < 0)
+  group = url.path;
+  url.path = strchr(url.path, '\0');
+  url_tostring(&url, server, sizeof(server), 0);
+  nserv = nntp_select_server(ctx->mailbox, server, true);
+  url_free(&url);
+  if (!nserv)
     return -1;
+  CurrentNewsSrv = nserv;
 
-  nntp_data.nserv = nserv;
-  nntp_data.group = NULL;
-  i = nserv->groups_num;
-  mutt_str_strfcpy(buf, "LIST\r\n", sizeof(buf));
-  rc = nntp_fetch_lines(&nntp_data, buf, sizeof(buf), msg, nntp_add_group, nserv);
-  if (rc)
+  /* find news group data structure */
+  nntp_data = mutt_hash_find(nserv->groups_hash, group);
+  if (!nntp_data)
   {
-    if (rc > 0)
-    {
-      mutt_error("LIST: %s", buf);
-    }
+    nntp_newsrc_close(nserv);
+    mutt_error(_("Newsgroup %s not found on the server"), group);
+    return -1;
+  }
+
+  mutt_bit_unset(ctx->mailbox->rights, MUTT_ACL_INSERT);
+  if (!nntp_data->newsrc_ent && !nntp_data->subscribed && !SaveUnsubscribed)
+    ctx->mailbox->readonly = true;
+
+  /* select newsgroup */
+  mutt_message(_("Selecting %s..."), group);
+  buf[0] = '\0';
+  if (nntp_query(nntp_data, buf, sizeof(buf)) < 0)
+  {
+    nntp_newsrc_close(nserv);
     return -1;
   }
 
-  if (new)
+  /* newsgroup not found, remove it */
+  if (mutt_str_strncmp("411", buf, 3) == 0)
   {
-    for (; i < nserv->groups_num; i++)
+    mutt_error(_("Newsgroup %s has been removed from the server"), nntp_data->group);
+    if (!nntp_data->deleted)
     {
-      struct NntpData *data = nserv->groups_list[i];
-      data->new = true;
+      nntp_data->deleted = true;
+      nntp_active_save_cache(nserv);
+    }
+    if (nntp_data->newsrc_ent && !nntp_data->subscribed && !SaveUnsubscribed)
+    {
+      FREE(&nntp_data->newsrc_ent);
+      nntp_data->newsrc_len = 0;
+      nntp_delete_group_cache(nntp_data);
+      nntp_newsrc_update(nserv);
     }
   }
 
-  for (i = 0; i < nserv->groups_num; i++)
+  /* parse newsgroup info */
+  else
   {
-    struct NntpData *data = nserv->groups_list[i];
+    if (sscanf(buf, "211 " ANUM " " ANUM " " ANUM, &count, &first, &last) != 3)
+    {
+      nntp_newsrc_close(nserv);
+      mutt_error("GROUP: %s", buf);
+      return -1;
+    }
+    nntp_data->first_message = first;
+    nntp_data->last_message = last;
+    nntp_data->deleted = false;
 
-    if (data && data->deleted && !data->newsrc_ent)
+    /* get description if empty */
+    if (NntpLoadDescription && !nntp_data->desc)
     {
-      nntp_delete_group_cache(data);
-      mutt_hash_delete(nserv->groups_hash, data->group, NULL);
-      nserv->groups_list[i] = NULL;
+      if (get_description(nntp_data, NULL, NULL) < 0)
+      {
+        nntp_newsrc_close(nserv);
+        return -1;
+      }
+      if (nntp_data->desc)
+        nntp_active_save_cache(nserv);
     }
   }
 
-  if (NntpLoadDescription)
-    rc = get_description(&nntp_data, "*", _("Loading descriptions..."));
+  time(&nserv->check_time);
+  ctx->mailbox->data = nntp_data;
+  if (!nntp_data->bcache && (nntp_data->newsrc_ent || nntp_data->subscribed || SaveUnsubscribed))
+    nntp_data->bcache = mutt_bcache_open(&nserv->conn->account, nntp_data->group);
 
-  nntp_active_save_cache(nserv);
+  /* strip off extra articles if adding context is greater than $nntp_context */
+  first = nntp_data->first_message;
+  if (NntpContext && nntp_data->last_message - first + 1 > NntpContext)
+    first = nntp_data->last_message - NntpContext + 1;
+  nntp_data->last_loaded = first ? first - 1 : 0;
+  count = nntp_data->first_message;
+  nntp_data->first_message = first;
+  nntp_bcache_update(nntp_data);
+  nntp_data->first_message = count;
+#ifdef USE_HCACHE
+  hc = nntp_hcache_open(nntp_data);
+  nntp_hcache_update(nntp_data, hc);
+#endif
+  if (!hc)
+  {
+    mutt_bit_unset(ctx->mailbox->rights, MUTT_ACL_WRITE);
+    mutt_bit_unset(ctx->mailbox->rights, MUTT_ACL_DELETE);
+  }
+  nntp_newsrc_close(nserv);
+  rc = nntp_fetch_headers(ctx, hc, first, nntp_data->last_message, 0);
+#ifdef USE_HCACHE
+  mutt_hcache_close(hc);
+#endif
   if (rc < 0)
     return -1;
-  mutt_clear_error();
+  nntp_data->last_loaded = nntp_data->last_message;
+  nserv->newsrc_modified = false;
   return 0;
 }
 
 /**
- * nntp_check_new_groups - Check for new groups/articles in subscribed groups
- * @param mailbox Mailbox
- * @param nserv   NNTP server
- * @retval  1 New groups found
- * @retval  0 No new groups
- * @retval -1 Error
+ * nntp_mbox_check - Implements MxOps::mbox_check()
+ * @param ctx        Mailbox
+ * @param index_hint Current message (UNUSED)
+ * @retval #MUTT_REOPENED Articles have been renumbered or removed from server
+ * @retval #MUTT_NEW_MAIL New articles found
+ * @retval  0             No change
+ * @retval -1             Lost connection
  */
-int nntp_check_new_groups(struct Mailbox *mailbox, struct NntpServer *nserv)
+static int nntp_mbox_check(struct Context *ctx, int *index_hint)
 {
-  struct NntpData nntp_data;
-  time_t now;
-  struct tm *tm = NULL;
-  char buf[LONG_STRING];
-  char *msg = _("Checking for new newsgroups...");
-  unsigned int i;
-  int rc, update_active = false;
-
-  if (!nserv || !nserv->newgroups_time)
-    return -1;
-
-  /* check subscribed newsgroups for new articles */
-  if (ShowNewNews)
-  {
-    mutt_message(_("Checking for new messages..."));
-    for (i = 0; i < nserv->groups_num; i++)
-    {
-      struct NntpData *data = nserv->groups_list[i];
-
-      if (data && data->subscribed)
-      {
-        rc = nntp_group_poll(data, 1);
-        if (rc < 0)
-          return -1;
-        if (rc > 0)
-          update_active = true;
-      }
-    }
-    /* select current newsgroup */
-    if (mailbox && (mailbox->magic == MUTT_NNTP))
-    {
-      buf[0] = '\0';
-      if (nntp_query(mailbox->data, buf, sizeof(buf)) < 0)
-        return -1;
-    }
-  }
-  else if (nserv->newgroups_time)
-    return 0;
-
-  /* get list of new groups */
-  mutt_message(msg);
-  if (nntp_date(nserv, &now) < 0)
-    return -1;
-  nntp_data.nserv = nserv;
-  if (mailbox && (mailbox->magic == MUTT_NNTP))
-    nntp_data.group = ((struct NntpData *) mailbox->data)->group;
-  else
-    nntp_data.group = NULL;
-  i = nserv->groups_num;
-  tm = gmtime(&nserv->newgroups_time);
-  snprintf(buf, sizeof(buf), "NEWGROUPS %02d%02d%02d %02d%02d%02d GMT\r\n",
-           tm->tm_year % 100, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour,
-           tm->tm_min, tm->tm_sec);
-  rc = nntp_fetch_lines(&nntp_data, buf, sizeof(buf), msg, nntp_add_group, nserv);
-  if (rc)
-  {
-    if (rc > 0)
-    {
-      mutt_error("NEWGROUPS: %s", buf);
-    }
-    return -1;
-  }
-
-  /* new groups found */
-  rc = 0;
-  if (nserv->groups_num != i)
+  int ret = check_mailbox(ctx);
+  if (ret == 0)
   {
-    int groups_num = i;
-
-    nserv->newgroups_time = now;
-    for (; i < nserv->groups_num; i++)
-    {
-      struct NntpData *data = nserv->groups_list[i];
-      data->new = true;
-    }
-
-    /* loading descriptions */
-    if (NntpLoadDescription)
-    {
-      unsigned int count = 0;
-      struct Progress progress;
-
-      mutt_progress_init(&progress, _("Loading descriptions..."),
-                         MUTT_PROGRESS_MSG, ReadInc, nserv->groups_num - i);
-      for (i = groups_num; i < nserv->groups_num; i++)
-      {
-        struct NntpData *data = nserv->groups_list[i];
-
-        if (get_description(data, NULL, NULL) < 0)
-          return -1;
-        mutt_progress_update(&progress, ++count, -1);
-      }
-    }
-    update_active = true;
-    rc = 1;
+    struct NntpData *nntp_data = ctx->mailbox->data;
+    struct NntpServer *nserv = nntp_data->nserv;
+    nntp_newsrc_close(nserv);
   }
-  if (update_active)
-    nntp_active_save_cache(nserv);
-  mutt_clear_error();
-  return rc;
+  return ret;
 }
 
 /**
- * nntp_check_msgid - Fetch article by Message-ID
- * @param ctx   Mailbox
- * @param msgid Message ID
- * @retval  0 Success
- * @retval  1 No such article
- * @retval -1 Error
+ * nntp_mbox_sync - Implements MxOps::mbox_sync()
+ *
+ * @note May also return values from check_mailbox()
  */
-int nntp_check_msgid(struct Context *ctx, const char *msgid)
+static int nntp_mbox_sync(struct Context *ctx, int *index_hint)
 {
   struct NntpData *nntp_data = ctx->mailbox->data;
-  char buf[LONG_STRING];
-
-  FILE *fp = mutt_file_mkstemp();
-  if (!fp)
-  {
-    mutt_perror(_("Can't create temporary file"));
-    return -1;
-  }
+  int rc;
+#ifdef USE_HCACHE
+  header_cache_t *hc = NULL;
+#endif
 
-  snprintf(buf, sizeof(buf), "HEAD %s\r\n", msgid);
-  int rc = nntp_fetch_lines(nntp_data, buf, sizeof(buf), NULL, fetch_tempfile, fp);
+  /* check for new articles */
+  nntp_data->nserv->check_time = 0;
+  rc = check_mailbox(ctx);
   if (rc)
+    return rc;
+
+#ifdef USE_HCACHE
+  nntp_data->last_cached = 0;
+  hc = nntp_hcache_open(nntp_data);
+#endif
+
+  for (int i = 0; i < ctx->mailbox->msg_count; i++)
   {
-    mutt_file_fclose(&fp);
-    if (rc < 0)
-      return -1;
-    if (mutt_str_strncmp("430", buf, 3) == 0)
-      return 1;
-    mutt_error("HEAD: %s", buf);
-    return -1;
-  }
+    struct Header *hdr = ctx->mailbox->hdrs[i];
+    char buf[16];
 
-  /* parse header */
-  if (ctx->mailbox->msg_count == ctx->mailbox->hdrmax)
-    mx_alloc_memory(ctx->mailbox);
-  ctx->mailbox->hdrs[ctx->mailbox->msg_count] = mutt_header_new();
-  struct Header *hdr = ctx->mailbox->hdrs[ctx->mailbox->msg_count];
-  hdr->data = mutt_mem_calloc(1, sizeof(struct NntpHeaderData));
-  hdr->env = mutt_rfc822_read_header(fp, hdr, false, false);
-  mutt_file_fclose(&fp);
+    snprintf(buf, sizeof(buf), "%d", NHDR(hdr)->article_num);
+    if (nntp_data->bcache && hdr->deleted)
+    {
+      mutt_debug(2, "mutt_bcache_del %s\n", buf);
+      mutt_bcache_del(nntp_data->bcache, buf);
+    }
 
-  /* get article number */
-  if (hdr->env->xref)
-    nntp_parse_xref(ctx->mailbox, hdr);
-  else
-  {
-    snprintf(buf, sizeof(buf), "STAT %s\r\n", msgid);
-    if (nntp_query(nntp_data, buf, sizeof(buf)) < 0)
+#ifdef USE_HCACHE
+    if (hc && (hdr->changed || hdr->deleted))
     {
-      mutt_header_free(&hdr);
-      return -1;
+      if (hdr->deleted && !hdr->read)
+        nntp_data->unread--;
+      mutt_debug(2, "mutt_hcache_store %s\n", buf);
+      mutt_hcache_store(hc, buf, strlen(buf), hdr, 0);
     }
-    sscanf(buf + 4, ANUM, &NHDR(hdr)->article_num);
+#endif
   }
 
-  /* reset flags */
-  hdr->read = false;
-  hdr->old = false;
-  hdr->deleted = false;
-  hdr->changed = true;
-  hdr->received = hdr->date_sent;
-  hdr->index = ctx->mailbox->msg_count++;
-  mx_update_context(ctx, 1);
+#ifdef USE_HCACHE
+  if (hc)
+  {
+    mutt_hcache_close(hc);
+    nntp_data->last_cached = nntp_data->last_loaded;
+  }
+#endif
+
+  /* save .newsrc entries */
+  nntp_newsrc_gen_entries(ctx);
+  nntp_newsrc_update(nntp_data->nserv);
+  nntp_newsrc_close(nntp_data->nserv);
   return 0;
 }
 
 /**
- * struct ChildCtx - Keep track of the children of an article
- */
-struct ChildCtx
-{
-  struct Mailbox *mailbox;
-  unsigned int num;
-  unsigned int max;
-  anum_t *child;
-};
-
-/**
- * fetch_children - Parse XPAT line
- * @param line String to parse
- * @param data ChildCtx
+ * nntp_mbox_close - Implements MxOps::mbox_close()
  * @retval 0 Always
  */
-static int fetch_children(char *line, void *data)
+static int nntp_mbox_close(struct Context *ctx)
 {
-  struct ChildCtx *cc = data;
-  anum_t anum;
+  struct NntpData *nntp_data = ctx->mailbox->data, *nntp_tmp = NULL;
 
-  if (!line || sscanf(line, ANUM, &anum) != 1)
+  if (!nntp_data)
     return 0;
-  for (unsigned int i = 0; i < cc->mailbox->msg_count; i++)
-    if (NHDR(cc->mailbox->hdrs[i])->article_num == anum)
-      return 0;
-  if (cc->num >= cc->max)
-  {
-    cc->max *= 2;
-    mutt_mem_realloc(&cc->child, sizeof(anum_t) * cc->max);
-  }
-  cc->child[cc->num++] = anum;
+
+  nntp_data->unread = ctx->mailbox->msg_unread;
+
+  nntp_acache_free(nntp_data);
+  if (!nntp_data->nserv || !nntp_data->nserv->groups_hash || !nntp_data->group)
+    return 0;
+
+  nntp_tmp = mutt_hash_find(nntp_data->nserv->groups_hash, nntp_data->group);
+  if (!nntp_tmp || nntp_tmp != nntp_data)
+    nntp_data_free(nntp_data);
   return 0;
 }
 
 /**
- * nntp_check_children - Fetch children of article with the Message-ID
- * @param ctx   Mailbox
- * @param msgid Message ID to find
- * @retval  0 Success
- * @retval -1 Failure
+ * nntp_msg_open - Implements MxOps::msg_open()
  */
-int nntp_check_children(struct Context *ctx, const char *msgid)
+static int nntp_msg_open(struct Context *ctx, struct Message *msg, int msgno)
 {
   struct NntpData *nntp_data = ctx->mailbox->data;
-  struct ChildCtx cc;
-  char buf[STRING];
-  int rc;
-  bool quiet;
-  void *hc = NULL;
+  struct Header *hdr = ctx->mailbox->hdrs[msgno];
+  char article[16];
 
-  if (!nntp_data || !nntp_data->nserv)
-    return -1;
-  if (nntp_data->first_message > nntp_data->last_loaded)
-    return 0;
+  /* try to get article from cache */
+  struct NntpAcache *acache = &nntp_data->acache[hdr->index % NNTP_ACACHE_LEN];
+  if (acache->path)
+  {
+    if (acache->index == hdr->index)
+    {
+      msg->fp = mutt_file_fopen(acache->path, "r");
+      if (msg->fp)
+        return 0;
+    }
+    /* clear previous entry */
+    else
+    {
+      unlink(acache->path);
+      FREE(&acache->path);
+    }
+  }
+  snprintf(article, sizeof(article), "%d", NHDR(hdr)->article_num);
+  msg->fp = mutt_bcache_get(nntp_data->bcache, article);
+  if (msg->fp)
+  {
+    if (NHDR(hdr)->parsed)
+      return 0;
+  }
+  else
+  {
+    char buf[PATH_MAX];
+    /* don't try to fetch article from removed newsgroup */
+    if (nntp_data->deleted)
+      return -1;
 
-  /* init context */
-  cc.mailbox = ctx->mailbox;
-  cc.num = 0;
-  cc.max = 10;
-  cc.child = mutt_mem_malloc(sizeof(anum_t) * cc.max);
+    /* create new cache file */
+    const char *fetch_msg = _("Fetching message...");
+    mutt_message(fetch_msg);
+    msg->fp = mutt_bcache_put(nntp_data->bcache, article);
+    if (!msg->fp)
+    {
+      mutt_mktemp(buf, sizeof(buf));
+      acache->path = mutt_str_strdup(buf);
+      acache->index = hdr->index;
+      msg->fp = mutt_file_fopen(acache->path, "w+");
+      if (!msg->fp)
+      {
+        mutt_perror(acache->path);
+        unlink(acache->path);
+        FREE(&acache->path);
+        return -1;
+      }
+    }
 
-  /* fetch numbers of child messages */
-  snprintf(buf, sizeof(buf), "XPAT References %u-%u *%s*\r\n",
-           nntp_data->first_message, nntp_data->last_loaded, msgid);
-  rc = nntp_fetch_lines(nntp_data, buf, sizeof(buf), NULL, fetch_children, &cc);
-  if (rc)
-  {
-    FREE(&cc.child);
-    if (rc > 0)
+    /* fetch message to cache file */
+    snprintf(buf, sizeof(buf), "ARTICLE %s\r\n",
+             NHDR(hdr)->article_num ? article : hdr->env->message_id);
+    const int rc = nntp_fetch_lines(nntp_data, buf, sizeof(buf), fetch_msg,
+                                    fetch_tempfile, msg->fp);
+    if (rc)
     {
-      if (mutt_str_strncmp("500", buf, 3) != 0)
-        mutt_error("XPAT: %s", buf);
-      else
+      mutt_file_fclose(&msg->fp);
+      if (acache->path)
       {
-        mutt_error(_("Unable to find child articles because server does not "
-                     "support XPAT command"));
+        unlink(acache->path);
+        FREE(&acache->path);
+      }
+      if (rc > 0)
+      {
+        if (mutt_str_strncmp(NHDR(hdr)->article_num ? "423" : "430", buf, 3) == 0)
+        {
+          mutt_error(_("Article %d not found on the server"),
+                     NHDR(hdr)->article_num ? article : hdr->env->message_id);
+        }
+        else
+          mutt_error("ARTICLE: %s", buf);
       }
+      return -1;
     }
-    return -1;
-  }
 
-  /* fetch all found messages */
-  quiet = ctx->mailbox->quiet;
-  ctx->mailbox->quiet = true;
-#ifdef USE_HCACHE
-  hc = nntp_hcache_open(nntp_data);
-#endif
-  for (int i = 0; i < cc.num; i++)
-  {
-    rc = nntp_fetch_headers(ctx, hc, cc.child[i], cc.child[i], 1);
-    if (rc < 0)
-      break;
+    if (!acache->path)
+      mutt_bcache_commit(nntp_data->bcache, article);
   }
-#ifdef USE_HCACHE
-  mutt_hcache_close(hc);
-#endif
-  ctx->mailbox->quiet = quiet;
-  FREE(&cc.child);
-  return (rc < 0) ? -1 : 0;
+
+  /* replace envelope with new one
+   * hash elements must be updated because pointers will be changed */
+  if (ctx->mailbox->id_hash && hdr->env->message_id)
+    mutt_hash_delete(ctx->mailbox->id_hash, hdr->env->message_id, hdr);
+  if (ctx->mailbox->subj_hash && hdr->env->real_subj)
+    mutt_hash_delete(ctx->mailbox->subj_hash, hdr->env->real_subj, hdr);
+
+  mutt_env_free(&hdr->env);
+  hdr->env = mutt_rfc822_read_header(msg->fp, hdr, false, false);
+
+  if (ctx->mailbox->id_hash && hdr->env->message_id)
+    mutt_hash_insert(ctx->mailbox->id_hash, hdr->env->message_id, hdr);
+  if (ctx->mailbox->subj_hash && hdr->env->real_subj)
+    mutt_hash_insert(ctx->mailbox->subj_hash, hdr->env->real_subj, hdr);
+
+  /* fix content length */
+  fseek(msg->fp, 0, SEEK_END);
+  hdr->content->length = ftell(msg->fp) - hdr->content->offset;
+
+  /* this is called in neomutt before the open which fetches the message,
+   * which is probably wrong, but we just call it again here to handle
+   * the problem instead of fixing it */
+  NHDR(hdr)->parsed = true;
+  mutt_parse_mime_message(ctx, hdr);
+
+  /* these would normally be updated in mx_update_context(), but the
+   * full headers aren't parsed with overview, so the information wasn't
+   * available then */
+  if (WithCrypto)
+    hdr->security = crypt_query(hdr->content);
+
+  rewind(msg->fp);
+  mutt_clear_error();
+  return 0;
 }
 
 /**
- * nntp_compare_order - Sort to mailbox order - Implements ::sort_t
+ * nntp_msg_close - Implements MxOps::msg_close()
+ *
+ * @note May also return EOF Failure, see errno
  */
-int nntp_compare_order(const void *a, const void *b)
+static int nntp_msg_close(struct Context *ctx, struct Message *msg)
 {
-  struct Header **ha = (struct Header **) a;
-  struct Header **hb = (struct Header **) b;
-
-  anum_t na = NHDR(*ha)->article_num;
-  anum_t nb = NHDR(*hb)->article_num;
-  int result = (na == nb) ? 0 : (na > nb) ? 1 : -1;
-  result = perform_auxsort(result, a, b);
-  return SORTCODE(result);
+  return mutt_file_fclose(&msg->fp);
 }
 
 /**