Jul
15

Automatic localization for ASP.NET websites

The solution consists in PowerShell scripts to automatically create resource files for your website's static content and translate the resource files in different languages (Google Translate API).

Direct download link: http://www.rahulsingla.com/sites/default/files/content/blog/asp.net-localization.zip

The scripts do the following:

  1. Go through your .aspx/.ascx files and add meta:resourcekey attributes to eligible tags.
  2. Create primary resource (.resx) files by extracting translatable attributes to these files from .aspx/.ascx files.
  3. Create secondary resource files for the desired locales you want to support in your ASP.NET application.
  4. Use Google Translate to automatically provide string translations in the secondary resource files.
  5. Extract translatable strings from javascript files.
  6. Create resource files again and provide automatic translation for these javascript strings through resource files.
  7. Create new javascript files (e.g. myfile.fr.js) which you can include on the page and your javascript strings wrapped in Rahul.t method call become translatable automatically.

To get started:

  1. Download the attached sample application, extract it and configure it in IIS. you can use it with Visual Studio's built-in Cassini web-server also.
  2. Open "resources\localize\Controller.ps1" file from the attached code. On 5th line, update the application's path.
  3. Open "resources\localize\GTranslate.ps1" file and on third line, update your Google Translate's API key. You can generate the same from here: https://code.google.com/apis/console/

Steps you need to perform to use this framework for yourself:

  1. Ensure you have Powershell 2.0 installed.
  2. Extract the scripts to a folder on your disk.
  3. Open Controller.ps1 in any text editor, and change line 5 to provide the path to root of your ASP.NET application.
  4. Next open ResourceFileGenerator.ps1 and change line 3 to include all locales you need to support.
  5. Then open GTranslate.ps1, and substitute your Google Translate API key in line 3. You can get the same at: https://code.google.com/apis/console/
  6. That's it, execute Controller.ps1 from Powershell console and see the messages that provide you the progress of the activity and what the scripts are doing.
  7. Open AddMetaTag.ps1 and change lines 8-10 to include all tags which should be definitely localized no matter they have an ID attribute or not. Please specify the tags in lowercase only together with the tag prefix if any.
  8. Open MetaTagExtractor.ps1 and change $localizableAttributes, $translatableAttributes, and $conditionallyTranslatable arrays/hashes on lines 9, 11 and 15 respectively.
    The comments in the file would help you understand what each of these array/hash is for.
  9. Open Controller.ps1 and change the paths at the bottom of the file if you need automatic localization/translation for a specific folder in your application only and not the whole application. You can also choose whether to process the directories recursively or not. Again comments there will help you.

They can also do javascript localisation.

Recently we used these scripts for a client project. We needed to do some customizations, but due to the clear documentation it was straightforward.

These scripts were developed by Rahul Singla. Thanks Rahul for the great work!

For more details, check the original blog posts:

http://www.rahulsingla.com/blog/2011/02/asp-net-using-powershell-and-google-translate-to-provide-automatic-localization

http://www.rahulsingla.com/blog/2011/06/asp-net-a-complete-translation-framework-through-powershell-and-google-translate

Posted by Voicu Matei | 1 Comment

Dec
15

Orbit One is now a Umbraco Certified Solution provider!

Since the beginning of December we are proud to be a Certified Solution Provider for Umbraco. Our delivery team now has Level 1 & Level 2 certifications.

image

Congratulations Thomas, Saskia and Bart for completing the exams with near-perfect scores!  

What is Umbraco?

Umbraco is an ASP.NET based Content Management System (CMS).

It’s the choice of many organizations because of its no-cost, open-source licensing, its simplicity, and its unlimited potential.  Umbraco provides a full-featured web content management system that is easy to use, simple to customize, and robust enough to run the sites such as www.aviapartner.aero  and www.oneoffice.be

Posted by Rik Helsen | 1 Comment

Nov
03

ASP.Net Exception Reporter

Orbit One has developed a unique solution that allows you to track and analyse “unhandled exceptions” in the code of web applications. “Unhandled exceptions” are problems that are not supposed to happen.
No system is perfect and environmental changes as well as different types of clients devices (browsers) can result in small issues.
You can make a friendly error page that explains the user an issue has happened, and invite him to try his action again.
If the problem remains, the user can click on a link and send an email to your support.
This email contains the unique reference number of the issue.
Your development team can monitor and fix the issues as soon as possible.
All issues are centralized in a database, allowing your team to quickly find detailed information about the problem.
This unique solution allows you to be efficient and provide high quality web applications.


