Tuesday, February 05, 2008

Class library to help setting Out of Office (Oof) via Exchange Web Services with Powershell and .NET

One of the added features that Exchange 2007 gives you when teamed with Outlook 2007 is an Enhanced Out of Office functionality. In previous version of Exchange the OOF was a on/off setting with a Text based message to represent your OOF reply. In 2007 you now have the 3 states on/off and scheduled (meaning that oof message will only be sent during the scheduled time). The user also has the ability to set Internal and External OOF response messages and the ability to set who the External oof messages will be sent to (the three states for this is All, None and Known contacts).

From a coders point of view you have gone from something that was two properties to something a little more complex (kind of like the difference between a Volkswagen beetle and a decent car)(*note to self must not blog while watching Top Gear) . Although it is still possible to set the OOF message via the old API’s Exchange Web Services is the only method that actually provides a fully supported and functional method of setting the OOF programmatically.

To make scripting a bit easy I decided to put a class library together this facilitates a few things that while not impossible in script make for a lot of complexity, bloat and possible errors (kind of like a Leyland P76). To cater for a number of different way one might want to set Oof setting I create a lot of overloads to allow executing the methods in a number of different ways.

I’ve included a basic autodiscover function in the class library it does have overloads that will allow you to specify the Cas server you want to use. Otherwise it will first do a search of Active Directory to find a Service Connection Point and then it will do an Autodisover request based on the email to find the URL of a CAS server. What this SCP and discovery function doesn’t do is try to find the closest CAS to the calling client just the first one it can find.

With Authentication I was planning to offer both impersonation and Delegation functionality in the library but this one of the peculiarlarites of Exchange Web Services. While the impersonation header is honored for nearly all other EWS operations it’s not honored by the availably service. So if you want to use this library (or EWS) to change OOF setting you need to be running the code as the Mailbox Owner or a user that has been delegated access rights to the mailbox. I’ve included overloads to allow you to specify the account you want the EWS code to run under. If you also specify the URL to the CAS server you can use this to run the script/class library from a machine that is not a member of an Internal AD domain .Otherwise by default if you don’t specify a username and password the library will run using the calling process security context (or more easily the currently logged on on user’s credentials).

The class library contains two Methods the GetOof Method that returns the current Oofsetting (the setting themselves are returned as class library properties). The SetOof method provides the ability to set the Oof setting on Exchange 2007. The SetOof method first makes a call to get the current setting and then applies the changes based on the overload you use to call the method it applies the changes to the current setting and then posts back the results to Exchange as a SetUserOofSettings request.This gets around any issues where you may loss fidelity of Oof Data where you just want to flip or change one property. Of course if you do this at the same time as the user is making changes well then you have a competition that the last one to write will win. (The user may not understand this though).

A few Notes –
The Message Text is always returned as HMTL Message body even if you just set text

Because Exchange Stores DateTimes as UTC when your setting or retrieving a duration you need to make sure you do UTC conversion.

I’ve put together a samples page the show example of using the different overloads in Powershell and C#. I’ve included a compiled version of the code as well as the source code if you want to compile it yourself or improve on or just laugh at the source code. This is only version1 so like most of things on this blog is potentially full of errors and the error handling needs to be improved but if you do find any please let me know I love a good bug

The sample page is here the download for with the DLL and source is here . The more interesting parts of the code looks like.

public String GetOof(string EmailAddress, string UserName, string Password,string Domain, string OofURL) {
String rsResult = null;
try
{
ExchangeServiceBinding ebExchangeServiceBinding = createesb(EmailAddress, UserName, Password,Domain,OofURL);
UserOofSettings uoUserOofSettings = ewsGetOOF(ebExchangeServiceBinding, EmailAddress);
this.intOofStatus = uoUserOofSettings.OofState.ToString();
if (uoUserOofSettings.InternalReply.Message != null) { this.intInternalMessage = uoUserOofSettings.InternalReply.Message.ToString(); }
if (uoUserOofSettings.InternalReply.lang != null) { this.intInternalMessageLanguage = uoUserOofSettings.InternalReply.lang.ToString(); }
if (uoUserOofSettings.ExternalReply.Message != null) { this.intExternalMessage = uoUserOofSettings.ExternalReply.Message.ToString(); }
if (uoUserOofSettings.ExternalReply.lang != null) { this.intExternalMessageLanguage = uoUserOofSettings.ExternalReply.lang.ToString(); }
this.intExternalAudienceSetting = uoUserOofSettings.ExternalAudience.ToString();
if (uoUserOofSettings.Duration != null) { this.intDuration = uoUserOofSettings.Duration; }
rsResult = "OOF Settings retrieved";
ServicePointManager.ServerCertificateValidationCallback = null;
ebExchangeServiceBinding = null;
return rsResult;
}
catch (Exception exException)
{
Console.WriteLine(exException.ToString());
return exException.ToString();
}
}

