To learn more about this book, visit Microsoft Learning at <URL looks like: http://www.microsoft.com/MSPress/books/#####.aspx> A.11 Extended Property Helpers Two helper files were created to simplify dealing with the extended property proxy classes. The first of these deals with the PathToExtendedFieldType proxy class and removes much of the tedium when dealing with these types. Much of the class houses code for converting from the property tag values to MapiPropertyTypeType values. Also included are several factory methods for creating the various and sundry types of extended field uris. // PathToExtendedFieldType.cs // // Copyright© 2006 David Sterling // using System; using System.Collections.Generic; using System.Text; namespace ProxyHelpers.EWS { /// <summary> /// Extension of PathToExtendedFieldType to add some helpful overloads /// and methods </summary> public partial class PathToExtendedFieldType { private static Dictionary<int, SingleAndArrayPair> mapping = new Dictionary<int, SingleAndArrayPair>(); DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 2 A. 11 Extended Property Helpers /// <summary> /// Static constructor. Used to fill up our dictionary /// </summary> static PathToExtendedFieldType() { mapping.Add(2, new SingleAndArrayPair( MapiPropertyTypeType.Short, MapiPropertyTypeType.ShortArray)); mapping.Add(3, new SingleAndArrayPair( MapiPropertyTypeType.Integer, MapiPropertyTypeType.IntegerArray)); mapping.Add(4, new SingleAndArrayPair( MapiPropertyTypeType.Float, MapiPropertyTypeType.FloatArray)); mapping.Add(5, new SingleAndArrayPair( MapiPropertyTypeType.Double, MapiPropertyTypeType.DoubleArray)); mapping.Add(6, new SingleAndArrayPair( MapiPropertyTypeType.Currency, MapiPropertyTypeType.CurrencyArray)); mapping.Add(7, new SingleAndArrayPair( MapiPropertyTypeType.ApplicationTime, DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. A.11 Extended Property Helpers MapiPropertyTypeType.ApplicationTimeArray)); mapping.Add(0xB, new SingleAndArrayPair(MapiPropertyTypeType.Boolean)); mapping.Add(0x14, new SingleAndArrayPair( MapiPropertyTypeType.Long, MapiPropertyTypeType.LongArray)); mapping.Add(0x1E, new SingleAndArrayPair( MapiPropertyTypeType.String, MapiPropertyTypeType.StringArray)); mapping.Add(0x1F, new SingleAndArrayPair( MapiPropertyTypeType.String, MapiPropertyTypeType.StringArray)); mapping.Add(0x40, new SingleAndArrayPair( MapiPropertyTypeType.SystemTime, MapiPropertyTypeType.SystemTimeArray)); mapping.Add(0x48, new SingleAndArrayPair( MapiPropertyTypeType.CLSID, MapiPropertyTypeType.CLSIDArray)); mapping.Add(0x102, new SingleAndArrayPair( MapiPropertyTypeType.Binary, MapiPropertyTypeType.BinaryArray)); } /// <summary> DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 3 4 A. 11 Extended Property Helpers /// Returns true if the prop tag type passed in represents an array /// type</summary> /// <param name="propTagType">Property tag type</param> /// <returns>True if array type</returns> /// private static bool IsArrayType(ushort propTagType) { return (propTagType & 0xF000) !=0; } /// <summary> /// Extracts the raw type from the prop tag. Will be the same for /// single and multivalued types </summary> /// <param name="propTagType">Type to examine</param> /// <returns>Raw type</returns> /// private static ushort ExtractTypeFromArrayType(ushort propTagType) { return (ushort)(propTagType & 0x0FFF); } /// <summary> /// Converts from a full property tag to the corresponding /// MapiPropertyTypeType schema value. /// </summary> /// <param name="fullPropertyTag">Full proptag including type /// part</param> /// <returns>MapiPropertyTypeType</returns> DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. A.11 Extended Property Helpers /// public static MapiPropertyTypeType GetMapiPropertyType( int fullPropertyTag) { // The type is in the low word. Mask it off // ushort type = (ushort)(fullPropertyTag & 0xFFFF); ushort rawType = ExtractTypeFromArrayType(type); SingleAndArrayPair pair; if (!mapping.TryGetValue(rawType, out pair)) { throw new ArgumentException( "Unsupported property type: " + type); } if (IsArrayType(type)) { if (pair.ArrayValueType.HasValue) { return pair.ArrayValueType.Value; } else { throw new ArgumentException( "No array type provided for type: " + type); } } else { return pair.SingleValueType; DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 5 6 A. 11 Extended Property Helpers } } /// <summary> /// Creates a prop tag extended field uri (proxy) /// </summary> /// <param name="propId">16-bit Id of property tag</param> /// <param name="propType">property type</param> /// <returns>PathToExtendedFieldType proxy object</returns> /// public static PathToExtendedFieldType BuildPropertyTag( ushort propId, MapiPropertyTypeType propType) { PathToExtendedFieldType result = new PathToExtendedFieldType(); result.PropertyTag = string.Format("0x{0:x}", propId); result.PropertyType = propType; return result; } /// <summary> /// Creates a GuidId extended field uri (proxy) /// </summary> /// <param name="guid">Guid representing the property set</param> /// <param name="propId">Property id of the named property</param> /// <param name="propType">Property type</param> DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. A.11 Extended Property Helpers /// <returns>PathToExtendedFieldType proxy object</returns> /// public static PathToExtendedFieldType BuildGuidId( Guid guid, int propId, MapiPropertyTypeType propType) { PathToExtendedFieldType result = new PathToExtendedFieldType(); result.PropertyId = propId; // Don’t forget to set the specified property to true for // optional value types!! // result.PropertyIdSpecified = true; result.PropertySetId = guid.ToString("D"); result.PropertyType = propType; return result; } /// <summary> /// Builds a guid/name extended property /// </summary> /// <param name="guid">Property set guid</param> /// <param name="propertyName">Property name</param> /// <param name="propType">Property type</param> /// <returns>Guid/Name extended property</returns> /// public static PathToExtendedFieldType BuildGuidName( Guid guid, string propertyName, DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 7 8 A. 11 Extended Property Helpers MapiPropertyTypeType propType) { PathToExtendedFieldType result = new PathToExtendedFieldType(); result.PropertySetId = guid.ToString("D"); result.PropertyName = propertyName; result.PropertyType = propType; return result; } /// <summary> /// Nested class for holding MapiPropertyTypeType values that are /// related</summary> private class SingleAndArrayPair { private MapiPropertyTypeType singleValue; private MapiPropertyTypeType? arrayValue; /// /// /// /// <summary> Constructor </summary> <param name="singleValue">Type for single valued /// items</param> /// <param name="arrayValue">OPTIONAL type for multi-valued /// items. There is no bool[] for instance</param> /// public SingleAndArrayPair( MapiPropertyTypeType singleValue, DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. A.11 Extended Property Helpers MapiPropertyTypeType arrayValue) { this.singleValue = singleValue; this.arrayValue = arrayValue; } /// <summary> /// Constructor to use for single valued items only /// </summary> /// <param name="singleValue">Type for single valued /// items</param> /// public SingleAndArrayPair(MapiPropertyTypeType singleValue) { this.singleValue = singleValue; this.arrayValue = null; } /// <summary> /// Accessor for the single value type /// </summary> public MapiPropertyTypeType SingleValueType { get { return this.singleValue; } } /// <summary> /// Accessor for the array value type /// </summary> public MapiPropertyTypeType? ArrayValueType DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 9 10 A. 11 Extended Property Helpers { get { return this.arrayValue; } } } } } The second of these files simplifies the use of the ExtendedPropertyType proxy class. It adds constructors for single and multi-valued properties. using System; using System.Collections.Generic; using System.Text; namespace ProxyHelpers.EWS { /// <summary> /// Extension of the ExtendedPropertyType /// </summary> public partial class ExtendedPropertyType { /// <summary> /// Constructor needed for serialization since we are providing /// overloaded constructors /// </summary> public ExtendedPropertyType(){} /// <summary> /// Constructor /// </summary> /// <param name="fieldURI">FieldURI representing metadata about the DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. A.11 Extended Property Helpers /// property</param> /// <param name="value">Value for the property</param> /// public ExtendedPropertyType( PathToExtendedFieldType fieldURI, string value) { this.ExtendedFieldURI = fieldURI; this.Item = value; } /// <summary> /// Constructor /// </summary> /// <param name="fieldURI">FieldURI representing metadata about the /// property</param> /// <param name="values">PARAMS array of values for multivalued /// property</param> /// public ExtendedPropertyType( PathToExtendedFieldType fieldURI, params string[] values) { this.ExtendedFieldURI = fieldURI; NonEmptyArrayOfPropertyValuesType array = new NonEmptyArrayOfPropertyValuesType(); array.Items = new string[values.Length]; int index = 0; foreach (string value in values) { DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 11 12 A. 11 Extended Property Helpers array.Items[index++] = value; } this.Item = array; } } } With these two partial classes, dealing with extended properties is much easier. For instance, creating a multi-valued extended property is reduced to two statements: PathToExtendedFieldType metadata = PathToExtendedFieldType.BuildGuidName( Guid.NewGuid(), "Foo Property", MapiPropertyTypeType.IntegerArray); ExtendedPropertyType prop = new ExtendedPropertyType( metadata, "1", "2", "3", "4", "5"); DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 11 Extended Properties With all these wonderful predefined properties available, we will never have occasion to access anything else, right? Okay, I concede, that is unlikely. And that is precisely why extended properties were added. So, just what is an extended property? Considering the name, it would be reasonable to conclude that such properties are in addition to some set of “standard” accessible properties. That is partially correct. Or it may be reasonable to deduce that we are taking existing properties and “extending” them in some way. Maybe. When we first starting writing specification documents for extended properties, we called the feature “Extended MAPI Properties”. Ah, now we are getting somewhere. As the feature progressed through docs and developement, we dropped the “MAPI” name, partially because we didn’t want to tie the schema directly to MAPI1, and partially because MAPI sounds too much like “happy”, and no one has ever heard of a happy property2. Regardless of the absence of MAPI from the feature title, extended properties focus completely on accessing native MAPI properties, and nothing more. So we can consider the feature name to mean a way to access MAPI properties without using the properties that are explicitly defined in the xml schema. Let’s look at the system that sits beneath EWS so that we can appreciate the need for such devices. Note that it is not necessary for you to understand the following background section. 1 We lost that battle too. We have a MapiPropertyTypeType enumeration in the schema. 2 To be honest, MAPI’s similarity to “happy” didn’t come up during any of the design reviews. The MAPI title just seemed to fall out of the design. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 2 Chapter 11 Extended Properties However, doing so will give you a better grasp of why extended properties work the way that they do. A little background Messages, items, folders and the like are stored within an Exchange mailbox. A mailbox is contained within a mailbox database. Now, if you have had any experience with databases, you know that a table within a database has an associated schema that describes each of the data columns and their types. Interestingly enough, whereas most relationship databases have relatively fixed schemas, the schema within Exchange can be extended. Each item or folder property within the database is assigned an identifier called a property tag (proptag for short) which uniquely identifies that column within that specific database. Think of this as the property’s name within the mailbox database. In a relational database, you ask for properties by column name such as CUSTOMER_ID, whereas properties within the mailbox database are accessed by proptag. A proptag is represented as an unsigned 32 bit integer. The lower word contains the type for the property tag whereas the high word represents the actual identifier for the property. Figure 11-1 Property Tag Division Property Type As shown in Figure 11-1, the lower 16 bits of a proptag (section to the right) give us the data type for that property. A given property can have one and only one type. EWS supported types are listed in Table 11-1. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties Table 11-1 Supported MAPI Types MAPI Property Type Value Extended Property Type .NET Type Comments Unspecified 0 Unsupported Unsupported Null 1 Null Unsupported Short 2 Short Short Int 3 Integer Int32 Float 4 Float float Double 5 Double double Currency 6 Currency Int64 Represents the number of cents. AppTime 7 ApplicationTime double Integer part represents the date, fractional part the time. Per MSDN, “This property type is the same as the OLE type VT_DATE and is compatible with the Visual Basic time representation.” Error 0xA Error Unsupported Exists for reporting purposes only. Boolean 0xB Boolean bool Object 0xD Object Unsupported Exists for reporting purposes only. Exists for reporting purposes only. Points to an object that implements DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 3 4 Chapter 11 Extended Properties IUnknown. Long 0x14 Long Int64 AnsiString 0x1E String string Value is an ANSI string. Both Unicode and ANSI strings map to WS String type. String 0x1F String string Value is a Unicode string SysTime 0x40 SystemTime DateTime Value is an NT FILETIME. Same as the VT_FILETIME OLE type. Guid 0x48 CLSID Guid Binary 0x102 Binary Byte[] Short Array 0x1002 ShortArray Short[] Int Array 0x1003 IntegerArray Int[] Float Array 0x1004 FloatArray Float[] Double Array 0x1005 DoubleArray Double[] Currency Array 0x1006 CurrencyArray Int64[] AppTime Array 0x1007 ApplicationTimeArray Double[] Object Array 0x100D ObjectArray Unsupported Long Array 0x1014 LongArray Int64[] AnsiString Array 0x101E StringArray String[] Exists for reporting purposes only. Array of ANSI strings. Both Unicode and ANSI DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 5 strings map to the WS String type. String Array 0x101F StringArray String[] SysTime Array 0x1040 SystemTimeArray DateTime[] Guid Array 0x1048 CLSIDArray Guid[] Binary Array 0x1102 BinaryArray Byte[][] MSDN has some good information on these types. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/mapi/html/bc51730098db-4d3e-8303-557e18b5e71f.asp So, given a proptag such as 0x0E03001E, we can break that into an identifier of 0x0E03 and a property type of 0x001E (ANSI String). For the inquisitive, that just happens to be the text that appears on the CC line of a message. The 16 bit identifier range is broken into a standard range (0x0000 to 0x7FFF) and a custom range (0x8000 – 0xFFFE). This is where things get a little bit interesting. The definition of property tags within the standard range will be the same across mailbox databases. These have predefined meanings and are publicly documented (well, some of them at least). Using our example from above, 0x0E03 will refer to “Display CC” on any mailbox database that you encounter, even across Exchange installations. Now the custom range is a different story. As was mentioned earlier, the “schema” for a mailbox database can be extended to include custom properties. When the store encounters a custom property that it hasn’t seen before within a given mailbox database, it will assign that property an unused property tag from the custom range. As a result, a given custom property will likely be assigned different proptags across different mailbox database instances. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 6 Chapter 11 Extended Properties To illustrate this, let’s say that I create a message and give it a custom property called “foo”. When I save/send the message, the mailbox database determines that it has never seen “foo” before and extends the schema to include “foo”. During that process, it gives “foo” a unique proptag in the custom prop range. Now, the email is shipped along the wire until it ultimately arrives at its destination on another Exchange server. The receiving Exchange server saves the message in the database containing the destination mailbox, and in the process of doing so, notices that it has never seen “foo” either. It also extends its schema to include “foo” and assigns the property an unused value in the custom prop range. What are the chances that the two assigned proptag tags are the same? I wouldn’t count on it. With this in mind, we can hopefully see that making assumptions about the proptag value of a custom property is not necessarily a good thing. Identifying extended properties “But how do you specify a custom prop if the prop tag isn’t assigned until after the store sees it?” I’m glad you asked. Custom properties have a secondary way to identify them. Actually, to be more accurate, the prop tag should be considered the second way to identify the custom property – at least when you consider the timeline of events. A custom property, or “named” property as it is sometimes called, is uniquely identified with either a guid + name or guid + id pair. In the case of the guid + name combination, the name is a string and must be unique within the namespace defined by the guid. In the case of the guid + id combination, the id is a 32-bit integer and must be unique within the namespace defined by the guid. A given custom property can be identified by either a guid + name or guid + id pair. It cannot, however, be identified by both. When you create the custom property, you must choose how it should be represented. Going back to our example, when we create the “foo” property, what we are really doing is creating a custom property called “foo” within some namespace. For our purposes, I am willing to part with a precious guid or two to assist in this chapter. MySpecialGuid = 24040483-cda4-4521-bb5f-a83fac4d19a4 YourSpecialGuid = 3cd40456-6991-4ebb-a01a-d4bc711b301f DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 7 MySpecialGuid now defines my namespace. Within this scope, I create a property with the name of “foo”. Note that it is not sufficient to just say that I am dealing with property “foo”, as there could also be a property “foo” within YourSpecialGuid. In fact, MySpecialGuid/foo is completely different from YourSpecialGuid/foo. So in creating our message from above, I actually set my custom property on the message using MySpecialGuid/foo. As suggested above, the store looks in its internal mapping tables to see if a proptag has been assigned to this guid + name pair. If not, it assigns one and puts the guid + name pair into its internal mapping tables. Very good. But how does this help us? Well, you see, although the prop tags may be different from one mailbox database to the next, the guid + name pair will always be the same. So rather than requesting the custom property by proptag, we request it by guid + name pair. The store still thinks in terms of proptags, so what it does it look at its internal mapping tables and convert the guid + name pair into its corresponding proptag. It then uses the proptag to manipulate the property within the store. Extended Properties in EWS So how does this all tie into Exchange web services? Let us being by looking at how the three types of extended properties are represented. EWS surfaces extended properties through the PathToExtendedFieldType schema type (in types.xsd). Listing 11-1 PathToExtendedFieldType schema type <xs:complexType name="PathToExtendedFieldType"> <xs:complexContent> <xs:extension base="t:BasePathToElementType"> <xs:attribute name="DistinguishedPropertySetId" type="t:DistinguishedPropertySetType" use="optional" /> <xs:attribute name="PropertySetId" type="t:GuidType" DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 8 Chapter 11 Extended Properties use="optional" /> <xs:attribute name="PropertyTag" type="t:PropertyTagType" use="optional" /> <xs:attribute name="PropertyName" type="xs:string" use="optional" /> <xs:attribute name="PropertyId" type="xs:int" use="optional" /> <xs:attribute name="PropertyType" type="t:MapiPropertyTypeType" use="required" /> </xs:extension> </xs:complexContent> </xs:complexType> <xs:element name="ExtendedFieldURI" type="t:PathToExtendedFieldType" substitutionGroup="t:Path" /> This single type can represent three different flavors of extended properties. We battled over whether to break this into three different extended field uri types or whether we should keep this as a single type. For better or worse, we kept the single type. The attribute combination determines the flavor of extended property. Notice in Listing 11-1, the only required attribute is the property type. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 9 Property Type Regardless of flavor, all PathToExtendedFieldType instances must have the PropertyType attribute set. The property type attribute dictates the data type that values of this custom property have. As noted in the schema, this is a “MapiPropertyTypeType” and can contain any of the supported values shown in Table 11-1. Rendered in xml, this would look as follows: <t:ExtendedFieldURI PropertyType="Integer" …more…/> If you are using the proxy classes, you would do the following: PathToExtendedFieldType result = new PathToExtendedFieldType(); // set the property type // myCustomProp.PropertyType = MapiPropertyTypeType.Integer; Notice that the PropertyType property does not have a PropertyTypeSpecified associated property. Can you guess why? Because the schema requires that property to be there (use=”required”), and therefore, we do not need an extra flag to indicate its existence. Property Tags Although we have mapped many of the standard properties in our schema, there are still a number of standard properties that can only be accessed by property tag. You express a property tag extended property by a combination of the PropertyTag attribute and the required PropertyType attribute. No other attributes are permitted. <t:ExtendedFieldURI PropertyTag="0x1234" PropertyType="String"/> One thing that should be noticeably different between the MAPI and EWS representation or property tags is that in EWS, the identifier and type are broken out into separate values. There are two main reasons for this. First, it is much easier to see what type a given property is referring to when dealing with enumeration values rather than hex identifiers. Second, by DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 10 Chapter 11 Extended Properties restricting the property type to an enumeration and that is schema validated, we remove the possibility of encountering a garbage type within a PathToExtendedFieldType instance. What this does mean, however, is that if you have a MAPI prop tag in hand, you will need to determine which property type the least significant word is referring to. To assist in this, we can use a simple routine 3such as the one below for converting between MAPI proptag values and EWS property types. Listing 11-2 Converting a property tag into a MapiPropertyTypeType value /// <summary> /// Converts from a full property tag to the corresponding /// MapiPropertyTypeType schema value.</summary> /// <param name="fullPropertyTag">Full proptag including type part</param> /// <returns>MapiPropertyTypeType</returns> /// public static MapiPropertyTypeType GetMapiPropertyType(int fullPropertyTag) { // The type is in the low word. Mask it off // short type = (short)(fullPropertyTag & 0xFFFF); switch (type) { case 2: return MapiPropertyTypeType.Short; case 3: return MapiPropertyTypeType.Integer; 3 Appendix A-11 provides an extension to the PathToUnindexedFieldType class that performs this work and more. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 11 case 4: return MapiPropertyTypeType.Float; case 0x001E: return MapiPropertyTypeType.String; // The rest are left as an exercise for the reader :) // default: throw new ArgumentException("Unsupported property type: " + type); } } Earlier in the chapter we discussed how proptag values for custom properties are not guaranteed to be the same across mailbox databases. As a result of this, EWS does not support referencing custom properties (0x8000 – 0xFFFF) by their proptag value. You must reference such properties by their guid/name or guid/id pair. Of course, all reserved properties (<0x8000) can be accessed via proptag. Custom properties by name Named custom properties deal with several different attributes in PathToExtendedFieldType in addition to the PropertyType attribute. The first of these defines the namespace or scope for your property name. Even though we are talking about the custom range here, there are some “standard” namespaces that are exposed through the DistinguishedPropertySetType enumeration. Internally, these map to their respective guid namespace values. They were provided simply to reduce eye strain – I have never really enjoyed staring at guids, although I am quite thankful for them. To use these well-known namespaces, you must set the DistinguishedPropertySetId attribute. <t:ExtendedFieldURI DistinguishedPropertySetId="PublicStrings" ..more../> And in proxy code… PathToExtendedFieldType fieldUri = new PathToExtendedFieldType(); DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 12 Chapter 11 Extended Properties fieldUri.DistinguishedPropertySetId = DistinguishedPropertySetType.PublicStrings; // Don't forget to set the specified property for optional value type // properties // fieldUri.DistinguishedPropertySetIdSpecified = true; For all other namespaces, or for those who really like guids, the PropertySetId attribute is available. This attribute expects a guid without the enclosing braces. Capitalization of the guid does not matter. Here we will use the guid format of the PublicStrings4 well known namespace. <t:ExtendedFieldURI PropertySetId="00020329-0000-0000C000-000000000046" … more… /> For the proxy class, the PropertySetId is a string rather than a guid. Since strings are reference types, there is no need for the *Specified property, even though the attribute is optional in the schema. Reference types use null to indicate that no value has been set. PathToExtendedFieldType fieldUri = new PathToExtendedFieldType(); fieldUri.PropertySetId = "00020329-0000-0000-C000-000000000046"; Of course it wouldn’t be too useful if we didn’t have a way to dictate which property we were referring to within our namespace. That is the job of the aptly named PropertyName attribute. This attribute takes a case sensitive string. PathToExtendedFieldType fieldUri = new PathToExtendedFieldType(); fieldUri.PropertySetId = "00020329-0000-0000-C000-000000000046"; fieldUri.PropertyName = "Foo"; fieldUri.PropertyType = MapiPropertyTypeType.Integer; 4 Both extended field uris will therefore point to the same property. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 13 Custom properties by Id Just as with guid/name custom properties, we use the property set (or distinguished property set) to identify our scope. The difference is that instead of specifying the PropertyName attribute, we use the PropertyId attribute. The PropertyId attribute is an integer. <t:ExtendedFieldURI PropertySetId=”00020329-0000-0000-C000-000000000046” PropertyId=”2” PropertyType=”CLSIDArray”/> Since PropertyId is an optional value type, the proxy surfaces the PropertyIdSpecified boolean property that must be set to true if you are going to supply a value for the property id. PathToExtendedFieldType fieldUri = new PathToExtendedFieldType(); fieldUri.PropertySetId = "00020329-0000-0000-C000-000000000046"; fieldUri.PropertyId = 2; fieldUri.PropertyIdSpecified = true; fieldUri.PropertyType = MapiPropertyTypeType.Integer; When working with the proxy classes, I found it extremely helpful to add a couple of constructors or factory methods to this class that would set the various properties correctly. I have added these in Listing 11-3. Listing 11-3 Factory methods for PathToExtendedFieldType /// <summary> /// Creates a prop tag extended field uri (proxy) /// </summary> /// <param name="propId">16-bit Id of property tag</param> /// <param name="propType">property type</param> /// <returns>PathToExtendedFieldType proxy object</returns> /// DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 14 Chapter 11 Extended Properties public static PathToExtendedFieldType BuildPropertyTag( ushort propId, MapiPropertyTypeType propType) { PathToExtendedFieldType result = new PathToExtendedFieldType(); result.PropertyTag = string.Format("0x{0:x}", propId); result.PropertyType = propType; return result; } /// <summary> /// Creates a GuidId extended field uri (proxy) /// </summary> /// <param name="guid">Guid representing the property set</param> /// <param name="propId">Property id of the named property</param> /// <param name="propType">Property type</param> /// <returns>PathToExtendedFieldType proxy object</returns> /// public static PathToExtendedFieldType BuildGuidId( Guid guid, int propId, MapiPropertyTypeType propType) { PathToExtendedFieldType result = new PathToExtendedFieldType(); DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 15 result.PropertyId = propId; // Don’t forget to set the specified property to true for optional value // types!! // result.PropertyIdSpecified = true; result.PropertySetId = guid.ToString("D"); result.PropertyType = propType; return result; } Metadata and data While schema-defined properties will always have their data represented in first class form, extended properties have no such first-class representation. For instance, the schema-defined unindexed field type “item:Subject” will always expose its data as a child of an Item element (or derivative). <!--Subject is declared in schema as a child of Item --> <t:Item> <t:Subject>Foo</t:Subject> </t:Item> You will certainly see the metadata representation of such unindexed properties <t:FieldURI FieldURI=”item:Subject”/>, but never directly in connection with the data values. In contrast, when we come to the actual data values for an extended property, we will encounter both the metadata and the data instance representations. Here we must represent both who (metadata) we are talking about and what (data) its value is. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 16 Chapter 11 Extended Properties Figure 11-2 Extended Property Structure It follows that the property type indicated within the metadata must agree with the type of the data. So the following would result in an error: <!-- This won’t work since the types don’t match --> <t:ExtendedProperty> <t:ExtendedFieldURI PropertyTag=”0x1234” PropertyType=”Integer”/> <t:Value>This is a string</t:Value> </t:ExtendedProperty> That brings up an interesting question. Since XML is all string based, how do you encode the various types within the value element? DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 17 Table 11-2 String representation of property types MapiPropertyTypeType Comments Example ApplicationTime Write out as a double 1234.12 Binary Base-64 encoded string. Use System.Convert.ToBase64String() BAUGBw== Boolean For False use false or 0 true For True use true or 1 CLSID A guid string without the enclosing brackets. If using a Guid type, use myGuid.ToString(“D”) 24040483-cda44521-bb5fa83fac4d19a4 Currency 1234 Double 1234.45 Float 1234.45 Integer 1234 Long 1234 Short 1234 SystemTime String Internally, we represent this as a DateTime and parse using DateTime.TryParse. We assume UTC if no time zone is specified. 5/6/2005 8:30am “foo” Listing 11-4 shows an example of using the proxy classes to create an extended property with a SystemTime type. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 18 Chapter 11 Extended Properties Listing 11-4 // Set up the metadata (which property we are referring to) // PathToExtendedFieldType metadata = new PathToExtendedFieldType(); metadata.PropertyTag = “0x1234”; metadata.PropertyType = MapiPropertyTypeType.SystemTime; // Now create the container and set the value // ExtendedPropertyType extendedProperty = new ExtendedPropertyType() extendedProperty.ExtendedFieldURI = metadata; // For single valued extended properties, Item must be a string // extendedProperty.Item = DateTime.Now.ToString(); Multi-valued properties Extended properties can also support multi-valued collections. This is done by modifying the data section of an extended property slightly as shown in Listing 11-5. Listing 11-5 Multi-valued extended property <t:ExtendedProperty> <t:ExtendedFieldURI PropertyTag=”0x1235” PropertyType=”IntegerArray”/> <t:Values> <t:Value>1</t:Value> <t:Value>2</t:Value> <t:Value>3</t:Value> </t:Values> </t:ExtendedProperty> DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 19 Now, let’s do the same thing with the proxy classes. // Set up the metadata (which property we are referring to) PathToExtendedFieldType metadata = new PathToExtendedFieldType(); metadata.PropertyTag = “0x1235”; metadata.PropertyType = MapiPropertyTypeType.IntegerArray; // Now create the container and set the value // ExtendedPropertyType extendedProperty = new ExtendedPropertyType() extendedProperty.ExtendedFieldURI = extendedFieldUri; // For multi-valued properties, Item is a NonEmptyArrayOfPropertyValuesType // NonEmptyArrayOfPropertyValuesType arrayValues = new NonEmptyArrayOfPropertyValuesType(); arrayValues.Items = new string[3]; for (int index = 1; index <= 3; index++) { arrayValues.Items[index-1] = index } extendedProperty.Item = arrayValues; Using Extended Properties Overriding extended property types Let’s say we already have extended property X in the mailbox database as an integer. What happens if you create a new item and set property X as a string? Good question. If you guessed that it averaged the two types and turned the result into a Guid, you are incorrect. If, however, you guessed that it overwrote the first value, you would be correct. If you then try DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 20 Chapter 11 Extended Properties to go back and retrieve the property using the original type, it will no longer be there. The new version (with the new type) overwrote the old version. This redefinition is specific to the item in question. So let’s say that as in the above example, we have extended property X as an integer and we set that on message A and on message B. If we then go and update message B so that property X is now a string, that does not affect property X on message A. Property X will remain an integer on message A. Property X on message A and the updated property X on message B are now two different properties and have two different property tags. Specifying extended properties in shapes Let’s assume that we have some arbitrary object within the store and we want to retrieve it along with several of its extended properties. We need to indicate our need for these extended properties using the AdditionalProperties element for the item or folder shape in question. First, let’s get something in the mailbox that has extended properties using our friend CreateItem from chapter 4. Since we are setting the values for these properties, we need to use the ExtendedProperty container element containing both the metadata (the property in question) and the data that we wish to set. Let’s set two different properties just for fun. Listing 11-6 Creating an item with two extended properties <CreateItem xmlns=".../messages" xmlns:t=".../types" MessageDisposition="SaveOnly"> <Items> <t:Message> <t:Subject>Test27</t:Subject> <t:ExtendedProperty> <t:ExtendedFieldURI PropertyTag="0x1234" PropertyType="StringArray"/> <t:Values> DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 21 <t:Value>Fee</t:Value> <t:Value>Fi</t:Value> </t:Values> </t:ExtendedProperty> <t:ExtendedProperty> <t:ExtendedFieldURI DistinguishedPropertySetId="PublicStrings" PropertyName="ShoeSize" PropertyType="Float"/> <t:Value>12</t:Value> </t:ExtendedProperty> </t:Message> </Items> </CreateItem> Notice in Listing 11-6 we were able to specify a plurality of ExtendedProperty elements for our message. After successful creation, we should be able to retrieve the item with a GetItem call. GetItem will return the properties that are applicable to the message we just created, but that certainly won’t include our custom properties. So how do we indicate our desire to retrieve those properties? We need to explicitly ask for them within the AdditionalProperties child element of the item response shape as shown in Listing 11-7. Listing 11-7 Retrieving a message with extended properties <GetItem xmlns=".../messages" xmlns:t=".../types"> <ItemShape> <t:BaseShape>IdOnly</t:BaseShape> <t:AdditionalProperties> <t:ExtendedFieldURI PropertyTag="0x1234" PropertyType="StringArray"/> <t:ExtendedFieldURI DistinguishedPropertySetId="PublicStrings" DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 22 Chapter 11 Extended Properties PropertyName="ShoeSize" PropertyType="Float"/> </t:AdditionalProperties> </ItemShape> <ItemIds> <t:ItemId Id="AAAtAEFkbWluaXN…=" ChangeKey="CQAAs8A6QI2x…" /> </ItemIds> </GetItem> In the AdditionalProperties element of our GetItem call, we only use the metadata format of the properties that we wish to request. That should make sense as we are requesting the data and therefore would have no values to put there. And the response shows us that things went as planned…. <GetItemResponse xmlns:m=".../messages" xmlns:t=".../types" xmlns=".../messages"> <m:ResponseMessages> <m:GetItemResponseMessage ResponseClass="Success"> <m:ResponseCode>NoError</m:ResponseCode> <m:Items> <t:Message> <t:ItemId Id="…" ChangeKey="…"/> <t:ExtendedProperty> <t:ExtendedFieldURI PropertyTag="0x1234" PropertyType="StringArray"/> <t:Values> <t:Value>Fee</t:Value> <t:Value>Fi</t:Value> </t:Values> </t:ExtendedProperty> DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 23 <t:ExtendedProperty> <t:ExtendedFieldURI DistinguishedPropertySetId="PublicStrings" PropertyName="ShoeSize" PropertyType="Float"/> <t:Value>12</t:Value> </t:ExtendedProperty> </t:Message> </m:Items> </m:GetItemResponseMessage> </m:ResponseMessages> </GetItemResponse> To specify extended properties within the response shape of a proxy request, use the ItemResponseShapeType proxy class as shown in Listing 11-8. Listing 11-8 Requesting extended properties using the proxy // Create our response shape and set the base shape type // ItemResponseShapeType responseShape = new ItemResponseShapeType(); responseShape.BaseShape = DefaultShapeNamesType.IdOnly; // Create our additional property array // responseShape.AdditionalProperties = new BasePathToElementType[2]; // Build our two extended field uris. // PathToExtendedFieldType extendedProp1 = new PathToExtendedFieldType(); DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 24 Chapter 11 Extended Properties extendedProp1.PropertyTag = "0x1234"; extendedProp1.PropertyType = MapiPropertyTypeType.StringArray; PathToExtendedFieldType extendedProp2 = new PathToExtendedFieldType(); extendedProp2.DistinguishedPropertySetId = DistinguishedPropertySetType.PublicStrings; extendedProp2.DistinguishedPropertySetIdSpecified = true; extendedProp2.PropertyName = "ShoeSize"; extendedProp2.PropertyType = MapiPropertyTypeType.Float; // Set the additional properties on the response shape // responseShape.AdditionalProperties[0] = extendedProp1; responseShape.AdditionalProperties[1] = extendedProp2; Updating extended properties Updating extended properties on an item follows the same pattern as updating any other property. Simply specify the property that you wish to change using the metadata format and then embed the extended property (both metadata and data) as a child element of the item in the update call. This is shown in Listing 11-9. Listing 11-9 Updating an item with extended properties <UpdateItem MessageDisposition="SaveOnly" ConflictResolution="AutoResolve" xmlns=".../messages" xmlns:t=".../types"> <ItemChanges> <t:ItemChange> <t:ItemId Id="AAAtAEFkbWluaXN…" ChangeKey="CQAAABYAA…"/> DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 25 <t:Updates> <t:SetItemField> <t:ExtendedFieldURI PropertyTag="0x1234" PropertyType="StringArray"/> <t:Message> <t:ExtendedProperty> <t:ExtendedFieldURI PropertyTag="0x1234" PropertyType="StringArray"/> <t:Values> <t:Value>Foe</t:Value> <t:Value>Fum</t:Value> </t:Values> </t:ExtendedProperty> </t:Message> </t:SetItemField> </t:Updates> </t:ItemChange> </ItemChanges> </UpdateItem> NOTE: Extended properties are not supported for append operations. As such trying to use an AppendToItemField element within a UpdateItem call on an extended property will result in an error. To “mimic” appends, first fetch the data, append the new data and then call UpdateItem with a SetItemField action. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 26 Chapter 11 Extended Properties Odds and Ends Overriding EWS business logic The EWS business logic layer tries to make sense of MAPI by restricting which properties you can set on a given item type. For instance, most people don’t need to get/set a start date on a message. That is typically used only by calendar items and tasks. What if you do really need to set the start date on a message? You can look up the MAPI information for the property in question and access it using the extended property syntax. There are other object-level validation steps that are performed before save time that you can’t get around. For instance, a calendar item must have both a start date and an end date, and the start date must be less than or equal to the end date. It will do you no good to try to set the start date to be greater than the end date via extended properties. For the most part, however, the field is quite open. I would recommend, however, that you have a valid business reason for trying to get around the EWS business logic that was put in place. MAPI versus Calculated properties A large portion of the properties defined within the EWS schema are backed by MAPI properties. There are some properties, however, that are calculated and therefore have no direct underlying MAPI property. In some cases, these calculations are quite simple, but others can be quite complex. What does this mean to you? Well, given that you can override the EWS business logic for MAPI properties, there will come a time when you want to find the MAPI property identifiers for one of these calculated properties. I speak with a clear conscience – they don’t exist. These properties are noted in the appendix. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 27 Make my life easier To make our lives a little easier, we can extend the partial proxy classes to move much of the boilerplate code into the proxy classes themselves. Refer to appendix A.11 for more details. There are a bunch of MAPI based applications out there that will potentially be migrated to EWS. So, what if you have a MAPI PR_ENTRYID and want to feed this to Exchange Web Services? I could imagine that this would occur in several cases. - You may have a local store of items that you would like to access, but all you have is the lowly PR_ENTRYID since the local store was created using a legacy application. - You may be upgrading a legacy app piece by piece and have some legacy components that hand your code PR_ENTRYIDs and you need to send these off to your shiny new web service code. - You are irritated that nothing in the web services schema is prefixed with PR_ and are intent on bucking the system. The good news is that this can be done. The bad news is that the performance is quite lower than accessing the item using the web services ItemId. With that in mind, let’s look at how we would do this. Note that the following uses concepts discussed in chapter 12. Grabbing an EntryID using Outlook Spy Now in the scenarios above, you will already have the PR_ENTRYID value. But for this example, I do not, so let’s grab one from Outlook Spy and play with it. I would highly recommend that you purchase a copy of Outlook Spy. It is a great tool for digging around in your mailbox and understanding how things work. Outlook Spy works as an add-in for Microsoft Outlook. You can find Outlook Spy on the web at http://www.dimastr.com/outspy. Using Outlook Spy, we can determine the MAPI property information for PR_ENTRYID. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 28 Chapter 11 Extended Properties Figure 11-3 PR_ENTRYID in Outlook Spy Using the binary HexView editor in Outlook Spy (the little folder icon by the value text box), I can save the binary data for the entry id out to a file (File |Save). Figure 11-4 Outlook Spy HexView viewing the PR_ENTRYID Now that we have an entry id to play with, let’s continue. Here is the general idea behind what we are going to do. First, we can use extended properties to reference the PR_ENTRYID extended MAPI property. Outlook Spy shows us that the property tag for PR_ENTRYID is 0x0FFF0102and its type is a byte[] (PT_BINARY), so all we are concerned with is the most significant word, which is the left four hex digits right after 0x (0FFF). We can use this in our ExtendedFieldURI to reference the property. With this information, we can then perform a shallow traversal item query to look for the item that has the entry id in question. We perform this search using the FindItem web method. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 29 All binary properties must be base-64 encoded when dealing with Exchange Web Services. The System.Convert class surfaces the ToBase64String and FromBase64String methods which will serve us quite nicely. So let’s take the binary entry Id that we saved out to a file, read it in and convert it to a base64 string. using (FileStream fs = File.OpenRead(@"c:\MyEntryId.dat")) { BinaryReader reader = new BinaryReader(fs); byte[] bytes = reader.ReadBytes((int)fs.Length); string base64 = System.Convert.ToBase64String(bytes); } When I run the above code, I get the following base64 encoded entry id: AAAAAIUnJ7skJWxMk4SkP5mmyOgHAIeKIfEv1k9KqJx6faPnw54AAACiQdwAANw+LZ+kl0 NBpOrzVAeB39sAO7niICgAAA== Now we have our EntryId and can continue where we left off. Taking the base64 representation our our EntryID, we can build a restriction (query) as shown in Listing 11-10. Listing 11-10 Finding an item by PR_ENTRYID <FindItem xmlns=".../messages" xmlns:t=".../types" Traversal="Shallow"> <ItemShape> <t:BaseShape>Default</t:BaseShape> </ItemShape> <Restriction> <t:IsEqualTo> <t:ExtendedFieldURI PropertyTag="0x0FFF" PropertyType="Binary"/> <t:FieldURIOrConstant> DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. 30 Chapter 11 Extended Properties <t:Constant Value="AAAAAIUnJ7skJWxMk4SkP5mmyOgHAIeKIfEv1k9KqJx6faPnw54 AAACiQdwAANw+LZ+kl0NBpOrzVAeB39sAO7niICgAAA=="/> </t:FieldURIOrConstant> </t:IsEqualTo> </Restriction> <ParentFolderIds> <t:DistinguishedFolderId Id="inbox"/> </ParentFolderIds> </FindItem> The query in Listing 11-10 says, “Give me all the items (<FindItem>) that are direct children of the inbox (<ParentFolderIds>) that have this extended property set to this binary value (<Restriction>)” Does it work? Let’s see… <FindItemResponse xmlns:m=".../messages" xmlns:t=".../types" xmlns=".../messages"> <m:ResponseMessages> <m:FindItemResponseMessage ResponseClass="Success"> <m:ResponseCode>NoError</m:ResponseCode> <m:RootFolder TotalItemsInView="1" IncludesLastItemInRange="true"> <t:Items> <t:Message> <t:ItemId Id="AAAeAGRdnX..." ChangeKey="CQAAABYAA..."/> <t:Subject>See if you can find me!</t:Subject> <t:Sensitivity>Normal</t:Sensitivity> <t:Size>2598</t:Size> <t:DateTimeSent>2006-0921T17:13:19Z</t:DateTimeSent> <t:DateTimeCreated>2006-0921T17:13:35Z</t:DateTimeCreated> DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document. Chapter 11 Extended Properties 31 <t:HasAttachments>false</t:HasAttachments> <t:From> <t:Mailbox> <t:Name>David Sterling</t:Name> </t:Mailbox> </t:From> <t:IsRead>false</t:IsRead> </t:Message> </t:Items> </m:RootFolder> </m:FindItemResponseMessage> </m:ResponseMessages> </FindItemResponse> Now, we can extract the ItemId from this response and we have our ItemId identifier. The above example has a large performance impact on the server, but it does get you there. Summary Ah, yes, extended properties. We now know what extended properties are, their various flavors, and how to identify them in both xml and in proxy code. We covered the difference between standard props and named props and discussed why you cannot identify custom properties by property tag. The difference between the metadata and data representations of extended properties was discussed. Extended properties are a valuable tool in any EWS developer’s toolbox, and a good understanding of them will provide you with hours of fun. DRAFT CONTENT: This content is excerpted from an upcoming title from Microsoft Learning. This content is not complete and is subject to change prior to the release of the final, fully edited, document.
© Copyright 2024