Wednesday, February 22, 2006

Opening the Address Book from .NET

The Problem
In today’s world, applications often to need to send e-mail messages. The .NET framework has a class for sending e-mail messages (System.Web.Mail.SmtpMail) that works pretty well (it has some shortcomings, but that is a topic for another article). In order to send an e-mail to someone, you need an e-mail address. Most people keep their e-mail addresses in a MAPI-compliant address book, like Outlook, Outlook Express, etc. The .NET framework, however, does not have a mechanism for opening and retrieving e-mail addresses from this address book. This is not good.

The Typical Solution
The typical solution to this problem is to use Simple MAPI’s MAPIAddress method to display the address book. This method works for most situations, but has one major drawback. It only works for ASCII text. There is not a Unicode version of Simple MAPI interface. If you display the address book and select a person with a Japanese (Chinese, Thai, etc.) name, your application will see their name as a series of questions marks (????).

The Extended MAPI Solution
The only way to support Unicode names is to use Extended MAPI. Unfortunately, Microsoft does not support calling the Extended MAPI library from .NET (see KB 813349). I have not personally tried to call the IAddrBook::Address method from .NET, but I have read enough newsgroup articles to think that it is probably not worth the effort.

My solution to this dilemma was to create an ATL/COM/Active X dll that wrapped the Extended MAPI call. I then called this dll from my .NET application using COM Interop. This solution worked very well for what I wanted, but it adds an extra two dlls to my application (oh well).

The Code
Here is the ATL code I wrote that makes the Extended MAPI calls.

// EasyMapi.cpp : Implementation of CEasyMapi

#include "stdafx.h"
#include "EasyMapi.h"

#include
#include ".\easymapi.h"

// CEasyMapi

// Constructor
CEasyMapi::CEasyMapi()
{
   MAPIInitialize(NULL);
}

STDMETHODIMP CEasyMapi::AddressBook(VARIANT_BOOL* Cancel,
   BSTR* AddressesSelected)
{
   AFX_MANAGE_STATE(AfxGetStaticModuleState());

   // Local Variables
   HRESULT hResult = NULL;
   ULONG ulUIParam = NULL;
   ULONG ulFlags = 0;
   LPMAPISESSION lpSession = NULL;
   LPADRBOOK pAddressBook = NULL;
   ADRPARM adrparm;
   LPTSTR rglpszDestTitles[1];
   ULONG rgulDestComps[1];
   LPADRLIST pReceipients = NULL;
   CString ReceipientsList;

   // Begin
   *Cancel = VARIANT_FALSE;

   // Logon to MAPI
   ulFlags = MAPI_UNICODE | MAPI_LOGON_UI | MAPI_NO_MAIL |
      MAPI_EXTENDED;
   if(FAILED(hResult = MAPILogonEx(
      ulUIParam, NULL, NULL, ulFlags,
      (LPMAPISESSION FAR *) &lpSession )))
   {
      return hResult;
   }

   // Open AddressBook Object
   if(FAILED(hResult = lpSession->OpenAddressBook(
      ulUIParam, NULL, 0, (LPADRBOOK FAR *) &pAddressBook)))
   {
      return hResult;
   }

   // Display Dialog
   ulFlags = DIALOG_MODAL | MAPI_UNICODE;

   rglpszDestTitles[0] = (LPTSTR)("To");
   rgulDestComps[0] = MAPI_TO;

   adrparm.cbABContEntryID = 0;
   adrparm.lpABContEntryID = NULL;
   adrparm.ulFlags = ulFlags;
   adrparm.lpReserved = NULL;
   adrparm.ulHelpContext = 0;
   adrparm.lpszHelpFileName = NULL;
   adrparm.lpfnABSDI = NULL;
   adrparm.lpfnDismiss = NULL;
   adrparm.lpvDismissContext = NULL;
   adrparm.lpszCaption = (LPTSTR)"Address Book";
   adrparm.lpszNewEntryTitle = (LPTSTR)("");
   adrparm.lpszDestWellsTitle = (LPTSTR)("");
   adrparm.cDestFields = 1;
   adrparm.nDestFieldFocus = 0;
   adrparm.lppszDestTitles = rglpszDestTitles;
   adrparm.lpulDestComps = rgulDestComps;
   adrparm.lpContRestriction = NULL;
   adrparm.lpHierRestriction = NULL;

   if(FAILED(hResult = pAddressBook->Address(
      &ulUIParam, &adrparm, (LPADRLIST FAR *) &pReceipients)))
   {
      if (hResult == MAPI_E_USER_CANCEL)
      {
         *Cancel = VARIANT_TRUE;
         return S_OK;
      }
      return hResult;
   }

   // Parse Return Values
   if ((pReceipients != NULL) && (pReceipients->cEntries > 0))
   {
      for(unsigned int ReceipientIndex = 0;
         ReceipientIndex < pReceipients->cEntries;
         ReceipientIndex++)
      {
         if (pReceipients->aEntries[ReceipientIndex].cValues > 0)
         {
            if (ReceipientsList.GetLength() > 0)
            {
               ReceipientsList.Format(_T("%s, "),
                  ReceipientsList);
            }
            ReceipientsList.Format(_T("%s\"%s\" <%s>"),
ReceipientsList,
pReceipients->aEntries[ReceipientIndex].rgPropVals[1].Value.lpszW,
pReceipients->aEntries[ReceipientIndex].rgPropVals[2].Value.lpszW);
         }
      }
      *AddressesSelected = ReceipientsList.AllocSysString();
   }
   else
   {
      *AddressesSelected = NULL;
   }

   // Logoff MAPI (ignor any errors)
   lpSession->Logoff(0,0,0);

   return S_OK;
}