elmahws1.png

You can find more information, the code, documentation and ready to use installers on Codeplex.

Posted by Wim De Coninck | 1 Comment

Sep
21

Format and validate phonenumbers in Dynamics CRM 4.0 Plugin

When importing data in Dynamics CRM 4.0 there is no client side code triggered, so to make sure you get phonenumbers (or anything else for that matter) in a clean format, you might want to consider creating a plugin that runs server side and generates an import failure to let the customer know some data is not valid in his imports.

This way the user knows when something went wrong and the correct data will still be imported.

To make this plugin work there is the issue of the country prefixes and areacodes that exist. Therefore it is necessary to create two new entities: Country and AreaCode.

The Country entity needs to contains the telephone prefix of that country (eg: belgium = 32, Netherlands = 33 …)

The AreaCode is contains a link to the country entity and contains the phone areacode. (eg: 9 for Ghent, Belgium …)

Ones this data is in CRM we can use it in our plugin through the webservice.

In the end the phonenumbers will be formatted in an outlook compatible format.

eg: +32 (9) 265.74.20

When using the installer inside the project on CodePlex the plugin will be registered for the create and update messages for lead, account and contact.

 

You can find the plugin on CodePlex in our Dynamics CRM 4.0 Plugins project

Posted by Wim De Coninck | Leave your feedback

Sep
08

Improved Duplicate Detection in Dynamics CRM

Why do I need it?

We've noticed the CRM Duplicate detection doesn't allow for much creativity, you can only create Duplicate Detection Rules with the following condition:

  • full string match
  • begins with (number of characters)
  • ends with (number of characters)


The problem with this is that people or companies aren't always entered in the same format. For instance if you look at our company name we would create the account with the name "Orbit One". Other people might write "Orbit One BVBA" (the bvba part is a type of business entity)
There are some other examples that come to mind:
"Dell NV" vs "Dell"
"HP Belgium" vs "HP"
"Gaëtan" vs "Gaetan"
"François" vs "Francois"
These exceptions will never be caught by the default CRM Duplicate detection.

How can I fix this?

We added the following attributes and filled the with data:

  • on account => new_matchname (contains the accounts name)
  • on contact => new_matchname (contains the contacts first + lastname)
  • on lead => new_matchcontact (contains the leads first+ lastname)
  • on lead => new_matchcompany (contains the lead company name)

The fields are filled with a normalized name. The process is:

  • to lower case and trim.
  • replace special characters (except parenthesis, a-z, 0-9, special characters like éèàç ..., and spaces) with empty string.
  • remove everything between the parentheses eg: "HP (was Compaq)" becomes "HP"
  • remove suffixes and prefixes that are type of business entity
  • remove all spaces
  • replace special characters (eg: é > e, à => a, ç => c, ñ => n, ...)

Next there need to be some new duplicate detection rules.

  • full string match on new_matchname for contact
  • full string match on new_matchname for account
  • full string match on new_matchcontact for lead (add companyname if needed in your specific cases)

The current implemented type of business entity which will be stripped are:

  • nv
  • sa
  • vzw
  • bvba
  • cvba
  • sarl
  • sprl
  • scrl
  • nvsa
  • bv
  • llc
  • gmbh
  • ltd
  • ag
  • kt
  • og
  • vof
  • cvoa
  • commva
  • commv
  • ebvba
  • ez
  • belgium

Duplicate Detection Jobs

In CRM you can add duplicate detection jobs by going to Settings > Data Management > Duplicate detection jobs

image

Create a new Duplicate detection job for for instance accounts. Make sure you filter on only active records. When you merge a duplicate record the record that isn’t the master record will be placed on inactive. So if you forget this filter you’ll keep getting the same duplicates ;-)

Once the job ran, you can see the duplicates entries.

image

What we think is another major pain in the a$$ is that you can only merge 2 records at a time. So not really in bulk… Maybe we will come up with a solution for this later :-)

 

You can find the Matchname plugin on CodePlex. There is an installer include so that you don’t need to worry about the plugin registration, creating the steps, …. More on this on CodePlex.

Posted by Wim De Coninck | Leave your feedback

Jun
23

Adding Fluent NHibernate support for Castle Windsor’s NHibernate Integration

We have used NHibernate as ORM mapper in many projects and to facilitate automatic session and transaction management we employed the Castle Windsor NHibernate Integration facility.

When using the Castle NHibernate Facility the configuration file for castle (castle.xml.config) looks like this:

image

Where the mapping assembly contains all the .hbm.xml files as embedded resources.

