This is some sort of follow up to yesterdays post about custom OWA forms. If a user opens an OWA form for editing, OWA opens the edit form for the item in a new window. In most forms, a click on the “Save” button will also close this window and OWA will reload the preview window as well as the items list via a custom AJAX call. Unfortunately, Microsoft has not documented how to do this for custom forms, so the method described here is to be considered totally unsupported, and it may stop to work with any future service pack or roll-up.
1: window.opener.ref(); // refresh the list view
2: window.opener.udRP(1); // refresh the reading pane
3: window.close(); // close the edit window
If you followed my steps from the last post, you can use a standard button (or link button) to create your own save button. In that case, use the following code to emit the required script:
1: protected void SaveButton_Click(object sender, EventArgs e)
3: if (!Page.IsValid)
8: // Do what is necessary to save the changes made by the user
With Service Pack 1, Microsoft introduced some features with Outlook Web Access that allows a developer to create custom forms. These custom forms can be used to display and edit items with custom message classes, much like Outlook does. Details of these features can be found on MSDN here and here and fellow MVP Glen Scales has an example here. Basically, all one needs to do is creating some formulas (in the form of .aspx files) and a registry.xml, describing the mapping of IPM message classes (say IPM.Note.Custom) to these formulas. Simple enough. And since aspx files are easy to develop, it should be a piece of cake to create a full-fledged solution.
So, here is the catch – To retain a single-sign on experience, all the forms have to be put in a directory beneath the directory C:\Program Files\Microsoft\Exchange Server\ClientAccess\Owa\forms. Then, a web-application has to be created that runs in the default OWA application pool (MSExchangeOWAAppPool). This way, OWA will handle all the authentication stuff and you can use it to call Exchange WebServices. The problem with this approach is this: OWA uses an HttpModule (Microsoft.Exchange.Clients.Owa.Core.OwaModule, implemented in the assembly Microsoft.Exchange.Clients.Owa.dll) that inspects every request that is made to a file beneath the /owa virtual directory. At least, every file that is controlled by the ASP.NET runtime. Any direct request will be rejected. This leads to two problems:
- When the custom form is rendered in the client browsers, the forms action attribute will point to something like “/owa/forms/customform/editform.aspx”. Any attempt to post back to this location will fail.
The solution to the first problem is quite simple: ASP.NET allows the modification of the form tag from a code-behind file:
1: protected override void OnLoad(EventArgs e)
4: if (Form.Action.IsNullOrEmpty())
6: Form.Action = "/owa/" + Request.Url.Query;
The important part is implemented in lines 4 to 7. This adjusts the action attribute of the form element to point to a direction OWA accepts. The lines 8 and 9 ensure that the form is always executed again. When omitted, OWA will gladly cache the files for quite some time – even if the underlying element changes.
The second problem does not have such an elegant solution. In fact, it now gets quite messy. Since OWA blocks access to the WebResource.axd handler, the only solution I found is to create a new web application project, start it up and manually download the script referenced in the standard aspx file. Once done, give the script a frindly name (something like DefaultScript.js) and put it in the folder of your custom forms and reference the script file from within your custom form with a <script> tag:
Far from nice, but it works.
Exchange 2007 introduced a new URL format (Constructing OWA 2007 item ids from WebDAV items) which contained an arbitrary item id, which was based on the EntryId of the item. The format was this:
|Length ||Meaning |
|1 ||Length of the structure |
|sizeof(EntryId) ||EntryId |
|1 ||Item type |
This format changed with Exchange Service Pack 1. The layout is now this:
|Length ||Meaning |
|4 ||Length of the user's email address |
|sizeof(EmailAddress) ||Email address specifying the mailbox which contains the item |
|4 ||Size of the EntryId |
|sizeof(EntryId) ||The EntryId of the item |
This layout also applies to folder ids within a mailbox.
2: <BinaryId="ManagedCustomAction"SourceFile="Include\ManagedCustomAction.dll" />
3: <ManagedCustomActionId="test"BinaryKey="ManagedCustomAction"Type="ManagedCustomAction.CustomAction"Execute="immediate"xmlns="http://schemas.infinitec.de/wix/MCAExtension" />
5: <ManagedAction="test"After="CostFinalize"SequenceTable="InstallUISequence" />
For Exchange 2000 and Exchange 2003 prior Service Pack 1, the building of an URL suitable for WebDAV requests against a users mailbox is a rather complicated thing. A solution for this is descrbied under the section Solution.
From Exchange 2003 SP 1 onwards, you can just use his SMTP address to get access to his mailbox. For example, to access the mailbox of John Doe (firstname.lastname@example.org) you would use the url
To build the webaddress, you must do the following:
- If you only have a domain\username, not the distinguished name of the user, you must first get the latter one. See this aricle how to retrieve the name.
- Get the directory entry with the distinguished name of the user
- From that object, retrieve the property homemdb. This property contains the distinguished name of the mailbox store.
- Retrieve the directory entry of the mailbox store.
- From this object, retrieve the property named msExchOwningServer. This is the distinguished name of the Exchange server that hosts the mailbox.
- Get the root of the http virtual server on the exchange server. The distinguished name is CN=http, CN=Protocols, <distinguished name of the exchange server>.
- Search for all http virtual server (LDAP filter: (objectClass=protocolCfgHttpServer), Scope: OneLevel, retrieve these properties: msExchServerBindings, msExchDefaultDomain).
- From this list, retrieve the default SMTP server. It's the one without the attribute msExchDefaultDomain.
- Get the default SMTP domain of the organization:
- Open the node CN=Default Policy, CN=Recipient Policies, CN=<Name of your organization>, CN=Microsoft Exchange, CN=Services,<Distinguished name of the configuration naming context>.
- Retrieve the property gatewayProxy. This is a multi-valued property that contains the default addresses of the organization.
- Check each of the entries in that list. If it starts with SMTP:, you have found it.
- Get the property proxyAddresses from the directory object of the user. This is a multi-valued properties that contains all addresses that are assigned to the user. Each entry has the following consists of a protocol identifier and an address entry. For example:
If the protocol identifier is all uppercase, it is the default entry for that protocol.
- Iterate through the list of assigned addresses and search for the SMTP address which domain part matches with the default SMTP retrieved above.
- Select the correct HTTP Server:
- If you found an email address corresponding to the default SMTP policy, extract the alias from the found address. For the above example the alias would be john.doe. The correct HTTP server to use is the default HTTP Server
- If there is no smtp address matching the default policy, iterate through the list of proxy addresses again, and do the following, if it is an SMTP address:
Get the property msExchServerBindings from the selected HTTP server node. This entry has the following structure:
Extract the alias and the domain from the address.
Iterate through the list of virtual HTTP server and check the property msExchDefaultDomain of each entry. If it matches the domain of the users address domain, you have found the correct http server.
The first and the last part of the string can be empty, like in this example:
:80:Depending on the content of this field, you can now build the desired url:
- If the servername of the msExchServerBindings property is not empty, build the OWA url like this:
- If the servername is empty, but an ipaddress is present, build the OWA url like this:
- If both, the servername and the ipaddress fields are empty, get the ipaddress for the exchange server:
- From the node of the exchange server, retrieve the property networkaddress.
- Iterate through this property and retrieve the value which starts with ncacn_ip_tcp:.
- Now, build the url with the above scheme, using the just found ip address.