InfiniTec - Henning Krauses Blog

Don't adjust your mind - it's reality that is malfunctioning

Setting the Homepage of an Exchange folder using the EWS Managed API

A while ago, Glen posted an article on his blog on how to set homepage of a folder using ADO and later he posted a version of that script which uses the EWS managed API to do this in this MSDN forum thread. However, he wrote it for the first version of the API, and the EWS Managed API 1.1 has a slightly different object model. Since someone on the MSDN forums had difficulties to update the script to work with the EWS Managed API 1.1, I thought I just post an updated version here:

private static void SetFolderHomePage(IEnumerable<string> pathFragments, string url, ExchangeService service)
{
    var folderWebviewinfoProperty = new ExtendedPropertyDefinition(14047, MapiPropertyType.Binary);
    var root = Folder.Bind(service, WellKnownFolderName.MsgFolderRoot);
    var targetFolder = root;
    foreach (var fragment in pathFragments)
    {
        var result = service.FindFolders(targetFolder.Id, new SearchFilter.IsEqualTo(FolderSchema.DisplayName, fragment), new FolderView(1));
        if (result.TotalCount == 0)
        {
            throw new InvalidOperationException(string.Format("Folder fragment {0} was not found.", fragment));
        }
        targetFolder = result.Folders[0];
    }

    targetFolder.SetExtendedProperty(folderWebviewinfoProperty, EncodeUrl(url));
    targetFolder.Update();
}

private static byte[] EncodeUrl(string url)
{
    var writer = new StringWriter();
    var dataSize = ((ConvertToHex(url).Length / 2) + 2).ToString("X2");

    writer.Write("02"); // Version
    writer.Write("00000001"); // Type
    writer.Write("00000001"); // Flags
    writer.Write("00000000000000000000000000000000000000000000000000000000"); // unused
    writer.Write("000000");
    writer.Write(dataSize);
    writer.Write("000000");
    writer.Write(ConvertToHex(url));
    writer.Write("0000");
    
    var buffer = HexStringToByteArray(writer.ToString());
    return buffer;
}

private static string ConvertToHex(string input)
{
    return string.Join(string.Empty, input.Select(c => ((int) c).ToString("x2") + "00").ToArray());
}

private static byte[] HexStringToByteArray(string input)
{
    return Enumerable
        .Range(0, input.Length/2)
        .Select(index => byte.Parse(input.Substring(index*2, 2), NumberStyles.AllowHexSpecifier)).ToArray();
}

You can set the homepage of a folder by calling the SetFolderHomepage method:

