Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Outlook: inProcess implementation to report replied / replied all / forwarded status on mail items in the message list #8756

Merged
merged 12 commits into from Sep 19, 2018

Conversation

michaelDCurran
Copy link
Member

@michaelDCurran michaelDCurran commented Sep 17, 2018

Link to issue number:

Fixes #6911

Summary of the issue:

Outlook's message list shows if a mail item has been replied to or forwarded, however NVDA does not report this information.
Previous tries at implementing this which failed were:

  • Fetch the info from UI Automation: UIA does expose text denoting replied/forwarded via the value pattern. However, this text is localized and therefore is impossible to match on programmatically with 100% accuracy. It is also included with other text such as read/unread status and mail item type, which we must strip.
  • Although the Outlook object model itself does not have a specific property on mail items conveying the last executed action (replied to, forwarded), it does have a propertyAccessor property which allows fetching MAPI properties. Fetching the status this way worked for some users, but for those who had set very high security policies, the fetching of the PropertyAccessor property caused a security dialog to appear. Further testing showed that even if ths property was fetched inProcess the security restrictions still applied.

Description of how this pull request fixes the issue:

The Outlook object model provides direct access to the MAPI object via a mapiObject property on mail items. However, as the IMAPIProp interface cannot be marshalled by COM, all calls must be made on the object directly, in the object's own COM apartment (the Outlook GUI thread).
This PR implements a new outlook_getMAPIProperty RPC call which marshals the given MAPI object back into its own thread, and fetches the requested property from there.

Testing performed:

  • Outlook 2016/365.
  • NVDA installed (UIAccess), portable, and running from source.
  • Outlook is set to block any insecure access (I.e. if NVDA fetches MailItem.PropertyAccessor, the security dialog appears).
  • Focus a mail item in the message list. NVDA reports if it is forwarded, replied, or replied all. No security dialog ever appears.

Known issues with pull request:

  • Although security restrictions are not currently applied to MailItem.mapiObject, there is no reason why they may not get applied in a future version of Outlook.
  • MailItem.mapiObject is deprecated in current versions of Outlook, though still functions at this point in time. A future version of Outlook may remove support completely. If this occurs, this code is already ready to handle a COM error or a NULL COM pointer. Worse case, NVDA simply stops reporting these statuses.

Change log entry:

New features:

Section: New features, Changes, Bug fixes

…s in the message list, by fetching info from the mail item's MAPI object in-process, bypassing security restrictions.
Copy link
Collaborator

@LeonarddeR LeonarddeR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a C++ expert, so it might be good to have @feerrenrut have a quick look at the NVDAHelper code, especially the new outlook module.

#include <windows.h>

// A Smart Library handle.
// Constrcut it with a handle returned by LoadLibrary or similar.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constrcut > Construct

