Saturday, April 21, 2012

Safely accessing Named Properties in Outlook

Whenever you want to access properties in Outlook that are not exposed by the Outlook Object Model, there is the PropertyAccessor object.

Not only does this powerful class allow you to retrieve any of the standard MAPI properties, it also gives you access to any custom Named Properties using a convenient string notation.

This class does have one minor problem though: it doesn't offer any way to explicitly check for the existence of a property. So when one half of your applications marks certain emails with a certain named property, and the other half needs to read out that marker, you end up with code like this:

var
  mailItem: OleVariant;
  propertyName: String;
  marker: String;

begin
  ...

  propertyName := 'http://schemas.microsoft.com/mapi/string/someGuid/my.secret.marker';

  try
      marker := mailItem.propertyAccessor.getProperty( propertyName );

  except
      // Apparently, the marker isn't set.
      marker := '';
  end;

end;

Ouch. Not so much fun when doing this in a loop over a few thousand email. And turning off Break on OleException in the IDE is not the best of ideas when you are doing office automation, either.

Moving from late to early binding (and you should, anyway) makes this problem blatantly apparent; here is the interface definition of PropertyAccessor (renamed IPropertyAccessor for compliance with Delphi naming standards):

  IPropertyAccessor = interface(IDispatch)
    ['{0006302D-0000-0000-C000-000000000046}']
    ... snip ...
    function  GetProperty(const SchemaName: WideString): OleVariant; safecall;
    procedure SetProperty(const SchemaName: WideString; Value: OleVariant); safecall;
    ... snip ...
  end;

See those safecall markers? Those means that Delphi has wrapped the calls in a little check, raising an exception whenver the result is not ok. Which is nice and error proof, but brushes over the fact that there are several different results possible from the underlying call, and not all of them are crucial errors. In our case the returned result is MAPI_E_NOT_FOUND, which just tells us the mailItem at hand wasn't marked... not exactly an "exceptional" situation in this particular application.

The trick is that we can simply redefine this interface, using HResult stdcalls directly, as opposed to the magically wrapped safecalls. This conversion is easy enough to do by hand (everything needs to return a HResult, move all function results to out parameters). However, for even more convenience, we can use use tlibimp to do this. From a command line, execute:


tlibimp -P -Ps- "C:\Program Files\Microsoft Office\Office14\MSOUTL.OLB"


(make sure to correct that path if office is installed elsewhere). This will output a nice Outlook_TLB where we can copy the property from. I aptly (although perhaps slightly confusing) named my copy ISafePropertyAccessor.
ISafePropertyAccessor = interface(IDispatch)
    ['{0006302D-0000-0000-C000-000000000046}']
    ... snip ...
    function GetProperty(const SchemaName: WideString; out Value: OleVariant): HResult; stdcall;
    function SetProperty(const SchemaName: WideString; Value: OleVariant): HResult; stdcall;
    ... snip ...
  end;

Here is how we'd use it in the example above:
var
  mailItem: OleVariant;
  propertyName: String;
  propertyAccessor: ISafePropertyAccessor;
  value: OleVariant;
  marker: String;

begin
  ...

  propertyName := 'http://schemas.microsoft.com/mapi/string/someGuid/my.secret.marker';
  propertyAccessor := IUnknown(mailItem.propertyAccessor) as ISafePropertyAccessor;

  hr := mailItem.propertyAccessor.getProperty( propertyName, value );

  if hr = S_OK then
  begin
      marker := value;
  end
  else
      marker := '';

  ...
end;
Slightly more verbose, but that can always be wrapped away in some convenience functions... and it sure is nice to get rid of the exception!

1 comment:

  1. This comment has been removed by a blog administrator.

    ReplyDelete