References
Differences between CDO, Simple MAPI, and Extended MAPI
http://support.microsoft.com/kb/q200018/

Microsoft Windows SDK - Simple MAPI
http://windowssdk.msdn.microsoft.com/library/en-us/mapi/
html/_mapi1book_simple_mapi.asp


Microsoft Windows SDK – MAPI – IAddrBook::Address
http://windowssdk.msdn.microsoft.com/library/en-us/mapi/
html/_mapi1book_iaddrbook_address.asp


Support policy for Microsoft Exchange APIs with the .NET Framework applications
http://support.microsoft.com/Default.aspx?id=813349

Saturday, February 18, 2006

Japan Mail Version 1.4

I made some updates to the Japanese e-mail program I wrote for my wife. The biggest update was to support address books containing Unicode (i.e. Japanese) names. The standard simple MAPI object does not support Unicode, so I had to use the extended MAPI library (not very fun). I hope to write up an article that discusses the pain I went through to get this to work properly in the near future.

Updated on 12/22/2010: I no longer have Japan Mail available for download because Office 2003 and above correctly encode Japanese e-mail messages. If for some reason you would like a copy, send me a note and I will e-mail it to you.

Wednesday, February 08, 2006

Duel Network Cards and the Default Gateway Setting

The Problem
We had a new Microsoft Windows Server 2003 Web Edition server from Dell that had a duel Intel(R) PRO/1000 MT Network Card install. We had one of the network card’s ports connected to our LAN and the other network card’s port connected to the DMZ. Occasionally the machine would stop responding to any traffic that came from the Internet. It would still respond to LAN traffic, but WAN traffic was blocked. We put a network sniffer on the network and confirmed the WAN/internet packets were getting to the server, but the server was not responding to the traffic. Disabling and re-enabling the network card would fix the problem.

The Cause
After searching through numerous newsgroup articles and websites, we called Microsoft’s technical support. Through talking with them, we discovered that Windows uses a single routing table rather than each network card having its own routing table. Each of our network cards had a default gateway defined. The DMZ network card had an internet default gateway and the LAN network card had a LAN default gateway. The DMZ network card (being in the DMZ) could not see the LAN default gateway. Since there is only one routing table for Windows, each network card essentially had two default gateways defined (the DMZ one and the LAN one). This was not a problem for the LAN card, because he could see both gateways, but this was a problem for the DMZ card, because he could only see one of the two default gateways. If, for some reason, the DMZ default gateway becomes unavailable, Windows will switch to the next default gateway in the list. In this case it would switch from the DMZ gateway to the LAN gateway, causing the DMZ network card to no longer work because it could not reach the LAN default gateway. It would continue in this state until the network card was reset, re-establishing the DMZ default gateway as the gateway that should be used.