pGIT->GetInterfaceFromGlobal(cookie,IID_IUnknown,(void**)&mapiObject);
if(res!=S_OK) {
LOG_ERROR(L"Could not marshal object");
return;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be return res?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently execInThread only takes callables that return void. I made res be passed into the lambda by reference thus once execInThread returns, res contains what ever error was inside. However, I should at least be returning res from the outer rpc function, which I forgot to do.

@LeonarddeR
Copy link
Collaborator

I'm getting the following error while arrowing through an outlook messages list INFO - RPC process 11932 (OUTLOOK.EXE) (08:33:23.914): Thread 1356, build\x86_64\remote\outlook.cpp, nvdaInProcUtils_outlook_getMAPIProp, 43: MAPI object in rpc thread: 0000019A74245B80

ERROR - RPC process 11932 (OUTLOOK.EXE) (08:33:23.921):
Thread 1356, build\x86_64\remote\outlook.cpp, nvdaInProcUtils_outlook_getMAPIProp, 62:
Could not locate function HrGetOneProp in mapi32.dll

This is outlook '16.0.10730.20088 X64

@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 17, 2018 via email

@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 17, 2018 via email

@LeonarddeR
Copy link
Collaborator

This was a standalone clikc to run installer

Strange enough, the HrGetOneProp function is there, so I don't get why neither ctypes nor NVDAHelper can't get it:

HrGetOneProp@12	0x1000d2d0	0x0000d2d0	135 (0x87)	mapi32.dll	C:\Windows\syswow64\mapi32.dll	Exported Function	

HrGetOneProp@12	0x1000d2d0	0x0000d2d0	135 (0x87)	mapi32.dll	C:\Windows\system32\mapi32.dll	Exported Function	

@LeonarddeR
Copy link
Collaborator

Actually, the HrGetOneProp docs indicate that this support is limited to Outlook >=2013.

@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 17, 2018 via email

@LeonarddeR
Copy link
Collaborator

Yes, My office is 64 bit indeed.

dllex is reporting the @12 suffix for both the x86 and x64 instance on my system. File version of mapi32.dll is 1.0.2536.0. Product version is 10.0.17134.1. This one looks like to be bundled with the OS, so I wonder whether Outlook version installed makes a difference.

@LeonarddeR
Copy link
Collaborator

A plain Windows 7 x86 VM without outlook installed also has the mapi32.dll variant with the @12 suffix.

IN this article, Microsoft explains how to link to MAPI functions. There's also an article to choose a specific version

@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 17, 2018 via email

@LeonarddeR
Copy link
Collaborator

Yes, this works like a charm.

I just pushed a try build for this, so I can test this on an Outlook 2010 system.

@LeonarddeR
Copy link
Collaborator

LeonarddeR commented Sep 17, 2018

This definitely causes heavy crashes when used with Outlook 2010. The outlook process is terminated as soon as a message gets focus.

Error log
ERROR - RPC process 8236 (nvda_slave.exe) (11:12:53.848):
__main__.main:
slave error
Traceback (most recent call last):
  File "nvda_slave.pyw", line 94, in main
  File "comHelper.pyc", line 22, in _lresultFromGetActiveObject
  File "comtypes\client\__init__.pyc", line 180, in GetActiveObject
  File "comtypes\__init__.pyc", line 1247, in GetActiveObject
  File "_ctypes/callproc.c", line 950, in GetResult
WindowsError: [Error -2147221021] Bewerking is niet beschikbaar
INFO - RPC process 11776 (OUTLOOK.EXE) (11:12:56.298):
Thread 14208, build\x86_64\remote\outlook.cpp, nvdaInProcUtils_outlook_getMAPIProp, 43:
MAPI object in rpc thread: 0000000010BB8430

INFO - RPC process 11776 (OUTLOOK.EXE) (11:12:56.298):
Thread 4112, build\x86_64\remote\outlook.cpp, nvdaInProcUtils_outlook_getMAPIProp::<lambda_d6a1fe29f2f3f1f2a36dfe801802c01b>::operator (), 98:
MAPI object in GUI thread: 1

ERROR - RPC process 8808 (nvda_slave.exe) (11:13:09.305):
main.main:
slave error
Traceback (most recent call last):
File "nvda_slave.pyw", line 94, in main
File "comHelper.pyc", line 22, in lresultFromGetActiveObject
File "comtypes\client_init
.pyc", line 180, in GetActiveObject
File "comtypes_init_.pyc", line 1247, in GetActiveObject
File "_ctypes/callproc.c", line 950, in GetResult
WindowsError: [Error -2147221021] Bewerking is niet beschikbaar
INFO - RPC process 3008 (OUTLOOK.EXE) (11:13:12.579):
Thread 7516, build\x86_64\remote\outlook.cpp, nvdaInProcUtils_outlook_getMAPIProp, 43:
MAPI object in rpc thread: 0000000010486920

INFO - RPC process 3008 (OUTLOOK.EXE) (11:13:12.585):
Thread 2188, build\x86_64\remote\outlook.cpp, nvdaInProcUtils_outlook_getMAPIProp::<lambda_d6a1fe29f2f3f1f2a36dfe801802c01b>::operator (), 98:
MAPI object in GUI thread: 1

ERROR - RPC process 5100 (nvda_slave.exe) (11:13:25.823):
main.main:
slave error
Traceback (most recent call last):
File "nvda_slave.pyw", line 94, in main
File "comHelper.pyc", line 22, in lresultFromGetActiveObject
File "comtypes\client_init
.pyc", line 180, in GetActiveObject
File "comtypes_init_.pyc", line 1247, in GetActiveObject
File "_ctypes/callproc.c", line 950, in GetResult
WindowsError: [Error -2147221021] Bewerking is niet beschikbaar
INFO - RPC process 11572 (OUTLOOK.EXE) (11:13:39.536):
Thread 15284, build\x86_64\remote\outlook.cpp, nvdaInProcUtils_outlook_getMAPIProp, 43:
MAPI object in rpc thread: 000000000A2DC1A0

INFO - RPC process 11572 (OUTLOOK.EXE) (11:13:39.536):
Thread 14540, build\x86_64\remote\outlook.cpp, nvdaInProcUtils_outlook_getMAPIProp::<lambda_d6a1fe29f2f3f1f2a36dfe801802c01b>::operator (), 98:
MAPI object in GUI thread: 1

@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 17, 2018 via email

…erfaceTable::getInterfaceFromGlobal, and return on error.
@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 17, 2018 via email

@LeonarddeR
Copy link
Collaborator

Still crashes:

ERROR - RPC process 76 (nvda_slave.exe) (13:27:34.888):
__main__.main:
slave error
Traceback (most recent call last):
  File "nvda_slave.pyw", line 94, in main
  File "comHelper.pyc", line 22, in _lresultFromGetActiveObject
  File "comtypes\client\__init__.pyc", line 180, in GetActiveObject
  File "comtypes\__init__.pyc", line 1247, in GetActiveObject
  File "_ctypes/callproc.c", line 950, in GetResult
WindowsError: [Error -2147221021] Bewerking is niet beschikbaar
INFO - RPC process 5740 (OUTLOOK.EXE) (13:27:39.892):
Thread 3540, build\x86_64\remote\outlook.cpp, nvdaInProcUtils_outlook_getMAPIProp, 43:
MAPI object in rpc thread: 00000000109B4150

INFO - RPC process 5740 (OUTLOOK.EXE) (13:27:39.903):
Thread 3972, build\x86_64\remote\outlook.cpp, nvdaInProcUtils_outlook_getMAPIProp::<lambda_8eeec166f96c7951c3b21da844cd2873>::operator (), 98:
MAPI object in GUI thread: 1

@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 17, 2018 via email

…er than PM_REMOVE, so check for PM_REMOVE specifically rather than PM_NOREMOVE. This stops messages being processed more than once if fetched with peakMessage with no PM_REMOVE but other flags.
@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 17, 2018 via email

@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 18, 2018 via email

@LeonarddeR
Copy link
Collaborator

The Outlook 2010 system is actually my colleague's system, but it indeed has Outlook 2010 X64. With the last try build, there are no crashes and things are reported as expected.

LeonarddeR
LeonarddeR previously approved these changes Sep 18, 2018
Copy link
Collaborator

@LeonarddeR LeonarddeR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strange errors in the Outlook 2010 instance NVDA log either.


// Our RPC function
error_status_t nvdaInProcUtils_outlook_getMAPIProp(handle_t bindingHandle, const long threadID, IUnknown* mapiObject, const unsigned long mapiPropTag, VARIANT* retVal) {
LOG_INFO(L"MAPI object in rpc thread: "<<mapiObject);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make sense to make this debug before merging.

LOG_ERROR(L"Could not unmarshal object, code "<<res);
return;
}
LOG_INFO(L"MAPI object in GUI thread: "<<static_cast<IUnknown*>(mapiObject));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

@@ -114,7 +114,7 @@ bool unregisterWindowsHook(int hookType, HOOKPROC hookProc) {

//GetMessage hook callback
LRESULT CALLBACK inProcess_getMessageHook(int code, WPARAM wParam, LPARAM lParam) {
if(code<0||wParam==PM_NOREMOVE) {
if(code<0||!(wParam&PM_REMOVE)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this equal to
if(code<0||wParam~PM_REMOVE) {
I consider that to be somewhat more readble, but that's just a personal preference.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

~ in c++ is a unary operator I think. So it would have to be wParam&~PM_REMOVE.

// The following declarations come from MAPIDEFS.h which is no longer included in the Windows SDK

#define PT_LONG ((ULONG)3)
#define MAPI_E_NOTFOUND 0x8004010f
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than #defines, can these be changed constexpr

// Right now this function only supports MAPI properties with a type of long.
// To support more types, we would need to know how to correctly pack them into a VARIANT.
LOG_ERROR(L"Unsupported MAPI prop type");
return -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than -1 can we have a named constant please.

execInThread(threadID,[=,&res](){
// Unmarshal the IUnknown pointer from the COM global interface table.
IUnknownPtr mapiObject=nullptr;
res=pGIT->GetInterfaceFromGlobal(cookie,IID_IUnknown,(void**)static_cast<IUnknown**>(&mapiObject));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(void**)static_cast<IUnknown**>(&mapiObject) looks wrong to me. Why cast to IUnknown** and then c-style cast to void**

res=HrGetOneProp(mapiObject,mapiPropTag,&_propValue);
propValue.reset(_propValue);
}
if(res!=S_OK||!propValue) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, res could be returned by the outer function with S_OK even though there was an error due to !propValue

if mapiObject:
v=comtypes.automation.VARIANT()
res=NVDAHelper.localLib.nvdaInProcUtils_outlook_getMAPIProp(self.appModule.helperLocalBindingHandle,self.windowThreadID,mapiObject,PR_LAST_VERB_EXECUTED,ctypes.byref(v))
if res==0:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a name for 0 instead.

@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 19, 2018 via email

@feerrenrut
Copy link
Contributor

Actually, no cast is necessary to cast to void** I believe this will happen implicitly. However, if its important to ensure that &mapiObject is dynamically convertible to IUnknown** then I would prefer this is done separately with a comment to explain. Like so:

// static_cast to ensure that the smart pointer's address-taking operator (&) correctly uses the encapsulated IUnknown* pointer
auto mapiObjPtr = static_cast<IUnknown**>(&mapiObject) 
res=pGIT->GetInterfaceFromGlobal(cookie, IID_IUnknown, mapiObjPtr);

That said, it's not necessary at all, mapiObject is a pointer type and thus can't override the & operator, so the result of &mapiObject has to be IUnknown**. So I think my preference would be to simplify the code to:

res=pGIT->GetInterfaceFromGlobal(cookie, IID_IUnknown, &mapiObject);

LOG_ERROR(L"NULL MAPI object");
return E_INVALIDARG;
}
if((mapiPropTag&0xffff)!=PT_LONG) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having 0xffff hard coded here, it might be better to give it a name. This ties in with the comment in nvdaInProcUtils right? Perhaps this could be called tagMask

mapiObject=None
if mapiObject:
v=comtypes.automation.VARIANT()
res=NVDAHelper.localLib.nvdaInProcUtils_outlook_getMAPIProp(self.appModule.helperLocalBindingHandle,self.windowThreadID,mapiObject,PR_LAST_VERB_EXECUTED,ctypes.byref(v))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, since this line is quite long, please split the arguments onto multiple lines.

@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 19, 2018 via email

@michaelDCurran
Copy link
Member Author

michaelDCurran commented Sep 19, 2018 via email

feerrenrut
feerrenrut previously approved these changes Sep 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

NVDA not inform about if an e-mail in Outlook 2013 is answered or forwarded
4 participants