SetFolderHomePage(service, new[] {"InfiniTec blog"}, http://www.infinitec.de);

Posted by Henning Krause on Wednesday, October 5, 2011 7:30 PM, last modified on Wednesday, October 5, 2011 7:34 PM
Permalink | Post RSSRSS comment feed

Custom Mail headers and EWS

The mail headers are exposed on the EmailMessage as first class property: InternetMessageHeaders. However, Exchange also stores each mail header in its own property and defines a unique property set for it: PS_INTERNET_HEADERS.

Here is a property definition for a hypothetical custom property:

private static readonly ExtendedPropertyDefinition FrobProperty =
    new ExtendedPropertyDefinition(DefaultExtendedPropertySet.InternetHeaders, "X-Frob", MapiPropertyType.String);

If a mail is processed by Exchange with message header like this:

Subject: Test mail
X-Frob: Yes
From: "Henning Krause" <someone@infinitec.de>
To: "Someone <someone@example.local>

Exchange will parse the mail and set the value of the extended property defined above to “Yes”.

Retrieving the custom property is as simple as this:

var item = Item.Bind(service, itemId,
    new PropertySet(BasePropertySet.FirstClassProperties, FrobProperty));

The value of the property can then be accessed using this line:

string frobValue;
if (item.TryGetProperty(FrobProperty, out frobValue))
{
    Console.Out.WriteLine("The item as a frob value of: {0}", frobValue);
}

You can even search for messages which have the header “X-Frob” set to “Yes”:

var itemId = folder.FindItems(new SearchFilter.IsEqualTo(FrobProperty, "yes"), 
    new ItemView(10) {PropertySet = PropertySet.IdOnly});

This query will return the first ten messages which have said header set to “Yes”.

Note that Exchange has a limit for named properties which are the result of custom mail headers. You may get one of these entries in your EventLog at some point:

Event ID: 9666
Type: Warning
Category: General
Source: msgidNamedPropsQuotaWarning
Description: The number of named properties created for database "<database name>" is close to quota limit. Current number of named properties: <number of named properties>. Quota limit for named properties: <configured quota>. User attempting to create the named property: <user name>. Named property GUID: <GUID of named property>. Named property name/id: <name of named property>.

 

Event ID: 9667
Type: Error
Category: General
Source: msgidNamedPropsQuotaError
Description: Failed to create a new named property for database "<database name>" because the number of named properties reached the quota limit (<configured quota>). User attempting to create the named property: <user name>. Named property GUID: <GUID of named property>. Named property name/id: <name of named property>.

 

See this blog post on how to deal with this issue: http://blogs.technet.com/b/exchange/archive/2010/07/29/3410545.aspx.


Posted by Henning Krause on Monday, August 1, 2011 12:00 PM, last modified on Wednesday, July 27, 2011 5:57 PM
Permalink | Post RSSRSS comment feed

Working with the Master Category List–EWS edition

Back in 2008 I wrote an article about accessing the Master Category List using WebDAV. If you are not sure what the Master Category List is, read that article first…

EWS could not be used at that time, since access to the Folder Associated Items via EWS is a Feature of Exchange 2010. So if you are on Exchange 2007, the old article still applies.

The last article contained some code which simplified updating the list. I’ve updated the code and aligned the method naming with that of the EWS managed API.

This is the class diagram for the updated code:

ClassDiagram

The classes are really easy to use:

var service = new ExchangeService(ExchangeVersion.Exchange2010_SP1) { Credentials = new NetworkCredential("someone@infinitec.de", "password") };
service.AutodiscoverUrl("someone@infinitec.de", url => true);

var list = MasterCategoryList.Bind(service);
foreach (var category in list.Categories)
{
    Console.Out.WriteLine("category = {0}", category.Name);
}

The only interesting line in the sample above is line 4. The call to MasterCategoryList.Bind() loads the MasterCategoryList from the users Exchange Mailbox. After that, the name of each console is written to the console.

Adding a new category is equally simple:

var service = new ExchangeService(ExchangeVersion.Exchange2010_SP1) { Credentials = new NetworkCredential("someone@infinitec.de", "password") };
service.AutodiscoverUrl("someone@infinitec.de", url => true);

var list = MasterCategoryList.Bind(service);
list.Categories.Add(new Category("Vacation", CategoryColor.DarkMaroon, CategoryKeyboardShortcut.CtrlF10));
list.Update();

This will add a new category named “Vacation” to the list.

So how does this work? The Master Category List is stored in a hidden message in the calendar folder of a mailbox. EWS in 2010 provides simple access to this message with the UserConfiguration class. This code show how the Master Category List is loaded:

var item = UserConfiguration.Bind(service, "CategoryList", WellKnownFolderName.Calendar,
                                  UserConfigurationProperties.XmlData);

var reader = new StreamReader(new MemoryStream(item.XmlData), Encoding.UTF8, true);

The configuration data is stored in the XmlData property as UTF-8 encoded byte array.

Here is the new code:

Sourcecode Download


Posted by Henning Krause on Thursday, July 28, 2011 4:00 PM, last modified on Tuesday, June 11, 2013 4:52 PM
Permalink | Post RSSRSS comment feed

Exchange Managed API autodiscover with Powershell

Powershell is a great tool to automate all sorts of things – including fiddling around with your Exchange mailbox. And the Autodiscover makes it really easy to connect to it – especially if you’re on Office 365 and don’t even know your CAS server.

So first, we need to load the EWS Managed API dll into the current runspace:

[Reflection.Assembly]::LoadFrom("C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll")

Then, create an ExchangeService instance and set its credentials:

$service =  New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)
$service.Credentials = New-Object System.Net.NetworkCredential("someone@infinitec.de", "password", "domain");

Now we are ready to use AutoDiscover. But depending on your infrastructure, AutoDiscover might need to follow some redirections before it has discovered your CAS Server. Like in this case:

$service.AutodiscoverUrl("someone@infinitec.de");

Exception calling "AutodiscoverUrl" with "1" argument(s): "Autodiscover blocked a potentially insecure redirection to https://autodiscover-s.outlook.com/autodiscover/autodiscover.xml. To allow Autodiscover to follow the redirection, use the AutodiscoverUrl(string, AutodiscoverRedirectionUrlValidationCallback) overload."
At line:1 char:25
+ $service.AutodiscoverUrl <<<< ("hkrause@infinitec.de");
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

This happens because the AutoDiscover process looks at autodiscover.infinitec.de and instead of an A record pointing to the AutoDiscover service, it finds a CNAME redirecting it to autodiscover.outlook.com. Because this might pose a security risk, the AutoDiscoverUrl method aborts  the discovery process and throws the Exception displayed above. The solution is also outlined: Instead of calling the method AutoDiscoverUrl(mailAddress) call the overload which expects a delegate as a second paramter. This delegate has a string as input and returns the $true if the discovery process should follow the redirection; false otherwise.

How can this overload be used with PowerShell? The answer is a ScriptBlock. If you simply want to allow the discovery process to follow all redirects, simply call it this way:

$service.AutodiscoverUrl("someone@infinitec.de", {$true})

But if you want to verify the discovery process is redirected to the correct url, use this version:

$TestUrlCallback = {
 param ([string] $url)
 if ($url -eq "https://autodiscover-s.outlook.com/autodiscover/autodiscover.xml") {$true} else {$false}
}

$service.AutodiscoverUrl("someone@infinitec.de", $TestUrlCallback)

You can embed whatever checks you need to verify the given url in the third line of the $TestUrlCallback method.


Posted by Henning Krause on Friday, July 22, 2011 5:30 PM, last modified on Sunday, July 24, 2011 1:22 AM
Permalink | Post RSSRSS comment feed

Using Streaming Notifications with Exchange 2010

That is the title of an article I wrote for Microsoft. It has been published here a few days ago:

Summary: Streaming notifications, which combine the functionalities of push and pull notifications, are new in Microsoft Exchange Server 2010 Service Pack 1 (SP1). This article explains how streaming notifications work and when to use them.

The code sample is missing, but that will be fixed in a few days.


Posted by Henning Krause on Friday, July 15, 2011 7:50 PM, last modified on Saturday, July 30, 2011 10:38 PM
Permalink | Post RSSRSS comment feed

Enumerating Sharepoint Connections in a mailbox with EWS

An interesting question on StackOverflow came up recently: Is it possible to get the SharePoint lists which are connected to an Exchange Mailbox? The data which is synchronized with Outlook is stored in a PST file on the local disk – so no interaction with Exchange on this end. But if a user logs on from another computer, the SharePoint list the user has subscribed to are synchronized there as well. So the configuration seems to be stored in the mailbox somewhere. And indeed, they are. Outlook creates a message item in the associated folder table in the users inbox. Associated items are not visible from Outlook, but they can be accessed using MAPI or EWS. It turns out that Outlook saves the SharePoint connections similar to the RSS feeds. So a good starting point is to have a look at the Sharing Message Object Protocol Specification which lists the properties used for these items.

The SharePoint configuration items have a MessageClass of IPM.Sharing.Index.In, and the property PidLidSharingProviderGuidProperty is set to {0006F0AD-0000-0000-C000-000000000046}.