private ExchangeServiceBinding createesb(String EmailAddress, string UserName, string Password, string Domain,string OofURL) {
ServicePointManager.ServerCertificateValidationCallback =
delegate(Object obj, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
{
// Ignore Self Signed Certs
return true;
};
ExchangeServiceBinding ebExchangeServiceBinding = new ExchangeServiceBinding();
ebExchangeServiceBinding.RequestServerVersionValue = new RequestServerVersion();
ebExchangeServiceBinding.RequestServerVersionValue.Version = ExchangeVersionType.Exchange2007_SP1;
if (UserName == "")
{
ebExchangeServiceBinding.UseDefaultCredentials = true;
}
else
{
NetworkCredential ncNetCredential = new NetworkCredential(UserName, Password, Domain);
ebExchangeServiceBinding.Credentials = ncNetCredential;
}
if (OofURL == "")
{
String caCasURL = DiscoverCAS();
OofURL = DiscoverOofURL(caCasURL, EmailAddress, UserName, Password,Domain);
}
ebExchangeServiceBinding.Url = OofURL;
return ebExchangeServiceBinding;

}
private String DiscoverCAS()
{
String ScpUrlGuidString = "77378F46-2C66-4aa9-A6A6-3E7A48B19596";
String ScpPtrGuidString = "67661d7F-8FC4-4fa7-BFAC-E1D7794C1F68";
DirectoryEntry rdRootDSE = new DirectoryEntry("LDAP://RootDSE");
DirectoryEntry cfConfigPartition = new DirectoryEntry("LDAP://" + rdRootDSE.Properties["configurationnamingcontext"].Value);
DirectorySearcher cfConfigPartitionSearch = new DirectorySearcher(cfConfigPartition);
cfConfigPartitionSearch.Filter = "(&(objectClass=serviceConnectionPoint)((keywords=" + ScpPtrGuidString + ")(keywords=" + ScpUrlGuidString + ")))";
cfConfigPartitionSearch.SearchScope = SearchScope.Subtree;
string CASURL = null;
SearchResult srSearchResult = cfConfigPartitionSearch.FindOne();
if (srSearchResult != null)
{
DirectoryEntry scpServiceConnectionPoint = srSearchResult.GetDirectoryEntry();
CASURL = scpServiceConnectionPoint.Properties["serviceBindingInformation"].Value.ToString();
}
else
{
throw new ADSearchException("No SCP found");
}
return CASURL;
}
private String DiscoverOofURL(string caCASURL, string emEmailAddress,string UserName,string Password,string Domain)
{

String OofURL = null;
String auDisXML = "" +
"" + emEmailAddress + "" +
"http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a" +
"
" +
"
";
System.Net.HttpWebRequest adAutoDiscoRequest = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(caCASURL);
adAutoDiscoRequest.ContentType = "text/xml";
adAutoDiscoRequest.Headers.Add("Translate", "F");
adAutoDiscoRequest.Method = "Post";
if (UserName == "")
{
adAutoDiscoRequest.UseDefaultCredentials = true;
}
else
{
NetworkCredential ncNetCredential = new NetworkCredential(UserName, Password, Domain);
adAutoDiscoRequest.Credentials = ncNetCredential;
}

byte[] bytes = Encoding.UTF8.GetBytes(auDisXML);
adAutoDiscoRequest.ContentLength = bytes.Length;
Stream rsRequestStream = adAutoDiscoRequest.GetRequestStream();
rsRequestStream.Write(bytes, 0, bytes.Length);
rsRequestStream.Close();
WebResponse adResponse = adAutoDiscoRequest.GetResponse();
Stream rsResponseStream = adResponse.GetResponseStream();
XmlDocument reResponseDoc = new XmlDocument();
reResponseDoc.Load(rsResponseStream);
XmlNodeList OofNodes = reResponseDoc.GetElementsByTagName("OOFUrl");
if (OofNodes.Count != 0)
{
OofURL = OofNodes[0].InnerText;
}
else {
throw new AutoDiscoveryException("Error during AutoDiscovery");
}
return OofURL;

}
private UserOofSettings ewsGetOOF(ExchangeServiceBinding ebExchangeServiceBinding, String emEmailAddress)
{
GetUserOofSettingsRequest goGetUserOofSettings = new GetUserOofSettingsRequest();
UserOofSettings ouOffSetting = null;
EmailAddress mbMailbox = new EmailAddress();
mbMailbox.Address = emEmailAddress;
goGetUserOofSettings.Mailbox = mbMailbox;
GetUserOofSettingsResponse goGetOoFResponse = ebExchangeServiceBinding.GetUserOofSettings(goGetUserOofSettings);
if (goGetOoFResponse.ResponseMessage.ResponseClass == ResponseClassType.Success)
{
ouOffSetting = goGetOoFResponse.OofSettings;
}
else
{
throw new EWSException(goGetOoFResponse.ResponseMessage.MessageText.ToString());
}

return ouOffSetting;
}
private String ewsSetOOF(ExchangeServiceBinding ebExchangeServiceBinding, UserOofSettings uoNewOoFSettings, String emEmailAddress)
{
SetUserOofSettingsRequest soSetUserOofSettings = new SetUserOofSettingsRequest();
soSetUserOofSettings.UserOofSettings = uoNewOoFSettings;
EmailAddress mbMailbox = new EmailAddress();
mbMailbox.Address = emEmailAddress;
soSetUserOofSettings.Mailbox = mbMailbox;
SetUserOofSettingsResponse soSetOoFResponse = ebExchangeServiceBinding.SetUserOofSettings(soSetUserOofSettings);
String rsResponse = "";

if (soSetOoFResponse.ResponseMessage.ResponseClass == ResponseClassType.Success)
{
this.intOofStatus = uoNewOoFSettings.OofState.ToString();
if (uoNewOoFSettings.InternalReply.Message != null) { this.intInternalMessage = uoNewOoFSettings.InternalReply.Message.ToString(); }
if (uoNewOoFSettings.InternalReply.lang != null) { this.intInternalMessageLanguage = uoNewOoFSettings.InternalReply.lang.ToString(); }
if (uoNewOoFSettings.ExternalReply.Message != null) { this.intExternalMessage = uoNewOoFSettings.ExternalReply.Message.ToString(); }
if (uoNewOoFSettings.ExternalReply.lang != null) { this.intExternalMessageLanguage = uoNewOoFSettings.ExternalReply.lang.ToString(); }
this.intExternalAudienceSetting = uoNewOoFSettings.ExternalAudience.ToString();
if (uoNewOoFSettings.Duration != null) { this.intDuration = uoNewOoFSettings.Duration; }
rsResponse = "Oof Setting Update Succesfully";

}
else
{
throw new EWSException(soSetOoFResponse.ResponseMessage.MessageText.ToString());
}

return rsResponse;
}
}
class EWSException : Exception
{
public EWSException(string ewsError)
{
Console.WriteLine(ewsError);
}
}
class ADSearchException : Exception
{
public ADSearchException(string AdSearchError)
{
Console.WriteLine(AdSearchError);
}
}
class AutoDiscoveryException : Exception
{
public AutoDiscoveryException(string AutoDiscoveryError)
{
Console.WriteLine(AutoDiscoveryError);
}
}
class OofSettingException : Exception
{
public OofSettingException(string OofSettingError)
{
Console.WriteLine(OofSettingError);
}
}

6 comments:

Charles Leeds said...

Great job! However as a Ruby programmer I expected something more like.

user = User.find('abc12345')
user.out_of_office = 'on'
user.out_of_office.message = 'Go away - come back another day'

Why does everything Microsoft have to be so difficult?

Anonymous said...

Does this work for Exchange 2003 SP2 as well?

Dave said...

Hello,

Great article!! I do have a question though.

Is there a way to have a begin and end date range to specify the out of office message?

I've looked for this and for the life of me cannot get it to work! It seems like having a date range would be something available.

Any information on this would be very much appreciated!

Thank you!

adrian_g said...

Hello! Thank you for your article. It works great for viewing personal oof messages. I do have a question though.
I could not get the oof message of another user. I have delegate access to his account.
I can see his emails using 'FindItemType' but I cannot access the Out Off Office message.
What can I do?
Thank you.

colinbashbash said...

If i was going to get the current user's OOF information from a .net outlook addin (outlook 2010), could i use any of this code?

Radouane SIRAL said...

Could you please assist me as I can't run a script based on this DLL against multiple users at the same time ?