Saturday, February 23, 2008

Combining multiple Exchange Management Shell powershell cmdlets and maintaining the Pipeline

This post continues on a little bit from last weeks update of the mailbox size GUI where I combined three Exchange powershell cmdlets get-mailbox,get-mailboxstatistics and get-mailboxdatabase to get the percentage of quota used for a users mailbox. When I did this although I used the Pipeline I didn’t really maintain it because I didn’t really need to I had my own custom methods for displaying the data I was querying. But one of the most useful things about Powershell is when using the pipeline normally you have a lot of built in functionality as to how you can format and export the results of whatever command you are running for instance Export-Csv format-table etc.

Someone asked the question this week

We need to get the user's list from POWER SHELL sorting by database , ProhibitsendrecieveQuota, mailboxsize & lastlogontime ( those who have not been logged on their mails for 60 days).

I hope there should be merged these two comlets "get-mailbox "and "get-mailboxstatistics".

Out put format should be as follows:

Alias, Databasename, ProhibitsendrecieveQuota, lastlogontime

Peter MailDB 100 MB 30/11/2007 “

While I might go about this another way such as writing another little gui or mod the mailbox size report it’s a useful problem to confront for people who are getting more and more comfortable with Pipelining in Exchange Management Shell but maybe not really sure how you could go about combining two or three cmdlets like I have and still get the normal output features.

The answer is relatively easy all you need to do is create your own custom collection and then write the result of your combined cmdlet queries to your custom collection and then at the end of the pipeline you can then use the normal export-csv or format-table cmds. For example here’s a script that solves the question asked and allows both outputting the result to a screen and exporting to a csv.

$mbcombCollection = @()
Get-Mailbox -ResultSize Unlimited | foreach-object{
$mbstatis = get-mailboxstatistics $_.identity
$mbcomb = "" | select Alias,Databasename,ProhibitsendrecieveQuota,lastlogontime
$pequote = "unlimited"
$dbarry = $_.Database.ToString().split("\")
if ($_.UseDatabaseQuotaDefaults -eq $true){
$dbsetting = get-Mailboxdatabase $_.database
if ($dbsetting.ProhibitSendReceiveQuota -ne "unlimited"){
$pequote = $dbsetting.ProhibitSendReceiveQuota.Value.ToMB()
}
}
else {
if ($_.ProhibitSendReceiveQuota -ne "unlimited"){
$pequote = $_.ProhibitSendReceiveQuota.Value.ToMB()
}}
$mbcomb.Alias = $_.Alias
$mbcomb.Databasename = $dbarry[2]
$mbcomb.ProhibitsendrecieveQuota = 299
$mbcomb.lastlogontime = $mbstatis.LastLogonTime
$mbcombCollection += $mbcomb
}


$mbcombCollection
$mbcombCollection | Export-Csv C:\LogonStats.csv

This is a simple example if you have a large number of users for example this would be a little slow it would be better just to make one get-mailbox request and one get-mailboxstatitics request store the results in two hash tables then merge the result of the hash tables which is what I did with the mailbox size gui. But the above method is much easier for people to understand so it’s probably a better example in that regard.

Tuesday, February 12, 2008

Version 4 of the Mailbox Size Gui and using Quotas in Mailbox Size Reports

Version 5 has now been posted that fixes issue with quotas please see this

Based on the feedback and requests I’ve received I’ve updated the Mailbox Size Gui to show the percent of Quota used by each user. While this was not hard to do it’s unfortunately not as simple as just pulling the information from the get-mailboxstatistics cmdlet. As I’ve shown in other posts before when you what to determine the Quota that applies to a user’s mailbox you need to check quota information in a number of locations. The first place you need to check is the users account to see whether then have the use store quotas defaults set to true of false. Within a Get-mailboxstatitics operation there is a property made available called UseDatabaseQuotaDefaults which can be used to do this. If this property returns false then the quota information will be set on the User's Mailbox itself (or more accurately the User object in Active Directory) so in this case you can use the Get-Mailbox cmdlet to retrieve the ProhibitSendReceiveQuota property on the user account. If the UseDatabaseQuotaDefaults is set to true the you need to find out which Mailstore the Mailbox is located on and then check the Mailstores quotas setting using the get-mailboxdatabase cmdlet. If you want to do this for a number of users you could pipleline it a few different ways the way I’ve chosen to do this in my Mailbox GUI script is to create 2 hashtables to store any MailStore and UserMailbox quotas and then during the get-mailboxstatisitics cmd use these Hashtables to determine the percent of Quota the user has actually used. Here’s a quick sample

$mstoresquotas = @{ }
get-mailboxdatabase -server servername | ForEach-Object{
$_.identity
$_.ProhibitSendReceiveQuota
if ($_.IsUnlimited -ne $true){
$mstoresquotas.add($_.identity,$_.ProhibitSendReceiveQuota)
}

}

$usrquotas = @{ }
Get-Mailbox -server servername -ResultSize Unlimited | foreach-object{
if($_.ProhibitSendQuota -ne "unlimited"){
$usrquotas.add($_.ExchangeGuid,$_.ProhibitSendReceiveQuota)
}
}

get-mailboxstatistics | ForEach-Object{
$quQuota = "unlimited"
if ($_.UseDatabaseQuotaDefaults -eq $false){
if ($usrquotas.ContainsKey($_.MailboxGUID)){
$quQuota = "{1:P0}" -f ($_.TotalItemSize.Value.ToMB()/$usrquotas[$_.MailboxGUID].Value.ToMB())
}
}
else{
if ($mstoresquotas.ContainsKey($_.database)){
$quQuota = "{1:P0}" -f ($_.TotalItemSize.Value.ToMB()/$mstoresquotas[$_.database].Value.ToMB())}
}
$quQuota
}

What’s next when I get time I want to add a history feature so that every time you run the Gui it will record the result to an XML file so that the next time you run it in a week or months time it can then show you how much each mailbox has grown since. Also I’d like to come up with a cmd-line version that will output the result to a HTML Email with all the same functions. If anyone has any other idea’s they would like to see please let me know and I’ll see what I can do.

I’ve put a download of the new version here.

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);
}
}