When the Fluent NHibernate framework appeared we started thinking about using it in our projects. But we wanted to do this with minimal changes to the Castle configuration schema (in the above screenshot).

The ideal situation would be to specify an assembly containing the Fluent NHibernate mapping classes instead of the assembly containing mapping xml files.

In the screenshot from above you can see in the  highlighted area an <assembly/> declaration. That assembly will be used by Castle NHibernate Facility’s default xml mapper.

We would like to make it possible to specify our own mapper which will make use of Fluent NHibernate to load the mapping classes from the specified assembly.

As the Fluent NHibernate framework is open-source, we considered making out custom build to add the needed functionality. Next I will describe the steps needed to implement and use the custom Fluent NHibernate mapper with Castle’s NHibernate Facility.

1. Download the necessary source files:

2. Create a new solution including the following projects:

  • Castle.Core
  • Castle.DynamicProxy
  • Castle.Facilities.NHibernateIntegration
  • Castle.MicroKernel
  • Castle.Services.Transaction
  • Castle.Windsor
  • FluentNHibernate
  • NHibernate.Mapping.Attributes-2.0

image

3. Create a new folder in the in the Castle.Facilities.NHibernateIntegration project. The project structure should look like the following:

image

4. In the previously created folder add the following:

  • An interface called INhibernateMapper This is the base for all the NHibernate mapper types.
  • A class named DefaultNHibernateMapper.
  • A class named AttributeNhibernateMapper.
  • A class named FluentNhibernateMapper.
    The class diagram is shown below:
     image

5. Implement the FluentNHibernateMapper:

 image The RegisterAssemblies method loads the assemblies specified in the <assembly/> section of the castle configuration file and registers the FluentNhibernate mapping classes with NHibernate.

 

6. Expand the Castle.Facilities.NHibernateIntegration project, look for the NHibernateFacility.cs file and locate the protected void RegisterAssemblies(Configuration cfg, IConfiguration facilityConfig) method.

image

Replace the method’s contents with the following code:

image

The method tests if a mapper is declared in the <assembly/> section of the castle configuration file. If yes, it will use it. If not it will use the DefaultNhibernateMapper (xml).

 

Conclusion:

The schema of the configuration file does not change if the preferred mapping type is xml. If the mappings are defined using FluentNHibernate then the castle configuration file transforms like the following:

image

The extra attribute – mapper  - specifies which IHNibernateMapper implementation to use for loading the NHibernate mappings.

The custom mappers can be implemented in a separate assembly. But the interface (IHNibernateMapper) needs to be located the Castle.Facilities.NHibernateIntegration assembly, because the entry point for loading the NHibernate mapping assemblies is the RegisterAssemblies method in the NHibernateFacility class.

The full source code can be downloaded here.

Posted by Voicu Matei | 1 Comment

Mar
28

Preserve created and modified information when importing a document in SharePoint.

I’m currently writing a web service on our SharePoint server to upload documents because one of our clients would like to migrate from a third party document management system to SharePoint.

This client would like to keep the legacy dates like the date of creation and the last modified date. After researching this, I still couldn’t find a way to do this. I noticed there are tools out there are able to do this so it is definitely possible.

So after some more trial and error I finally figured it out. And it wasn’t even that exotic, so I’d like to share how it is done.

When you’ve uploaded the file and checked it in you can change the dates with the following code:

public void UpdateFileDates(
string siteCollectionUrl, string site, 
string file, DateTime created, DateTime modified)
{
    if (siteCollectionUrl == null) 
       throw new ArgumentNullException("siteCollectionUrl");
 
    using (var spSite = new SPSite(siteCollectionUrl))
    using (var web = string.IsNullOrEmpty(site)
                        ? spSite.RootWeb
                        : spSite.OpenWeb(site))
    {
        var spFile = web.GetFile(file);
        if (spFile != null)
        {
            spFile.CheckIn("");
            var item = spFile.Item;
 
            if (item != null)
            {
            item["Created"] = created;
            item["Modified"] = modified;
 
            spFile.CheckOut();
            item.Update();
            //When you do overwritecheckin the 
	   // version doesn't change.
            spFile.CheckIn("", SPCheckinType.OverwriteCheckIn);
            }
        }
    }
}
Posted by Wim De Coninck | 1 Comment

Feb
27

Dynamics CRM 4.0 tool - Bulk update / export to csv

One of our customers recently asked to do an update on their instance of the microsoft CRM 4.0 platform.

The goal was to change the current Country Picklist to a lookup field which points to a new_country entity.