The configuration data is stored on a few properties. The following method lists all SharePoint connections connected to a mailbox:

   1:  using System;
   2:  using System.Net;
   3:  using Microsoft.Exchange.WebServices.Data;
   4:   
   5:  namespace ExchangeTest
   6:  {
   7:      internal class Program
   8:      {
   9:          private static readonly Guid PropertySetSharing = new Guid("{00062040-0000-0000-C000-000000000046}");
  10:   
  11:          private static readonly ExtendedPropertyDefinition PidLidSharingProviderGuidProperty =
  12:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A01, MapiPropertyType.CLSID);
  13:   
  14:          private static readonly ExtendedPropertyDefinition SharingRemotePathProperty =
  15:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A04, MapiPropertyType.String);
  16:   
  17:          private static readonly ExtendedPropertyDefinition SharingLocalNameProperty =
  18:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A0F, MapiPropertyType.String);
  19:   
  20:          private static readonly ExtendedPropertyDefinition SharingRemoteNameProperty =
  21:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A05, MapiPropertyType.String);
  22:   
  23:          private static readonly ExtendedPropertyDefinition SharingBrowseUrlProperty =
  24:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A51, MapiPropertyType.String);
  25:   
  26:          private static readonly ExtendedPropertyDefinition SharingRemoteTypeProperty =
  27:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A1D, MapiPropertyType.String);
  28:   
  29:          private static readonly Guid SharePointProviderId = new Guid("{0006F0AD-0000-0000-C000-000000000046}");
  30:   
  31:          public static void Main(string[] args)
  32:          {
  33:              var service = new ExchangeService(ExchangeVersion.Exchange2010)
  34:                            {Credentials = new NetworkCredential(test@contoso.com, "Password!")};
  35:              
  36:              service.AutodiscoverUrl(test@contoso.com, url => true);
  37:   
  38:              var folder = Folder.Bind(service, WellKnownFolderName.Inbox);
  39:              var filter = new SearchFilter.SearchFilterCollection(LogicalOperator.And,
  40:                                                                   new SearchFilter.IsEqualTo(ItemSchema.ItemClass,
  41:                                                                                              "IPM.Sharing.Index.In"),
  42:                                                                   new SearchFilter.IsEqualTo(PidLidSharingProviderGuidProperty,
  43:                                                                                              SharePointProviderId.ToString()));
  44:              var view = new ItemView(512)
  45:                         {
  46:                             Traversal = ItemTraversal.Associated,
  47:                             PropertySet = new PropertySet(BasePropertySet.IdOnly,
  48:                                                           SharingRemotePathProperty, SharingBrowseUrlProperty,
  49:                                                           SharingLocalNameProperty, SharingRemoteNameProperty,
  50:                                                           SharingRemoteTypeProperty)
  51:                         };
  52:   
  53:              var items = folder.FindItems(filter, view);
  54:              foreach (var item in items)
  55:              {
  56:                  Console.Out.WriteLine("RemotePath = {0}", item.GetValueOrDefault<string>(SharingRemotePathProperty));
  57:                  Console.Out.WriteLine("BrowseUrl = {0}", item.GetValueOrDefault<string>(SharingBrowseUrlProperty));
  58:                  Console.Out.WriteLine("LocalName = {0}", item.GetValueOrDefault<string>(SharingLocalNameProperty));
  59:                  Console.Out.WriteLine("Remotename = {0}", item.GetValueOrDefault<string>(SharingLocalNameProperty));
  60:                  Console.Out.WriteLine("Type = {0}", item.GetValueOrDefault<string>(SharingRemoteTypeProperty));
  61:                  Console.Out.WriteLine(new string('=', 80));
  62:              }
  63:          }
  64:      }
  65:   
  66:      public static class ItemExtension
  67:      {
  68:          public static T GetValueOrDefault<T>(this Item item, PropertyDefinitionBase property, T defaultValue = default(T))
  69:          {
  70:              T result;
  71:              return item.TryGetProperty(property, out result) ? result : defaultValue;
  72:          }
  73:      }
  74: }

This method dumps the configuration of all SharePoint connections to the console.

To use this method, you’ll need .NET 4. If you are running .NET 2.0, you’ll have to adapt it.

Additionally, this won't work with Exchange 2007, because EWS in that version does not allow a FindItems call on the associated items table. WebDAV is the API of choice in this case.


Posted by Henning Krause on Wednesday, July 13, 2011 9:05 PM, last modified on Wednesday, July 13, 2011 9:05 PM
Permalink | Post RSSRSS comment feed

Correlating a Non-Delivery-Notification to its original message with the Exchange Managed API

If you are automatically process emails in an Exchange Mailbox, you might also need to process non delivery reports (NDRs) and correlate these NDRs to their original values. Luckily, Exchange does most of the heavy lifting, as long as the NDR conforms to the RFC 3461, which defines the proper structure for an NDR that can be automatically parsed. Not all MTAs (Mail transfer agents) do generate these types of NDRs.

