Bug 547027 - Create a framework for MAPI tests. r=standard8,jorgk
authorJoshua Cranmer <Pidgeot18@gmail.com>
Sun, 10 Feb 2019 21:16:26 +0100
changeset 34506 67e20f7993b0f4ce493efca29b24edbe8448e5dc
parent 34505 669f00f5e599e95a59a6aa6c23231b788d75e3e7
child 34507 4831d30eb0f88ed6462650fa2b00d1cad44c9b5c
push id390
push userclokep@gmail.com
push dateMon, 20 May 2019 17:04:42 +0000
reviewersstandard8, jorgk
bugs547027
Bug 547027 - Create a framework for MAPI tests. r=standard8,jorgk
mailnews/mapi/mapiDll/MapiDll.cpp
mailnews/mapi/mapihook/src/msgMapiHook.cpp
mailnews/mapi/test/moz.build
mailnews/mapi/test/unit/.eslintrc.js
mailnews/mapi/test/unit/head_mapi.js
mailnews/mapi/test/unit/tail_mapi.js
mailnews/mapi/test/unit/test_mapisendmail.js
mailnews/mapi/test/unit/xpcshell.ini
mailnews/moz.build
--- a/mailnews/mapi/mapiDll/MapiDll.cpp
+++ b/mailnews/mapi/mapiDll/MapiDll.cpp
@@ -162,22 +162,17 @@ ULONG FAR PASCAL MAPISendMail (LHANDLE l
     if (lpMessage->nFileCount > MAX_FILES)
         return MAPI_E_TOO_MANY_FILES ;
 
     if ( (!(flFlags & MAPI_DIALOG)) && (lpMessage->lpRecips == NULL) )
         return MAPI_E_UNKNOWN_RECIPIENT ;
 
     if (!lhSession || pNsMapi->IsValidSession(lhSession) != S_OK)
     {
-        FLAGS LoginFlag = 0;
-        if ( (flFlags & MAPI_LOGON_UI) && (flFlags & MAPI_NEW_SESSION) )
-            LoginFlag = MAPI_LOGON_UI | MAPI_NEW_SESSION ;
-        else if (flFlags & MAPI_LOGON_UI)
-            LoginFlag = MAPI_LOGON_UI ;
-
+        FLAGS LoginFlag = flFlags & (MAPI_LOGON_UI | MAPI_NEW_SESSION);
         hr = MAPILogon(ulUIParam, nullptr, nullptr, LoginFlag, 0, &lhSession);
         if (hr != SUCCESS_SUCCESS)
             return MAPI_E_LOGIN_FAILURE ;
         bTempSession = TRUE ;
     }
 
     hr = pNsMapi->SendMail(lhSession, lpMessage, flFlags, ulReserved);
 
@@ -208,22 +203,17 @@ ULONG FAR PASCAL MAPISendMailW(LHANDLE l
     if (lpMessage->nFileCount > MAX_FILES)
       return MAPI_E_TOO_MANY_FILES;
 
     if ((!(flFlags & MAPI_DIALOG)) && (lpMessage->lpRecips == nullptr))
       return MAPI_E_UNKNOWN_RECIPIENT;
 
     if (!lhSession || pNsMapi->IsValidSession(lhSession) != S_OK)
     {
-      FLAGS LoginFlag = 0;
-      if ((flFlags & MAPI_LOGON_UI) && (flFlags & MAPI_NEW_SESSION))
-        LoginFlag = MAPI_LOGON_UI | MAPI_NEW_SESSION;
-      else if (flFlags & MAPI_LOGON_UI)
-        LoginFlag = MAPI_LOGON_UI;
-
+      FLAGS LoginFlag = flFlags & (MAPI_LOGON_UI | MAPI_NEW_SESSION);
       hr = MAPILogon(ulUIParam, nullptr, nullptr, LoginFlag, 0, &lhSession);
       if (hr != SUCCESS_SUCCESS)
         return MAPI_E_LOGIN_FAILURE;
       bTempSession = TRUE;
     }
 
     hr = pNsMapi->SendMailW(lhSession, lpMessage, flFlags, ulReserved);
 
--- a/mailnews/mapi/mapihook/src/msgMapiHook.cpp
+++ b/mailnews/mapi/mapihook/src/msgMapiHook.cpp
@@ -31,83 +31,108 @@
 #include "nsDirectoryServiceUtils.h"
 #include "msgMapi.h"
 #include "msgMapiHook.h"
 #include "msgMapiSupport.h"
 #include "msgMapiMain.h"
 #include "nsThreadUtils.h"
 #include "nsMsgUtils.h"
 #include "nsNetUtil.h"
+#include "mozilla/Monitor.h"
 #include "mozilla/Services.h"
 #include "nsIArray.h"
 #include "nsArrayUtils.h"
 #include "nsEmbedCID.h"
 #include "mozilla/Logging.h"
 
 extern mozilla::LazyLogModule MAPI; // defined in msgMapiImp.cpp
 
-class nsMAPISendListener : public nsIMsgSendListener
+class MAPISendListener : public nsIMsgSendListener,
+                         public mozilla::Monitor
 {
 public:
+  MAPISendListener()
+  : Monitor("MAPISendListener monitor"),
+    m_done(false) {}
 
     // nsISupports interface
     NS_DECL_THREADSAFE_ISUPPORTS
 
     /* void OnStartSending (in string aMsgID, in uint32_t aMsgSize); */
     NS_IMETHOD OnStartSending(const char *aMsgID, uint32_t aMsgSize) { return NS_OK; }
 
     /* void OnProgress (in string aMsgID, in uint32_t aProgress, in uint32_t aProgressMax); */
     NS_IMETHOD OnProgress(const char *aMsgID, uint32_t aProgress, uint32_t aProgressMax) { return NS_OK;}
 
     /* void OnStatus (in string aMsgID, in wstring aMsg); */
     NS_IMETHOD OnStatus(const char *aMsgID, const char16_t *aMsg) { return NS_OK;}
 
     /* void OnStopSending (in string aMsgID, in nsresult aStatus, in wstring aMsg, in nsIFile returnFile); */
     NS_IMETHOD OnStopSending(const char *aMsgID, nsresult aStatus, const char16_t *aMsg,
                            nsIFile *returnFile) {
-        PR_CEnterMonitor(this);
-        PR_CNotifyAll(this);
         m_done = true;
-        PR_CExitMonitor(this);
-        return NS_OK ;
+        NotifyAll();
+        return NS_OK;
     }
 
     /* void OnSendNotPerformed */
     NS_IMETHOD OnSendNotPerformed(const char *aMsgID, nsresult aStatus)
     {
       return OnStopSending(aMsgID, aStatus, nullptr, nullptr) ;
     }
 
     /* void OnGetDraftFolderURI (); */
     NS_IMETHOD OnGetDraftFolderURI(const char *aFolderURI) {return NS_OK;}
 
-    static nsresult CreateMAPISendListener( nsIMsgSendListener **ppListener);
-
     bool IsDone() { return m_done ; }
 
-protected :
-    nsMAPISendListener() {
-        m_done = false;
-    }
-
+private:
     bool            m_done;
-private:
-    virtual ~nsMAPISendListener() { }
-
+    virtual ~MAPISendListener() { }
 };
 
-
-NS_IMPL_ISUPPORTS(nsMAPISendListener, nsIMsgSendListener)
+  /// Helper for setting up the hidden window for blind MAPI.
+  class MOZ_STACK_CLASS AutoHiddenWindow {
+  public:
+    explicit AutoHiddenWindow(nsresult &rv)
+      : mAppService(do_GetService("@mozilla.org/appshell/appShellService;1"))
+    {
+      mCreatedHiddenWindow = false;
+      rv = mAppService->GetHiddenDOMWindow(getter_AddRefs(mHiddenWindow));
+      if (rv == NS_ERROR_FAILURE)
+      {
+        // Try to get a hidden window. If it doesn't exist, create a hidden
+        // window for us to use.
+        rv = mAppService->CreateHiddenWindow();
+        NS_ENSURE_SUCCESS_VOID(rv);
+        mCreatedHiddenWindow = true;
+        rv = mAppService->GetHiddenDOMWindow(getter_AddRefs(mHiddenWindow));
+      }
+      NS_ENSURE_SUCCESS_VOID(rv);
+    }
+    ~AutoHiddenWindow()
+    {
+      if (mCreatedHiddenWindow)
+        mAppService->DestroyHiddenWindow();
+    }
+    mozIDOMWindowProxy *operator->()
+    {
+      return mHiddenWindow;
+    }
+    operator mozIDOMWindowProxy *()
+    {
+      return mHiddenWindow;
+    }
+  private:
+    nsCOMPtr<nsIAppShellService> mAppService;
+    nsCOMPtr<mozIDOMWindowProxy> mHiddenWindow;
+    bool mCreatedHiddenWindow;
+  };
 
-nsresult nsMAPISendListener::CreateMAPISendListener( nsIMsgSendListener **ppListener)
-{
-    NS_ENSURE_ARG_POINTER(ppListener) ;
-    NS_ADDREF(*ppListener = new nsMAPISendListener());
-    return NS_OK;
-}
+NS_IMPL_ISUPPORTS(MAPISendListener, nsIMsgSendListener)
 
 bool nsMapiHook::isMapiService = false;
 
 void nsMapiHook::CleanUp()
 {
     // This routine will be fully implemented in future
     // to cleanup mapi related stuff inside mozilla code.
 }
@@ -257,30 +282,25 @@ nsMapiHook::IsBlindSendAllowed()
     prefBranch->SetBoolPref(PREF_MAPI_WARN_PRIOR_TO_BLIND_SEND, false);
 
   return okayToContinue;
 }
 
 // this is used for Send without UI
 nsresult nsMapiHook::BlindSendMail (unsigned long aSession, nsIMsgCompFields * aCompFields)
 {
-  nsresult rv = NS_OK ;
+  nsresult rv = NS_OK;
 
   if (!IsBlindSendAllowed())
     return NS_ERROR_FAILURE;
 
-  /** create nsIMsgComposeParams obj and other fields to populate it **/
+  // Get a hidden window to use for compose.
+  AutoHiddenWindow hiddenWindow(rv);
+  NS_ENSURE_SUCCESS(rv, rv);
 
-  nsCOMPtr<mozIDOMWindowProxy> hiddenWindow;
-  // get parent window
-  nsCOMPtr<nsIAppShellService> appService = do_GetService( "@mozilla.org/appshell/appShellService;1", &rv);
-  if (NS_FAILED(rv)|| (!appService) ) return rv ;
-
-  rv = appService->GetHiddenDOMWindow(getter_AddRefs(hiddenWindow));
-  if ( NS_FAILED(rv) ) return rv ;
   // smtp password and Logged in used IdKey from MapiConfig (session obj)
   nsMAPIConfiguration * pMapiConfig = nsMAPIConfiguration::GetMAPIConfiguration() ;
   if (!pMapiConfig) return NS_ERROR_FAILURE ;  // get the singelton obj
   char16_t * password = pMapiConfig->GetPassword(aSession) ;
 
   // Id key
   nsCString MsgIdKey;
   pMapiConfig->GetIdKey(aSession, MsgIdKey);
@@ -289,19 +309,17 @@ nsresult nsMapiHook::BlindSendMail (unsi
   nsCOMPtr <nsIMsgAccountManager> accountManager = do_GetService (NS_MSGACCOUNTMANAGER_CONTRACTID) ;
   if (NS_FAILED(rv) || (!accountManager) ) return rv ;
 
   nsCOMPtr <nsIMsgIdentity> pMsgId ;
   rv = accountManager->GetIdentity (MsgIdKey, getter_AddRefs(pMsgId)) ;
   if (NS_FAILED(rv) ) return rv ;
 
   // create a send listener to get back the send status
-  nsCOMPtr <nsIMsgSendListener> sendListener ;
-  rv = nsMAPISendListener::CreateMAPISendListener(getter_AddRefs(sendListener)) ;
-  if (NS_FAILED(rv) || (!sendListener) ) return rv;
+  RefPtr<MAPISendListener> sendListener = new MAPISendListener;
 
   // create the compose params object
   nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams (do_CreateInstance(NS_MSGCOMPOSEPARAMS_CONTRACTID, &rv));
   if (NS_FAILED(rv) || (!pMsgComposeParams) ) return rv ;
 
   // populate the compose params
   bool forcePlainText;
   aCompFields->GetForcePlainText(&forcePlainText);
@@ -310,43 +328,41 @@ nsresult nsMapiHook::BlindSendMail (unsi
   pMsgComposeParams->SetIdentity(pMsgId);
   pMsgComposeParams->SetComposeFields(aCompFields);
   pMsgComposeParams->SetSendListener(sendListener) ;
   if (password)
     pMsgComposeParams->SetSmtpPassword(nsDependentString(password));
 
   // create the nsIMsgCompose object to send the object
   nsCOMPtr<nsIMsgCompose> pMsgCompose (do_CreateInstance(NS_MSGCOMPOSE_CONTRACTID, &rv));
-  if (NS_FAILED(rv) || (!pMsgCompose) ) return rv ;
-
-  /** initialize nsIMsgCompose, Send the message, wait for send completion response **/
-
+  NS_ENSURE_SUCCESS(rv, rv);
   rv = pMsgCompose->Initialize(pMsgComposeParams, hiddenWindow, nullptr);
-  if (NS_FAILED(rv)) return rv ;
+  NS_ENSURE_SUCCESS(rv, rv);
 
-  // If we're in offline mode, we'll need to queue it for later. No point in trying to send it.
-  return pMsgCompose->SendMsg(WeAreOffline() ? nsIMsgSend::nsMsgQueueForLater : nsIMsgSend::nsMsgDeliverNow,
-                              pMsgId, nullptr, nullptr, nullptr);
-  if (NS_FAILED(rv)) return rv ;
+  // If we're in offline mode, we'll need to queue it for later.
+  rv = pMsgCompose->SendMsg(WeAreOffline() ? nsIMsgSend::nsMsgQueueForLater
+                                           : nsIMsgSend::nsMsgDeliverNow,
+                            pMsgId, nullptr, nullptr, nullptr);
+  NS_ENSURE_SUCCESS(rv, rv);
 
-  // assign to interface pointer from nsCOMPtr to facilitate typecast below
-  nsIMsgSendListener * pSendListener = sendListener ;
+  // We need to wait to make sure that we only return when the send is
+  // completed. If we're offline, we're not sending yet, so don't bother
+  // waiting.
+  if (WeAreOffline())
+    return NS_OK;
 
-  // we need to wait here to make sure that we return only after send is completed
-  // so we will have a event loop here which will process the events till the Send IsDone.
   nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
-  while ( !((nsMAPISendListener *) pSendListener)->IsDone() )
+  while (!sendListener->IsDone())
   {
-    PR_CEnterMonitor(pSendListener);
-    PR_CWait(pSendListener, PR_MicrosecondsToInterval(1000UL));
-    PR_CExitMonitor(pSendListener);
+    mozilla::MonitorAutoLock mal(*sendListener);
+    sendListener->Wait(mozilla::TimeDuration::FromMilliseconds(1000UL));
     NS_ProcessPendingEvents(thread);
   }
 
-  return rv ;
+  return rv;
 }
 
 nsresult nsMapiHook::HandleAttachments (nsIMsgCompFields * aCompFields, int32_t aFileCount,
                                         lpnsMapiFileDesc aFiles, bool aIsUTF8)
 {
     nsresult rv = NS_OK ;
     // Do nothing if there are no files to process.
     if (!aFiles || aFileCount <= 0)
@@ -902,19 +918,17 @@ nsresult nsMapiHook::PopulateCompFieldsF
 }
 
 // this used for Send with UI
 nsresult nsMapiHook::ShowComposerWindow (unsigned long aSession, nsIMsgCompFields * aCompFields)
 {
     nsresult rv = NS_OK ;
 
     // create a send listener to get back the send status
-    nsCOMPtr <nsIMsgSendListener> sendListener ;
-    rv = nsMAPISendListener::CreateMAPISendListener(getter_AddRefs(sendListener)) ;
-    if (NS_FAILED(rv) || (!sendListener) ) return rv ;
+    RefPtr<MAPISendListener> sendListener = new MAPISendListener;
 
     // create the compose params object
     nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams (do_CreateInstance(NS_MSGCOMPOSEPARAMS_CONTRACTID, &rv));
     if (NS_FAILED(rv) || (!pMsgComposeParams) ) return rv ;
 
     // If we found HTML, compose in HTML.
     bool forcePlainText;
     aCompFields->GetForcePlainText(&forcePlainText);
new file mode 100644
--- /dev/null
+++ b/mailnews/mapi/test/moz.build
@@ -0,0 +1,7 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
+
new file mode 100644
--- /dev/null
+++ b/mailnews/mapi/test/unit/.eslintrc.js
@@ -0,0 +1,14 @@
+"use strict";
+
+module.exports = {
+  "extends": "plugin:mozilla/xpcshell-test",
+
+  "rules": {
+    "func-names": "off",
+    "mozilla/import-headjs-globals": "error",
+    "no-unused-vars": ["error", {
+      "args": "none",
+      "vars": "all",
+    }],
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mailnews/mapi/test/unit/head_mapi.js
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
+var {ctypes} = ChromeUtils.import("resource:///modules/ctypes.jsm");
+var {localAccountUtils} = ChromeUtils.import("resource://testing-common/mailnews/localAccountUtils.js");
+
+// Ensure the profile directory is set up.
+do_get_profile();
+
+// Import fakeserver
+var {nsMailServer} = ChromeUtils.import("resource://testing-common/mailnews/maild.js");
+var {
+  smtpDaemon,
+  SMTP_RFC2821_handler,
+} = ChromeUtils.import("resource://testing-common/mailnews/smtpd.js");
+
+var SMTP_PORT = 1024 + 120;
+var POP3_PORT = 1024 + 121;
+
+// Setup the daemon and server
+function setupServerDaemon(handler) {
+  if (!handler)
+    handler = function(d) { return new SMTP_RFC2821_handler(d); };
+  let daemon = new smtpDaemon();
+  let server = new nsMailServer(handler, daemon);
+  return [daemon, server];
+}
+
+function getBasicSmtpServer() {
+  // We need to have a default account for MAPI.
+  localAccountUtils.loadLocalMailAccount();
+  let incoming = localAccountUtils.create_incoming_server("pop3", POP3_PORT,
+    "user", "password");
+  let server = localAccountUtils.create_outgoing_server(SMTP_PORT,
+    "user", "password");
+  // We also need to have a working identity, including an email address.
+  let account = MailServices.accounts.FindAccountForServer(incoming);
+  localAccountUtils.associate_servers(account, server, true);
+  let identity = account.defaultIdentity;
+  identity.email = "tinderbox@tinderbox.invalid";
+  MailServices.accounts.defaultAccount = account;
+
+  return server;
+}
+
+/**
+ * Returns a structure allowing access to all of the Simple MAPI functions.
+ * The functions do not have the MAPI prefix on the variables. Also added are
+ * the three structures needed for MAPI.
+ */
+function loadMAPILibrary() {
+  // This is a hack to load the MAPI support in the current environment, as the
+  // profile-after-change event is never sent out.
+  var gMapiSupport = Cc["@mozilla.org/mapisupport;1"]
+                       .getService(Ci.nsIObserver);
+  gMapiSupport.observe(null, "profile-after-change", null);
+  // Set some preferences to make MAPI (particularly blind MAPI, aka work
+  // without a dialog box) work properly.
+  Services.prefs.setBoolPref("mapi.blind-send.enabled", true);
+  Services.prefs.setBoolPref("mapi.blind-send.warn", false);
+
+  // The macros that are used in the definitions
+  let WINAPI = ctypes.winapi_abi;
+  let ULONG = ctypes.unsigned_long;
+  let LHANDLE = ULONG.ptr;
+  let LPSTR = ctypes.char.ptr;
+  let LPVOID = ctypes.voidptr_t;
+  let FLAGS = ctypes.unsigned_long;
+
+  // Define all of the MAPI structs we need to use.
+  let functionData = {};
+  functionData.MapiRecipDesc = new ctypes.StructType("gMapi.MapiRecipDesc", [
+    {ulReserved: ULONG},
+    {ulRecipClass: ULONG},
+    {lpszName: LPSTR},
+    {lpszAddress: LPSTR},
+    {ulEIDSize: ULONG},
+    {lpEntryID: LPVOID},
+  ]);
+  let lpMapiRecipDesc = functionData.MapiRecipDesc.ptr;
+
+  functionData.MapiFileDesc = new ctypes.StructType("gMapi.MapiFileDesc", [
+    {ulReserved: ULONG},
+    {flFlags: ULONG},
+    {nPosition: ULONG},
+    {lpszPathName: LPSTR},
+    {lpszFileName: LPSTR},
+    {lpFileType: LPVOID},
+  ]);
+  let lpMapiFileDesc = functionData.MapiFileDesc.ptr;
+
+  functionData.MapiMessage = new ctypes.StructType("gMapi.MapiMessage", [
+    {ulReserved: ULONG},
+    {lpszSubject: LPSTR},
+    {lpszNoteText: LPSTR},
+    {lpszMessageType: LPSTR},
+    {lpszDateReceived: LPSTR},
+    {lpszConversationID: LPSTR},
+    {flFlags: FLAGS},
+    {lpOriginator: lpMapiRecipDesc},
+    {nRecipCount: ULONG},
+    {lpRecips: lpMapiRecipDesc},
+    {nFileCount: ULONG},
+    {lpFiles: lpMapiFileDesc},
+  ]);
+  let lpMapiMessage = functionData.MapiMessage.ptr;
+
+  // Load the MAPI library. We're using our definition instead of the global
+  // MAPI definition.
+  let mapi = ctypes.open("mozMapi32.dll");
+
+  // Load the MAPI functions,
+  // see https://developer.mozilla.org/en-US/docs/Mozilla/js-ctypes/Using_js-ctypes/Declaring_types
+  // for details. The first three parameters of the declaration are name, API flag and output value.
+  // This is followed by input parameters.
+
+  // MAPIAddress is not supported.
+
+  functionData.DeleteMail = mapi.declare(
+    "MAPIDeleteMail", WINAPI, ULONG,
+    LHANDLE,    // lhSession
+    ULONG.ptr,  // ulUIParam
+    LPSTR,      // lpszMessageID
+    FLAGS,      // flFlags
+    ULONG);     // ulReserved
+
+  // MAPIDetails is not supported.
+
+  functionData.FindNext = mapi.declare(
+    "MAPIFindNext", WINAPI, ULONG,
+    LHANDLE,    // lhSession
+    ULONG.ptr,  // ulUIParam
+    LPSTR,      // lpszMessageType
+    LPSTR,      // lpszSeedMessageID
+    FLAGS,      // flFlags
+    ULONG,      // ulReserved
+    LPSTR);     // lpszMessageID
+
+  functionData.FreeBuffer = mapi.declare(
+    "MAPIFreeBuffer", WINAPI, ULONG,
+    LPVOID);    // pv
+
+  functionData.Logoff = mapi.declare(
+    "MAPILogoff", WINAPI, ULONG,
+    LHANDLE,    // lhSession
+    ULONG.ptr,  // ulUIParam
+    FLAGS,      // flFlags
+    ULONG);     // ulReserved
+
+  functionData.Logon = mapi.declare(
+    "MAPILogon", WINAPI, ULONG,
+    ULONG.ptr,  // ulUIParam
+    LPSTR,      // lpszProfileName
+    LPSTR,      // lpszPassword
+    FLAGS,      // flFlags
+    ULONG,      // ulReserved
+    LHANDLE.ptr); // lplhSession
+
+  functionData.ReadMail = mapi.declare(
+    "MAPIReadMail", WINAPI, ULONG,
+    LHANDLE,    // lhSession
+    ULONG.ptr,  // ulUIParam
+    LPSTR,      // lpszMessageID
+    FLAGS,      // flFlags
+    ULONG,      // ulReserved
+    lpMapiMessage.ptr); // *lppMessage
+
+  functionData.ResolveName = mapi.declare(
+    "MAPIResolveName", WINAPI, ULONG,
+    LHANDLE,    // lhSession
+    ULONG.ptr,  // ulUIParam
+    LPSTR,      // lpszName
+    FLAGS,      // flFlags
+    ULONG,      // ulReserved
+    lpMapiRecipDesc.ptr); // *lppRecip
+
+  // MAPISaveMail is not supported.
+
+  functionData.SendDocuments = mapi.declare(
+    "MAPISendDocuments", WINAPI, ULONG,
+    ULONG.ptr,  // ulUIParam
+    LPSTR,      // lpszDelimChar
+    LPSTR,      // lpszFilePaths
+    LPSTR,      // lpszFileNames
+    ULONG);     // ulReserved
+
+  functionData.SendMail = mapi.declare(
+    "MAPISendMail", WINAPI, ULONG,
+    LHANDLE,    // lhSession
+    ULONG.ptr,  // ulUIParam
+    lpMapiMessage, // lpMessage
+    FLAGS,      // flFlags
+    ULONG);     // ulReserved
+
+  return functionData;
+}
new file mode 100644
--- /dev/null
+++ b/mailnews/mapi/test/unit/tail_mapi.js
@@ -0,0 +1,1 @@
+load("../../../resources/mailShutdown.js");
new file mode 100644
--- /dev/null
+++ b/mailnews/mapi/test/unit/test_mapisendmail.js
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {ctypes} = ChromeUtils.import("resource:///modules/ctypes.jsm");
+var {MimeParser} = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+function run_test() {
+  // Set up an SMTP server and the MAPI daemon.
+  getBasicSmtpServer();
+  let [daemon, server] = setupServerDaemon();
+  server.start(SMTP_PORT);
+  let mapi = loadMAPILibrary();
+
+  // Build a message using the MAPI interface.
+  let message = new mapi.MapiMessage();
+  message.lpszSubject = ctypes.char.array()("Hello, MAPI!");
+  message.lpszNoteText = ctypes.char.array()("I successfully sent a message!");
+  message.lpszMessageType = ctypes.char.array()("");
+  message.lpFiles = null;
+  let recipient = new mapi.MapiRecipDesc();
+  recipient.ulRecipClass = 1; /* MAPI_TO */
+  recipient.lpszName = ctypes.char.array()("John Doe");
+  recipient.lpszAddress = ctypes.char.array()("SMTP:john.doe@example.com");
+  message.nRecipCount = 1;
+  message.lpRecips = recipient.address();
+
+  // Use MAPISendMail to send this message.
+  mapi.SendMail(null /* No session */, null /* No HWND */, message.address(),
+    0x2 /* MAPI_NEW_SESSION */, 0);
+
+  // Check that the post has the correct information.
+  let [headers, body] = MimeParser.extractHeadersAndBody(daemon.post);
+  Assert.equal(headers.get("from")[0].email, "tinderbox@tinderbox.invalid");
+  Assert.equal(headers.get("to")[0].email, "john.doe@example.com");
+  Assert.equal(headers.get("subject"), "Hello, MAPI!");
+  Assert.equal(body.trim(), "I successfully sent a message!");
+
+  server.stop();
+}
new file mode 100644
--- /dev/null
+++ b/mailnews/mapi/test/unit/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+head = head_mapi.js
+tail = tail_mapi.js
+run-sequentially = Need to use well-known port for SMTP.
+
+[test_mapisendmail.js]
+skip-if = true # Not yet enabled, see bug 1526807.
--- a/mailnews/moz.build
+++ b/mailnews/moz.build
@@ -46,16 +46,17 @@ TEST_DIRS += [
     'local/test',
 ]
 
 if CONFIG['MOZ_MAPI_SUPPORT']:
     DIRS += [
         'mapi/mapiDLL',
         'mapi/mapihook',
     ]
+    TEST_DIRS += ['mapi/test']
 
 DIRS += [
     'build',
     'import/build',
 ]
 
 if CONFIG['MOZ_MOVEMAIL'] and not (
         CONFIG['MOZ_THUNDERBIRD'] and CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa'):