The new_country entity contains a few translations of the countries in different languages like english, dutch and french, and the ISO codes for each country.

Try to update that in CRM 4.0. So we start looking. The first thing I came across was the MSCRM 4.0 Bulk Data Export Tool

This tool allows you to export data generated by an advanced find view to a CSV file which you can later on import in CRM 4.0 trhough an update job. So I started playing around with it.

The major issue for me is that with that tool I couldn’t get the lookup field to be populated. I tried a bunch of different things like “account,E52B43A8-5FC6-4934-A5A3-96B2983EE140″ or just the “E52B43A8-5FC6-4934-A5A3-96B2983EE140″ but nothing seemed to work.

So I decided to create a new tool that basically does what i want, without creating an import job. Everything is done through the CRM SDK.

A few things I thought are very cool:

  • You can save your connection. We are a hosting company and we have 6 different CRM instances on 3 different servers, so this is quite nice.
  • One of our clients has 15.000+ records in their contacts. Because CRM returns only 5.000 records for each request, I can’t update them all in one go. This was changed by making multiple requests if necessary.
  • Access all the records and all the fields. In the MSCRM 4.0 Bulk Data Export Tool you can only use the tool on Advanced find views. I allow you to use the tool on the main entity. Which is really helpfull in some cases.
  • It allows you to choose the Authentication type. You can connect to CRM with Active Directory, SPLA, Passport (this last one I haven’t been able to test so if anybody has a CRM with passport please let me know)

It is still a work in progress but it does the job quite nice already so try it out.
The CRM Bulk Update / Export Tool

I hope it helps some of you in your daily (or not so daily) tasks.
I know it saved a colleague of mine about 80% of his time for only one case, and I consider that a big improvement.

Posted by Wim De Coninck | 48 Comments

Oct
06

Exchange webservice: Creating a distribution list

Hello again,

Today I would like to look into creating a Distribution List through the Exchange Webservice. This is not as straight forward as creating a contact, but it should work none the less. According to this book and the documentation found on the internet, it is not possible to accomplish this. But there is a way around. Lets take the scenic route to create a Distribution List J

Take a look at the code in the previous post. You can see that - in the method to create the contact - a CreateItemType is instantiated. If you look at the Items property of that variable you can see that the it expects an NonEmptyArrayOfAllItemsType which again has an Items Property. In that property we can store any ItemType we want except a Distribution List, as you can see here. But if we can not insert the DistributionList in a CreateItem, then how the f* are we going to get it to the exchange server. Well we are going to create an ItemType (base class) and use that to set all the properties needed so that exchange and outlook recognizes the Item as a distribution list. Because an ItemType is the base class, it is not provided with any of the properties we need to create a distribution list. That is where the Extended Properties come in handy. Everything needs to be set through those properties.

Here are some links with more information on the properties:

And you can also still use OutlookSpy which was linked in the previous post.
Now for some code.
The first thing I'd like to do is declare the path to extended field variables and set them to point to the properties we're going to use. I'll name them all starting with ptef so we can get the quickly through intellisense.

private static PathToExtendedFieldType ptefDisplayName = 
   new PathToExtendedFieldType
{
  PropertyTag = "0x3001",
  PropertyType = MapiPropertyTypeType.String
};

private static PathToExtendedFieldType ptefDistributionListName = 
   new PathToExtendedFieldType
{
  PropertyId = 0x8053,
  PropertyIdSpecified = true,
  DistinguishedPropertySetId = DistinguishedPropertySetType.Address,
  DistinguishedPropertySetIdSpecified = true,
  PropertyType = MapiPropertyTypeType.String
};

private static PathToExtendedFieldType ptefFileUnder = 
   new PathToExtendedFieldType
{
  PropertyId = 0x8005,
  PropertyIdSpecified = true,
  DistinguishedPropertySetId = DistinguishedPropertySetType.Address,
  DistinguishedPropertySetIdSpecified = true,
  PropertyType = MapiPropertyTypeType.String
};

private static PathToExtendedFieldType ptefMembers = 
   new PathToExtendedFieldType
  {
  PropertyId = 0x8055,
  PropertyIdSpecified = true,
  DistinguishedPropertySetId = DistinguishedPropertySetType .Address,
  DistinguishedPropertySetIdSpecified = true,
  PropertyType = MapiPropertyTypeType.BinaryArray
};

