Bug 636285 - add support for IMAP Move RFC; r=Standard8
authorDavid Bienvenu <bienvenu@nventure.com>
Thu, 10 Mar 2011 18:53:03 +0000
changeset 7316 141c656b38ea3cca9da1caef32153c9eb9dc266a
parent 7297 407379b3fee96a95223ad20318f0d9a84a8dc60b
child 7317 dd7768eaabf2e4a0e3e2ce13b33fcda09fd9c71b
push idunknown
push userunknown
push dateunknown
reviewersStandard8
bugs636285
Bug 636285 - add support for IMAP Move RFC; r=Standard8
mailnews/imap/src/nsImapCore.h
mailnews/imap/src/nsImapProtocol.cpp
mailnews/imap/src/nsImapServerResponseParser.cpp
mailnews/imap/test/unit/test_imapMove.js
mailnews/test/fakeserver/imapd.js
mailnews/test/resources/IMAPpump.js
--- a/mailnews/imap/src/nsImapCore.h
+++ b/mailnews/imap/src/nsImapCore.h
@@ -146,17 +146,19 @@ typedef enum {
     kHasAuthMSNCapability = 0x00200000,   /* AUTH MSN extension */
     kHasStartTLSCapability = 0x00400000,   /* STARTTLS support */
     kHasAuthNoneCapability = 0x00800000, /* needs no login */
     kHasAuthGssApiCapability = 0x01000000, /* GSSAPI AUTH */
     kHasCondStoreCapability =  0x02000000, /* RFC 3551 CondStore extension */
     kHasEnableCapability    =  0x04000000, /* RFC 5161 ENABLE extension */
     kHasXListCapability    =  0x08000000,  /* XLIST extension */
     kHasCompressDeflateCapability  = 0x10000000,  /* RFC 4978 COMPRESS extension */
-    kHasAuthExternalCapability  = 0x20000000  /* RFC 2222 SASL AUTH EXTERNAL */
+    kHasAuthExternalCapability  = 0x20000000,  /* RFC 2222 SASL AUTH EXTERNAL */
+    kHasMoveCapability  = 0x40000000,  /* Proposed MOVE RFC */
+    kHasHighestModSeqCapability  = 0x80000000  /* Subset of RFC 3551 */
 } eIMAPCapabilityFlag;
 
 // this used to be part of the connection object class - maybe we should move it into 
 // something similar
 typedef enum {
     kEveryThingRFC822,
     kEveryThingRFC822Peek,
     kHeadersRFC822andUid,
--- a/mailnews/imap/src/nsImapProtocol.cpp
+++ b/mailnews/imap/src/nsImapProtocol.cpp
@@ -2871,17 +2871,18 @@ void nsImapProtocol::ProcessSelectedStat
               (ImapOnlineCopyState) ImapOnlineCopyStateType::kSuccessfulCopy :
             (ImapOnlineCopyState) ImapOnlineCopyStateType::kFailedCopy;
             if (m_imapMailFolderSink)
               m_imapMailFolderSink->OnlineCopyCompleted(this, copyState);
 
             // Don't mark msg 'Deleted' for aol servers since we already issued 'xaol-move' cmd.
             if (GetServerStateParser().LastCommandSuccessful() &&
               (m_imapAction == nsIImapUrl::nsImapOnlineMove) &&
-              !GetServerStateParser().ServerIsAOLServer())
+              !(GetServerStateParser().ServerIsAOLServer() ||
+                GetServerStateParser().GetCapabilityFlag() & kHasMoveCapability))
             {
               Store(messageIdString, "+FLAGS (\\Deleted \\Seen)",
                 bMessageIdsAreUids);
               PRBool storeSuccessful = GetServerStateParser().LastCommandSuccessful();
 
               if (gExpungeAfterDelete && storeSuccessful)
                 Expunge();
 
@@ -7785,16 +7786,19 @@ void nsImapProtocol::Copy(const char * m
     IncrementCommandTagNumber();
     nsCAutoString protocolString(GetServerCommandTag());
     if (idsAreUid)
       protocolString.Append(" uid");
     // If it's a MOVE operation on aol servers then use 'xaol-move' cmd.
     if ((m_imapAction == nsIImapUrl::nsImapOnlineMove) &&
         GetServerStateParser().ServerIsAOLServer())
       protocolString.Append(" xaol-move ");
+    else if ((m_imapAction == nsIImapUrl::nsImapOnlineMove) &&
+             GetServerStateParser().GetCapabilityFlag() & kHasMoveCapability)
+      protocolString.Append(" move ");
     else
       protocolString.Append(" copy ");
 
 
     protocolString.Append(idString);
     protocolString.Append(" \"");
     protocolString.Append(escapedDestination);
     protocolString.Append("\"" CRLF);
--- a/mailnews/imap/src/nsImapServerResponseParser.cpp
+++ b/mailnews/imap/src/nsImapServerResponseParser.cpp
@@ -2261,16 +2261,20 @@ void nsImapServerResponseParser::capabil
       else if (token.Equals("CONDSTORE", nsCaseInsensitiveCStringComparator()))
         fCapabilityFlag |= kHasCondStoreCapability;
       else if (token.Equals("ENABLE", nsCaseInsensitiveCStringComparator()))
         fCapabilityFlag |= kHasEnableCapability;
       else if (token.Equals("XLIST", nsCaseInsensitiveCStringComparator()))
         fCapabilityFlag |= kHasXListCapability;
       else if (token.Equals("COMPRESS=DEFLATE", nsCaseInsensitiveCStringComparator()))
         fCapabilityFlag |= kHasCompressDeflateCapability;
+      else if (token.Equals("MOVE", nsCaseInsensitiveCStringComparator()))
+        fCapabilityFlag |= kHasMoveCapability;
+      else if (token.Equals("HIGHESTMODSEQ", nsCaseInsensitiveCStringComparator()))
+        fCapabilityFlag |= kHasHighestModSeqCapability;
     }
   } while (fNextToken && endToken < 0 && !fAtEndOfLine && ContinueParse());
 
   if (fHostSessionList)
     fHostSessionList->SetCapabilityForHost(
     fServerConnection.GetImapServerKey(), 
     fCapabilityFlag);
   nsImapProtocol *navCon = &fServerConnection;