First of all, you need to check whether the mail item in question is actually a non delivery report. This is done by examining the Item Class of the mail. An NDR has an item class of REPORT.IPM.Note.NDR. Next, you need to retrieve the property PidTagParentKey from the item. This property contains the PidTagSearchKey of the original mail.

Unfortunately, the PidTagSearchKey cannot be used to bind to the item directly. Instead, you have to issue a FindItem operation to get it. Normally, sent items are all stored in the SentItems folder, so this narrows the search down.

Here is a sample method which describes the process:

 

   1:  class Program
   2:  {
   3:      private static readonly ExtendedPropertyDefinition PidTagParentKeyProperty = new ExtendedPropertyDefinition(0x25, MapiPropertyType.Binary);
   4:      private static readonly ExtendedPropertyDefinition PidTagSeachKeyProperty = new ExtendedPropertyDefinition(0x300B, MapiPropertyType.Binary);
   5:   
   6:   
   7:      private static bool TryGetOriginalMail(string ndrId, ExchangeService service, out Item originalItem)
   8:      {
   9:          byte[] value;
  10:   
  11:          var item = service.BindToItems(new[] {new ItemId(ndrId)},
  12:                                          new PropertySet(BasePropertySet.IdOnly,
  13:                                                          PidTagParentKeyProperty, ItemSchema.Subject)).First().Item;
  14:   
  15:          if (!item.TryGetProperty(PidTagParentKeyProperty, out value))
  16:          {
  17:              Console.Out.WriteLine("Correlationtoken not found.");
  18:              originalItem = null;
  19:              return false;
  20:          }
  21:   
  22:          originalItem = service
  23:              .FindItems(WellKnownFolderName.SentItems,
  24:                          new SearchFilter.IsEqualTo(PidTagSeachKeyProperty, Convert.ToBase64String(value)),
  25:                          new ItemView(1) {PropertySet = PropertySet.FirstClassProperties})
  26:              .FirstOrDefault();
  27:   
  28:          return true;
  29:      }
  30:  }

Posted by Henning Krause on Monday, June 27, 2011 8:12 PM, last modified on Monday, June 27, 2011 8:12 PM
Permalink | Post RSSRSS comment feed

Searching a meeting with a specific UID using Exchange Web Services 2007

For most items, the item id (which is based on the MAPI entry id) is a good candidate for a long-term identifier. As long as the item is not moved between folders, it remains stable. At least for most items. This is, however, not true for meetings. Exchange recreates meeting items under certain circumstances and the meeting items loose their original entry id, thus the item id is also modified. But a meeting has a different property which uniquely identifies it, even across mailbox boundaries: The UID property. This property is available as a first-class property since Exchange 2007 Service Pack 1. It is a string that looks like this:

   1: 040000008200E00074C5B7101A82E008000000007069352667BBC9010000000000000000100000000C74ABD802575A41BC09B0E12352657B

Since this property is not an Item Id, the GetItem operation cannot be used to bind to a meeting element directly. Sadly, the FindItem operation doesn’t work either, because the Exchange WebServices do not accept a restriction that is based on the UID property.

Luckily, the UID can be read and searched via a MAPI property: The GlobalObjectId. This is a binary property and when fetched looks like this:

   1: BAAAAIIA4AB0xbcQGoLgCAAAAABwaTUmZ7vJAQAAAAAAAAAAEAAAAAx0q9gCV1pBvAmw4SNSZXs=

The value is the equivalent to the UID displayed above, but in a different format. The UID is formatted as a hex-string and the GlobalObjectId is displayes as a Base64 string. Using the following small function, the first representation can be converted into the latter:

   1: private static string GetObjectIdStringFromUid(string id)
   2: {
   3:     var buffer = new byte[id.Length/2];
   4:     for (int i = 0; i < id.Length/2; i++)
   5:     {
   6:         var hexValue = byte.Parse(id.Substring(i*2, 2), NumberStyles.AllowHexSpecifier);
   7:         buffer[i] = hexValue;
   8:     }
   9:     return Convert.ToBase64String(buffer);
  10: }