private static PathToExtendedFieldType ptefOneOffMembers = 
   new PathToExtendedFieldType 
{
  PropertyId = 0x8054,
  PropertyIdSpecified = true,
  DistinguishedPropertySetId = DistinguishedPropertySetType.Address,
  DistinguishedPropertySetIdSpecified = true,
  PropertyType = MapiPropertyTypeType.BinaryArray
};

Let's take a look at these PathToExtendedFieldTypes:

  • ptefDisplayName: The name of the Distribution List
  • ptefDistributionListName: The name of the Distribution List
  • ptefFileUnder: Must be the same as the name of the Distribution List
  • ptefMembers: The members extended property: This i a list of EntryIds of the objects corresponding to the members of the personal distribution list. Members of the personal distribution list can be other distribution lists, electronic addresses contained in a contact, global address list users or distribution lists, or one-off e-mail addresses. The format of each EntryId MUST be either a one-off EntryId or a Wrapped Entry Id. When setting this property, be sure to ensure its total size is less than 15,000 bytes. Since this property is a BinaryArray type it expects a string Array of Base64 strings. More info can be found in the Protocol Specifications on the MSDN site.
  • ptefOneOffMembers: The property specifies the list of one-off EntryIds corresponding to the members of the personal distribution list. These one-off EntryIds encapsulate display names and e-mail addresses of the personal distribution list members. For each entry in this property, there should be an entry in the Members property. The size of this property must also be less than 15,000 bytes. Since this property is a BinaryArray type it expects a string Array of Base64 strings.

How the member entry id and the one off member entry id strings are built can also be found in the protocol specifications.
I'll show you how I created the Member Entry Id:
First off you'll need the ItemType of the contact we created in the previous post. To do that we can use the FindItemMethod or simply return the ItemType in the previous posts CreateContact method. So let us refactor that method a little bit:

private static ContactItemType CreateContact(string pGivenName, string pSurname, Gender pGender, string pTitle)
{
  ContactItemType retval = null;
  ContactItemType contact = new ContactItemType();
  contact.GivenName = pGivenName;
  contact.Surname = pSurname;
  contact.DisplayName = contact.Subject = pGivenName + " " + pSurname;
  contact.EmailAddresses = new EmailAddressDictionaryEntryType[1];
  EmailAddressDictionaryEntryType email1 = 
    new EmailAddressDictionaryEntryType();
  email1.Key = EmailAddressKeyType.EmailAddress1;
  email1.Value = "testUser@example.com";
  contact.EmailAddresses[0] = email1;
  contact.ExtendedProperty = new ExtendedPropertyType[2];
  ExtendedPropertyType gender = new ExtendedPropertyType();
  gender.ExtendedFieldURI = new PathToExtendedFieldType();
  gender.ExtendedFieldURI.PropertyTag = "0x3a4d";
  gender.ExtendedFieldURI.PropertyType = MapiPropertyTypeType.Short;
  gender.Item = ((int)pGender).ToString();
  ExtendedPropertyType title = new ExtendedPropertyType();
  title.ExtendedFieldURI = new PathToExtendedFieldType();
  title.ExtendedFieldURI.PropertyTag = "0x3a45";
  title.ExtendedFieldURI.PropertyType = MapiPropertyTypeType.String;
  title.Item = pTitle;
  contact.ExtendedProperty[0] = gender;
  contact.ExtendedProperty[1] = title;
  CreateItemType createItem = new CreateItemType();
  createItem.Items = new NonEmptyArrayOfAllItemsType
    { 
      Items = 
new[] { contact }
    };

  DistinguishedFolderIdType folder = 
    new DistinguishedFolderIdType();
  folder.Id = DistinguishedFolderIdNameType.contacts;
  TargetFolderIdType targetFolder = new TargetFolderIdType();
  targetFolder.Item = folder;
  createItem.SavedItemFolderId = targetFolder;
  var response = esb.CreateItem(createItem);
  //There is only one item in the createItem 
  //so the line below should work in this case

  if (response.ResponseMessages.Items[0].ResponseClass == 
      ResponseClassType.Success)
  {
    ItemInfoResponseMessageType rmt = 
      (
ItemInfoResponseMessageType)response.ResponseMessages.Items[0];
    retval = (ContactItemType)rmt.Items.Items[0];
  }
  return retval;
}

To create a Distribution List with members you will need to find the EntryId when all we have is the ItemId and we want it in a Hex format. To achieve this I want to use the ConvertId Method on the ExchangeServiceBinding. The convertId Method accepts a ConvertIdType as a parameter. The ConvertIdType properties that are used here are the sourceIds and the destinationFormat. They kinda speak for themselves.