new file mode 100644
--- /dev/null
+++ b/mailnews/imap/test/unit/test_imapMove.js
@@ -0,0 +1,138 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   David Bienvenu <bienvenu@mozillamessaging.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// This tests that we use IMAP move if the IMAP server supports it.
+
+var gMessages = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+var gCopyService = Cc["@mozilla.org/messenger/messagecopyservice;1"]
+                .getService(Ci.nsIMsgCopyService);
+const ioService = Cc["@mozilla.org/network/io-service;1"]
+                     .getService(Ci.nsIIOService);
+
+
+load("../../../resources/logHelper.js");
+load("../../../resources/mailTestUtils.js");
+load("../../../resources/asyncTestUtils.js");
+load("../../../resources/messageGenerator.js");
+
+// IMAP pump
+load("../../../resources/IMAPpump.js");
+setupIMAPPump("CUSTOM1");
+
+var gIMAPInbox, gFolder1;
+
+var tests = [
+  startTest,
+  doMove,
+  testMove,
+  endTest
+];
+
+function startTest()
+{
+  // Add folder listeners that will capture async events
+  const nsIMFNService = Ci.nsIMsgFolderNotificationService;
+  let MFNService = Cc["@mozilla.org/messenger/msgnotificationservice;1"]
+                     .getService(nsIMFNService);
+  MFNService.addListener(mfnListener, nsIMFNService.folderAdded);
+
+  gIMAPIncomingServer.rootFolder.createSubfolder("folder 1", null);
+  yield false;
+  let messages = [];
+  let gMessageGenerator = new MessageGenerator();
+  messages = messages.concat(gMessageGenerator.makeMessage());
+  gSynthMessage = messages[0];
+  let dataUri = ioService.newURI("data:text/plain;base64," +
+                  btoa(messages[0].toMessageString()),
+                  null, null);
+  let imapMsg = new imapMessage(dataUri.spec, gIMAPMailbox.uidnext++, []);
+  gIMAPMailbox.addMessage(imapMsg);
+
+  gIMAPInbox.updateFolderWithListener(null, UrlListener);
+  yield false;
+}
+
+function doMove() {
+  let rootFolder = gIMAPIncomingServer.rootFolder;
+  gFolder1 = rootFolder.getChildNamed("folder 1")
+               .QueryInterface(Components.interfaces.nsIMsgImapMailFolder);
+  let msg = gIMAPInbox.msgDatabase.GetMsgHdrForKey(gIMAPMailbox.uidnext - 1);
+  gMessages.appendElement(msg, false);
+  gIMAPServer._test = true;
+  gCopyService.CopyMessages(gIMAPInbox, gMessages, gFolder1, true,
+                            asyncCopyListener, null, false);
+  gIMAPServer.performTest("UID MOVE");
+  yield false;
+}
+
+function testMove() {
+  do_check_eq(gIMAPInbox.getTotalMessages(false), 0);
+  gFolder1.updateFolderWithListener(null, UrlListener);
+  yield false;
+  do_check_eq(gFolder1.getTotalMessages(false), 1);
+  yield true;
+}
+
+var UrlListener = {
+  OnStartRunningUrl: function _OnStartRunningUrl(aUrl) {
+  },
+  OnStopRunningUrl: function _OnStopRunningUrl(aUrl, aExitCode) {
+    async_driver();
+  }
+};
+
+var mfnListener =
+{
+  folderAdded: function folderAdded(aFolder)
+  {
+    // we are only using async yield on the target folder add
+    if (aFolder.name == "folder 1")
+      async_driver();
+  },
+};
+
+function run_test()
+{
+  async_run_tests(tests);
+}
+
+
+function endTest()
+{
+  teardownIMAPPump();
+  do_test_finished();
+}
--- a/mailnews/test/fakeserver/imapd.js
+++ b/mailnews/test/fakeserver/imapd.js
@@ -651,16 +651,17 @@ function IMAP_RFC3501_handler(daemon) {
   this.resetTest();
 }
 IMAP_RFC3501_handler.prototype = {
 
   kUsername : "user",
   kPassword : "password",
   kAuthSchemes : [], // Added by RFC2195 extension. Test may modify as needed.
   kCapabilities : [/*"LOGINDISABLED", "STARTTLS",*/], // Test may modify as needed.
+  kUidCommands : ["FETCH", "STORE", "SEARCH", "COPY"],
 
   resetTest : function() {
     this._state = IMAP_STATE_NOT_AUTHED;
     this._multiline = false;
     this._nextAuthFunction = undefined; // should be in RFC2195_ext, but too lazy
   },
   onStartup : function () {
     return "* OK IMAP4rev1 Fakeserver started up";
@@ -757,16 +758,20 @@ IMAP_RFC3501_handler.prototype = {
       // Are we allowed to execute this command?
       if (this._enabledCommands[this._state].indexOf(command) == -1)
         return this._tag + " BAD illegal command for current state " + this._state;
 
       try {
         // Format the arguments nicely
         args = this._treatArgs(args, command);
 
+      // UID command by itself is not useful for PerformTest
+      if (command == "UID")
+        this._lastCommand += " " + args[0];
+
         // Finally, run the thing
         var response = this[command](args);
       } catch (e if typeof e == "string") {
         var response = e;
       }
     } else {
       var response = "BAD " + command  + " not implemented";
     }
@@ -1286,18 +1291,19 @@ IMAP_RFC3501_handler.prototype = {
         if (alarm.getTime() - startingMSeconds > this.copySleep)
           break;
       }
     }
     return "OK COPY completed";
   },
   UID : function (args) {
     var name = args.shift();
-    if (["FETCH", "STORE", "SEARCH", "COPY"].indexOf(name) == -1)
+    if (this.kUidCommands.indexOf(name) == -1)
       return "BAD illegal command " + name;
+
     args = this._treatArgs(args, name);
     return this[name](args, true);
   },
 
   postCommand : function (reader) {
     if (this.closing) {
       this.closing = false;
       reader.closeSocket();
@@ -1576,23 +1582,26 @@ IMAP_RFC3501_handler.prototype = {
 // An extension is defined as follows: it is an object (not a function and    //
 // prototype pair!). This object is "mixed" into the handler via the helper   //
 // function mixinExtension, which applies appropriate magic to make the       //
 // handler compliant to the extension. Functions are added untransformed, but //
 // both arrays and objects are handled by appending the values onto the       //
 // original state of the handler. Semantics apply as for the base itself.     //
 ////////////////////////////////////////////////////////////////////////////////
 
+// Note that UIDPLUS (RFC4315) should be mixed in last (or at least after the
+// MOVE extension) because it changes behavior of that extension.
 var configurations = {
   Cyrus: ["RFC2342", "RFC2195"],
   UW: ["RFC2342", "RFC2195"],
   Dovecot: ["RFC2195"],
   Zimbra: ["RFC2342", "RFC2195"],
   Exchange: ["RFC2342", "RFC2195"],
   LEMONADE: ["RFC2342", "RFC2195"],
+  CUSTOM1: ["RFCMOVE", "RFC4315"],
 };
 
 function mixinExtension(handler, extension) {
   if (extension.preload)
     extension.preload(handler);
 
   for (var property in extension) {
     if (property == 'preload')
@@ -1642,22 +1651,59 @@ var IMAP_RFC2342_extension = {
     return response;
   },
   kCapabilities : ["NAMESPACE"],
   _argFormat : { NAMESPACE : [] },
   // Enabled in AUTHED and SELECTED states
   _enabledCommands : { 1 : ["NAMESPACE"], 2 : ["NAMESPACE"] }
 };
 
+// Proposed MOVE extension (imapPump requires the string "RFC").
+var IMAP_RFCMOVE_extension = {
+  MOVE: function (args, uid) {
+    let messages = this._parseSequenceSet(args[0], uid);
+
+    let dest = this._daemon.getMailbox(args[1]);
+    if (!dest)
+      return "NO [TRYCREATE] what mailbox?";
+
+    for each (var message in messages) {
+      let newMessage = new imapMessage(message._URI, dest.uidnext++,
+                                       message.flags);
+      newMessage.recent = false;
+      dest.addMessage(newMessage);
+    }
+    let mailbox = this._selectedMailbox;
+    let response = "";
+    for (let i = messages.length - 1; i >= 0; i--) {
+      let msgIndex = mailbox._messages.indexOf(messages[i]);
+      if (msgIndex != -1) {
+        response += "* " + (msgIndex + 1) + " EXPUNGE\0";
+        mailbox._messages.splice(msgIndex, 1);
+      }
+    }
+    if (response.length > 0)
+      delete mailbox.__highestuid;
+
+    return response + "OK MOVE completed";
+  },
+  kCapabilities: ["MOVE"],
+  kUidCommands: ["MOVE"],
+  _argFormat: { MOVE: ["number", "mailbox"] },
+  // Enabled in SELECTED state
+  _enabledCommands: { 2: ["MOVE"] }
+};
+
 // RFC 4315: UIDPLUS
 var IMAP_RFC4315_extension = {
   preload: function (toBeThis) {
     toBeThis._preRFC4315UID = toBeThis.UID;
     toBeThis._preRFC4315APPEND = toBeThis.APPEND;
     toBeThis._preRFC4315COPY = toBeThis.COPY;
+    toBeThis._preRFC4315MOVE = toBeThis.MOVE;
   },
   UID: function (args) {
     // XXX: UID EXPUNGE is not supported.
     return this._preRFC4315UID(args);
   },
   APPEND: function (args) {
     let response = this._preRFC4315APPEND(args);
     if (response.indexOf("OK") == 0) {
@@ -1674,16 +1720,28 @@ var IMAP_RFC4315_extension = {
     let response = this._preRFC4315COPY(args);
     if (response.indexOf("OK") == 0) {
       let last = mailbox.uidnext - 1;
       response = "OK [COPYUID " + first + ":" + last + "]" +
                   response.substring(2);
     }
     return response;
   },
+  MOVE: function (args) {
+    let mailbox = this._daemon.getMailbox(args[1]);
+    if (mailbox)
+      var first = mailbox.uidnext;
+    let response = this._preRFC4315MOVE(args);
+    if (response.indexOf("OK") == 0) {
+      let last = mailbox.uidnext - 1;
+      response = "OK [COPYUID " + first + ":" + last + "]" +
+                  response.substring(2);
+    }
+    return response;
+  },
   kCapabilities: ["UIDPLUS"]
 };
 
 
 /**
  * This implements AUTH schemes. Could be moved into RFC3501 actually.
  * The test can en-/disable auth schemes by modifying kAuthSchemes.
  */
--- a/mailnews/test/resources/IMAPpump.js
+++ b/mailnews/test/resources/IMAPpump.js
@@ -66,17 +66,17 @@ load(gDEPTH + "mailnews/resources/mailTe
 
 // define globals
 var gIMAPDaemon;         // the imap fake server daemon
 var gIMAPServer;         // the imap fake server
 var gIMAPIncomingServer; // nsIMsgIncomingServer for the imap server
 var gIMAPInbox;          // nsIMsgFolder/nsIMsgImapMailFolder for imap inbox
 var gIMAPMailbox;        // imap fake server mailbox
 
-function setupIMAPPump()
+function setupIMAPPump(extensions)
 {
 
   // These are copied from imap's head_server.js to here so we can run
   //   this from any directory.
 
   const IMAP_PORT = 1024 + 143;
 
   function makeServer(daemon, infoString) {
@@ -101,17 +101,17 @@ function setupIMAPPump()
     let server = create_incoming_server("imap", IMAP_PORT, "user", "password");
     server.QueryInterface(Ci.nsIImapIncomingServer);
     return server;
   }
 
   // end copy from head_server.js
 
   gIMAPDaemon = new imapDaemon();
-  gIMAPServer = makeServer(gIMAPDaemon, "");
+  gIMAPServer = makeServer(gIMAPDaemon, extensions);
 
   gIMAPIncomingServer = createLocalIMAPServer();
 
   if (!this.gLocalInboxFolder)
     loadLocalMailAccount();
 
   // We need an identity so that updateFolder doesn't fail
   let acctMgr = Cc["@mozilla.org/messenger/account-manager;1"]