The value returned from the GetObjectIdStringFromUid can now be used to execute a FindItem operation, specifying a restriction on the GlobalObjectId property. Here is the whole function:

   1: static void Main(string[] args)
   2: {
   3:     ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
   4:  
   5:     const string id = "040000008200E00074C5B7101A82E008000000007069352667BBC9010000000000000000100000000C74ABD802575A41BC09B0E12352657B";
   6:  
   7:     string bufferString = GetObjectIdStringFromUid(id);
   8:  
   9:     var service = new ExchangeServiceBinding
  10:         {
  11:           Url = "https://w2k3x64/ews/exchange.asmx",
  12:           Credentials = new NetworkCredential("bob", "Password!", "contoso"),
  13:           RequestServerVersionValue = new RequestServerVersion { Version = ExchangeVersionType.Exchange2007_SP1}
  14:         };
  15:  
  16:     var response = service.FindItem(new FindItemType
  17:         {
  18:             ParentFolderIds = new[] { new DistinguishedFolderIdType { Id = DistinguishedFolderIdNameType.calendar } },
  19:             ItemShape = new ItemResponseShapeType
  20:                 {
  21:                     BaseShape = DefaultShapeNamesType.AllProperties,
  22:                 },
  23:             Restriction = new RestrictionType {
  24:                 Item = new IsEqualToType
  25:                     {
  26:                       Item = new PathToExtendedFieldType
  27:                         {
  28:                            DistinguishedPropertySetId = DistinguishedPropertySetType.Meeting,
  29:                            DistinguishedPropertySetIdSpecified = true,
  30:                            PropertyId = 0x03,
  31:                            PropertyIdSpecified = true,
  32:                            PropertyType = MapiPropertyTypeType.Binary
  33:                         },
  34:                       FieldURIOrConstant = new FieldURIOrConstantType
  35:                         {
  36:                           Item = new ConstantValueType
  37:                           {
  38:                               Value = bufferString
  39:                           }
  40:                         }
  41:                     }
  42:                 }
  43:         });
  44:     var messageType = ((FindItemResponseMessageType) response.ResponseMessages.Items[0]);
  45:     Console.Out.WriteLine("messageType.ResponseClass = {0}", messageType.ResponseClass);
  46:  
  47:     if (messageType.ResponseClass != ResponseClassType.Success)
  48:     {
  49:         Console.Out.WriteLine(messageType.MessageText);
  50:         return;
  51:     }
  52:     var returnedItems = messageType.RootFolder.Item as ArrayOfRealItemsType;
  53:  
  54:     if (returnedItems == null || returnedItems.Items == null)
  55:     {
  56:         Console.Out.WriteLine("Nothing found.");
  57:         return;
  58:     }
  59:  
  60:     foreach (CalendarItemType item in returnedItems.Items)
  61:     {
  62:         Console.Out.WriteLine("item.Subject = {0}", item.Subject);
  63:         Console.Out.WriteLine("item.UID = {0}", item.UID);
  64:     }
  65:     Console.ReadLine();
  66: }

Posted by Henning Krause on Monday, April 13, 2009 11:45 PM, last modified on Sunday, May 8, 2011 8:37 AM
Permalink | Post RSSRSS comment feed

Resolving the primary email address with Exchange WebServices ResolveNames operation

To access another users mailbox with Exchange WebServices, the primary email address of that mailbox is required. Secondary email addresses just don’t work. But if that all one’s got, the secondary email address needs to be resolved to the primary email address. The ResolveNames operation seems a good candidate for that job. But feeding that operation with a secondary email address will yield no results.

The ResolveNames operation executes an ANR search (Ambiguous name resolution) request on Active Directory, which translates (at least in part) to: (|(…)(proxyAddresses=address*)(…)). But the syntax of the proxyAddresses field is different: It is a multi-valued field where each entry has the form protocol:address. For normal email addresses, an entry looks like this: smtp:jdoe@contoso.com.

To resolve a secondary email address, the ResolveNames operation has to be called with something like smtp:jdoe@contoso.com.


Posted by Henning Krause on Monday, April 13, 2009 2:26 PM, last modified on Sunday, November 28, 2010 3:38 AM
Permalink | Post RSSRSS comment feed