Lets take a look at some code again.

private static string CreateMemberEntryId(ItemType item)
{
  byte[] retval = new byte[0];
  string WrappedEntryIDPrefix = 
      "00000000C091ADD3519DCF11A4A900AA0047FAA4C3";
  ConvertIdType convertReq = new ConvertIdType();
  convertReq.DestinationFormat = IdFormatType.HexEntryId;
  convertReq.SourceIds = new[]
  {      new AlternateIdType()
     {
        Format = IdFormatType.EntryId,
        Id = item.ItemId.Id
     }
  };
  try
  {
    ConvertIdResponseType response = esb.ConvertId(convertReq);
    ArrayOfResponseMessagesType aormt = response.ResponseMessages;
    ResponseMessageType[] rmta = aormt.Items;
    foreach (ConvertIdResponseMessageType resp in rmta)
    {
      if (resp.ResponseClass == ResponseClassType.Success)
      {
        ConvertIdResponseMessageType cirmt = resp;
        AlternateIdType myId = (cirmt.AlternateId as AlternateIdType);
        if (myId != null)
        {
        //Strip the last 48 bytes (96 chars) 
        //this is actually the folderId.

        //Strip the first 7 bytes.
        //then remove 1 byte right after the actual entryId (40 bytes)
        //then the ChangeKey follows (in the rest of the string).
           StringBuilder sb = new StringBuilder();
           sb.Append(WrappedEntryIDPrefix);
           sb.Append("00000000");
           sb.Append(myId.Id.Substring(14, 84));
           sb.Append(myId.Id.Substring(100, myId.Id.Length - 196));
           retval = sb.ToString().ToByteArray();
        }
      }
      else if (resp.ResponseClass == ResponseClassType.Error)
      {
      //TODO error Logging
      }
      else
      {
      //TODO warning logging
      }
    }
  }
  catch (Exception e)
  {
    //TODO error logging
  }
  return Convert.ToBase64String(retval);
}

You can see in previous code block that there is some comment:
Strip the last 48 bytes (96 chars) - this is actually the folderId.
Strip the first 7 bytes.
Then remove 1 byte right after the actual entryId (40 bytes)
Then the ChangeKey follows (in the rest of the string).

Don't pay to much attention to this for the moment. If you want more information about this check out [MS-OXOCNTC]: Contact Object Protocol Specification. The WrappedEntryIDPrefix is a fixed string except the last 2 characters. What it stands for you see in the protocol specification but in the MS-OXOMSG pdf.

They represent a bit settings which specifies what kind of email address the entry id is going to be. So if you change that you could be talking about another distribution list for instance.

We are going to use this MemberEntryId and Members extended property of a distribution list. As said before we'll need the OneOffMembers entry's also so we can put them in the same position as the MemberEntryId. The OneOffMembers entry is a bit less complicated.

private static string CreateOneOffMemberEntryId(ContactItemType contact)
{
  var retval = new List<byte>();
  var flags = Encoding.Unicode.GetBytes("");
  var version = Encoding.Unicode.GetBytes("");
  var pad = Encoding.Unicode.GetBytes("");
  var muid = new byte[]
  {
    0x81, 0x2b, 0x1f, 0xa4, 0xbe,
    0xa3, 0x10, 0x19, 0x9d, 0x6e,
    0x00, 0xdd, 0x01, 0x0f, 0x54, 0x02
  };
  var wFlags = new byte[] {0x01, 0x90};
  var first = Encoding.Unicode.GetBytes(contact.Subject + "(" + contact.EmailAddresses[0].Value + ")");
  var middle = Encoding.Unicode.GetBytes("UNKNOWN");
  var last = Encoding.Unicode.GetBytes(contact.EmailAddresses[0].Value);
  retval.AddRange(flags);
  retval.AddRange(muid);
  retval.AddRange(version);
  retval.AddRange(wFlags);
  retval.AddRange(first);
  retval.AddRange(pad);
  retval.AddRange(middle);
  retval.AddRange(pad);
  retval.AddRange(last);
  retval.AddRange(pad);
  return Convert.ToBase64String(retval.ToArray());
}

Here we only need some actual strings from the contact to create the One Off Members entry. All the values except for wFlags, first, middle and last have fixed values. The wFlags should be changed to 0x8001 if you want to specify a distribution list. So it should read like this:

var wFlags = new byte[] {0x01, 0x80};
var first = Encoding.Unicode.GetBytes(distributionlist.Subject);
var middle = Encoding.Unicode.GetBytes("MAPIPDL");
var last = Encoding.Unicode.GetBytes("Unknown");