The Solution
The solution to this problem is to only have one default gateway defined, even though there are two network cards on the server. The DMZ network card was set to use the DMZ default gateway and the LAN network card did not have any default gateway specified.

More Information
Multiple Default Gateways Can Cause Connectivity Problems
http://support.microsoft.com/default.aspx/kb/159168
Default Gateway Configuration for Multihomed Computers
http://support.microsoft.com/default.aspx/kb/157025

Tuesday, February 07, 2006

Windows XP with Asian Language Support – Textbox Caret Issue

Here is an issue that I have noticed (and has bugged me) for a long time, but I think I may be the only person who has noticed this because I never read about it on any newsgroups, blogs, or knowledge base articles. I believe there is a problem with textboxes in the English version Windows XP when Asian language support is enabled (specifically Japanese). If you are typing in a standard windows textbox and the caret gets to the end of the textbox, the caret appears to be either in the middle or to the left of the character instead of to the right of the character. This does not happen in the English version Windows XP when Asian language support is not enabled.

For Example:
If you were to type “abcdefghijkl” into a textbox and it completely filled up the textbox, it would look like the following on Windows XP with Asian support: abcdefghijk|l (the | represents the position of the caret). On Windows XP without Asian support it would look like the following: abcdefghijkl| (which is how it should look).


Example of a textbox with this issue. The carat is actually to the left of the "i" even though it appears on the right side.


This is the same textbox on a machine without Asian language support. As you can see, the carat appears on the right-hand side of the "i" character as you would expect.

If the character is wide (w, s, etc.) the caret appears in the middle of the character. If the character is narrow (i, l, etc.) the caret appears to the left of the character.

This only happens at the end of the textbox.

Am I the only person who has noticed this, or are there others of you out there? If you have noticed this and it bugs you, please leave a comment. I want to know how many of us there are out there.

ASCII Control Characters and Web Service Strings

The Problem
We had an interesting situation come up recently with one of the web services we were writing. This web service returned a string value back to its caller, but occasionally the code Visual Studio .NET generated to handle the web service call would raise the following error message: “There is an error in XML document (1, 285).” The underlying error message was: System.Xml.XmlException: ' ', hexadecimal value 0x11, is an invalid character. Line 1, position 351. This caused us to do a little investigation and we discovered that the returned string would sometimes contain a 0x11 (17) control character. We determined that this character should not have been there in the first place, but we were surprised that it should cause an error. We did a little experiment and discovered that any ASCII character in the range of 0x00 to 0x1F (0 to 31), with the exception of 0x09 (TAB), 0x0A (LF), and 0x0D (CR), would cause this error. This error did not occur in the web service itself, but in the code that processed the XML response returned by the web service. This code is generated automatically by Visual Studio, so there is not much we can do to control it.

XML String Limitation
We did a little more digging and discovered that string values in XML documents are not allowed to contain characters in the range 0x00 – 0x1F (0 – 31), except for 0x09 (TAB), 0x0A (LF), and 0x0D (CR). Details about this restriction are documented in the following locations:
http://www.w3.org/TR/xmlschema-2/#string
http://www.w3.org/TR/2000/WD-xml-2e-20000814#dt-character

.NET Inconsistency
After discovering this restriction on XML string values, I was no longer surprised the code that parsed the XML response from the web service raised an error. However, I was surprised that the web service method did not raise an error when we tried to return a string containing an invalid character. I would have though that the web service method would not be allowed to return data that would be invalid when it was converted into XML.

Things to Remember
  • XML strings cannot contain characters in the range 0x00 – 0x1F (0 – 31), except for 0x09 (TAB), 0x0A (LF), and 0x0D (CR).
  • Since XML strings cannot contain ASCII control characters, web service methods cannot send or receive strings that contain ASCII control characters.
  • If you do need to send a string containing ASCII control characters, you can pass it as a Char or Byte array to get around this restriction.