Now to actually create a distribution list with members we'll need to create an Item that has all the correct Extended Properties:

private static void CreateDistributionList(string name, string description, ContactItemType contact)
{
  ItemType list = new ItemType();
  list.ItemClass = "IPM.DistList";
  list.Subject = name;
  list.Body = new BodyType 
   { BodyType1 = BodyTypeType.Text, 
   Value = description };

  List<string> members = new List<string>();
  List<string> oneOffMembers = new List<string>();
  members.Add(CreateMemberEntryId(contact));
  members.Add(CreateOneOffMemberEntryId(contact));
  List<ExtendedPropertyType> eProps = 
    new List<ExtendedPropertyType>();
  eProps.Add(new ExtendedPropertyType
  {
    ExtendedFieldURI = ptefDisplayName,
    Item = name
  });
  eProps.Add(new ExtendedPropertyType
  {
    ExtendedFieldURI = ptefDistributionListName,
    Item = name
  });
  eProps.Add(new ExtendedPropertyType
  {
    ExtendedFieldURI = ptefFileUnder,
    Item = name
  });
  eProps.Add(new ExtendedPropertyType
  {
    ExtendedFieldURI = ptefMembers,
    Item = new NonEmptyArrayOfPropertyValuesType
    {
      Items = members.ToArray()
    }
  });
  eProps.Add(new ExtendedPropertyType
  {
    ExtendedFieldURI = ptefOneOffMembers,
    Item = new NonEmptyArrayOfPropertyValuesType
  {
    Items = oneOffMembers.ToArray()
  }
  });
  list.ExtendedProperty = eProps.ToArray();
}

UPDATE

I recently noticed that their are some errors in the CreateMemberEntryId. The best method is to retrieve the contact from the exchange store and ask for the entryId in the extended properties. In my experience this has much more performance and the created WrappedEntryId is correct. So the code will be more like following:

private string GetMemberEntryId(ItemType item, EmailAddressKeyType emailAddressKey)
    {
      if (item == null)
        throw new ArgumentNullException("item");
 
      string WrappedEntryIDPrefix = null;
      if (item is ContactItemType)
      {
        switch (emailAddressKey)
        {
          case EmailAddressKeyType.EmailAddress2:
            WrappedEntryIDPrefix = "D3";
            break;
          case EmailAddressKeyType.EmailAddress3:
            WrappedEntryIDPrefix = "E3";
            break;
          default:
            WrappedEntryIDPrefix = "C3";
            break;
        }
      }
      if (item is DistributionListType)
        WrappedEntryIDPrefix = "B4";
 
      WrappedEntryIDPrefix = "00000000C091ADD3519DCF11A4A900AA0047FAA4" + WrappedEntryIDPrefix;
      var retval = WrappedEntryIDPrefix +
         Convert.FromBase64String(item.ExtendedProperty
         [item.ExtendedProperty.Length - 1].Item
         .ToString()).ToMyString();
      return Convert.ToBase64String(retval.ToByteArray());
    }

The ToMyString() extension method:
public static string ToMyString(this byte[] bytes)
    {
      StringBuilder sb = new StringBuilder();
 
      bytes.ForEach(b => sb.Append(b.ToString("X").PadLeft(2,'0').ToUpper()));
 
      return sb.ToString();
    }
 

UPDATE 2:

Microsoft recently released the Update Rollup 4 for Exchange Server 2007 Service Pack 1

In this document you will see a link that directs you to the description of improvements in functionality that occur in Exchange Web Services operations

It reads: When you use this operation, it returns items that do not have a strong schema type as "messages" instead of as "items."

From this we can deduce that the code above has to change a bit. In the createDistributionList we used an ItemType. This has to change to a MessageType. An other thing we need to set is the MessageDispositionType to the CreateItem. This will be SaveOnly in most cases (especially when we are talking about distribution lists).

 

As you can probably see in the code this is not ready to ship...

There is alot more exception handling to be done and so forth, but in order to keep this document a bit more to the point I've skipped those parts.

Posted by Wim De Coninck | 3 Comments

Sep
25

Exchange webservice: Adding a contact

During the past 2 weeks or so I've been experimenting with the Exchange Webservice (in Exchange 2007 SP1).

I've been trying to create a contact in the contacts folder of a user (testuser). I came accross some problems while trying to accomplish this. Specifically when I tried to set properties that are actually used by Outlook (client properties) and which are not necessarily used by Exchange 2007. But you can still access and even create properties all you want. Lets kick this blog off with the basics of working with the ews (Exchange WebService).

Some ground work:

  • Create a new console app.
  • Add the Web Reference to your Exchange Webservice (eg.: http://myMailServer/ews/Exchange.Asmx) and lets name it ExchangeService.
  • Add the using to the ExchangeService

Now we can go and add a private member

private static ExchangeServiceBinding esb;

Initialize the ExchangeServiceBinding. You can insert credentials here in case of a https connection as seen in the example below.

esb = new ExchangeServiceBinding
{
   Credentials = new NetworkCredential
   {
      Domain = "example.com",
      UserName = "testUser",
      Password = "p455w0rd"
   },
   Url = https://192.168.2.143/EWS/Exchange.asmx
};
esb.RequestServerVersionValue = new RequestServerVersion();
esb.RequestServerVersionValue.Version = 
   ExchangeVersionType.Exchange2007_SP1;

As you can see in above code you need to set the server version, please do not ommit these 2 lines. If you do your code probably won't work. In some cases you will get an error like this: 

System.Web.Services.Protocols.SoapException: The request is valid but does not specify the correct server version in the RequestServerVersion SOAP header. Ensure that the RequestServerVersion SOAP header is set with the correct RequestServerVersionValue.

Now that we have the base of the application we can start concentrating on creating a contact, so we're going to create a method that does just that for us.

private static void CreateContact (string givenName, string surname)
{
  var contact = new ContactItemType();
  contact.GivenName = givenName;
  contact.Surname = surname;
  contact.DisplayName = contact.Subject = givenName + " " + surname;
  contact.EmailAddresses = new EmailAddressDictionaryEntryType[1];
  var email1 = new EmailAddressDictionaryEntryType();
  email1.Key = EmailAddressKeyType.EmailAddress1;
  email1.Value = "testUser@example.com";
  contact.EmailAddresses[0] = email1;
  var createItem = new CreateItemType();
  createItem.Items = new NonEmptyArrayOfAllItemsType
    {
       Items = new[] { contact }
    };
  var folder = new DistinguishedFolderIdType();
  folder.Id = DistinguishedFolderIdNameType.contacts;
  var targetFolder = new TargetFolderIdType();
  targetFolder.Item = folder;
  createItem.SavedItemFolderId = targetFolder;
  var response = esb.CreateItem(createItem);
}

You'll see the contact being created in you testUser's contacts folder.
If you don't fill out the DisplayName, the contact will complain about it in outlook when you open the contact and then close it without editing anything. Outlook will try to save the contact item although we did not change anything.

This was fairly easy to achieve, but just try to fill out all the contact's porperties you need. You'll soon find out that you need to use Extended Properties, because not all the properties are available on the ContactItemType type. I'll show you how to add 2 Extended Properties. Firstly the Gender of the contact and secondly the Title. Insert following somewhere before the instantiation of the CreateItemType createItem.

contact.ExtendedProperty =  new ExtendedPropertyType[2];
var gender = new ExtendedPropertyType();
gender.ExtendedFieldURI = new PathToExtendedFieldType();
gender.ExtendedFieldURI.PropertyTag = "0x3a4d";
gender.ExtendedFieldURI.PropertyType = MapiPropertyTypeType.Short;
gender.Item = "1";
var title = new ExtendedPropertyType();
title.ExtendedFieldURI = new PathToExtendedFieldType();
title.ExtendedFieldURI.PropertyTag = "0x3a45";
title.ExtendedFieldURI.PropertyType = MapiPropertyTypeType.String;
title.Item = "Dr.";
contact.ExtendedProperty[0] = gender;
contact.ExtendedProperty[1] = title;

As you can see both these Extended Properties have a PropertyTag. This is not always the case. In other properties you'll need to use the PropertyId and DistinguisedPropertyId, but we'll see samples of that in a later post in which I am going to discuss how you need to create a DistributionList with the contact created here as the only member.

The Property Tags and Ids can be found in the pdf's concerning the exchange protocol on MSDN or through a nice tool that helped me alot these past weeks: OutlookSpy.

That's it for now. I hope it was clear and helpfull in some way. If you have questions or remarks, do not hesitate to post a comment.

Posted by Wim De Coninck | 11 Comments

 Next >>
Orbit One on LinkedIn
Contact us - Raas Van Gaverestraat 83, 9000 Gent, Belgium - Tel. +32 (9) 330.15.00 - Privacy Statement - Sitemap - Sign In Developed with Microsoft Office SharePoint Server 2007