Understanding Sharing Invitation Requests – EWS Managed API 1.2 : Part 1

Before I go into details on exactly how to accomplish programmatically creating a sharing invitation in C# for Exchange 2010 like it’s no big deal, I want to put a disclaimer down right up front:

Please consider the following while reading:

1) I in no way endorse or support any of the code posted here to be used in a production environment. I’m the last guy who needs to be blogging about optimized/secure coding practices and methodologies.
2) I am not responsible if you use this code and subsequently melt down your datacenter…
3) This is a proof of concept approach, so there are probably MANY areas that could be optimized to be less expensive when calling EWS. Again, I just wanted to see if it was possible…and I don’t code for a living…so don’t laugh at my code :P
4) Finally, the following information on what I did will assume that you have an account setup for impersonation and have the security rights to perform all the relevant operations against EWS in order for this process to work.

Tools and Environment:

Environment Used:

Exchange Version: Exchange 2010 SP1 RU5
EWS Managed API Version: 1.2
My Operating System: Windows 7 x64
Application .NET version: Version 4.0 (NOT the Client Profile)

Development Tools Utilized:
– C# 2010 Express
– OutlookSpy
– EWSEditor
– MFCMAPI
– Outlook 2010

Other Items of note:

Using this method has only been tested for creating sharing folder invitations within the same Exchange Organization. Federated Sharing is a whole other topic that needs to be understood before trying to use this method to send sharing invites to external recipients…

Now that that’s out of the way, down to business!! So I had been playing with the Exchange Web Services Managed API 1.2 and recently built a relatively simple but effective utility that mimics Outlook’s Permissions GUI, except I added the ability to let myself impersonate anyone and used a ListTree of the users’ folders to allow me to manage the permissions on them. Yes, we do a lot of coddling to our users, but also realize we just got done with a huge GroupWise 7.0 -> Exchange 2010 SP1 RU5 migration and so I wanted an easy and effective way of handling the flood of requests coming in without having to perform too many steps to switch between users. The other reason for this app was that there were MANY outdated/irrelevant permissions that came across during the migrations, and some users had upwards of 100 folders and subfolders with tons of outdated permissions on them. Using EWS I was able to recursively loop through all the folders, leaving only the default permissions left for the user to set up once again how they wished. It’s pretty slick!!

In any case, that only whetted my appetite to see what else that the EWS Managed API could do. I decided as a final piece to this little utility, I wanted to be able to call a function that would create a sharing invite on behalf of the impersonated user and send it to the person I had just given reviewer rights on the default calendar folder for. Sounds simple enough…right???

WRONG!!!!! I tell you what, I was not prepared for this adventure….

First things first: Conversion from/to Binary/Hex & other *cough* fun stuff..

Before we even get into the structuring of the objects, let’s run through a couple functions I will be referencing in other code throughout this series.

In order to get the data formatted properly for some of the properties we will be setting later on the message and attachment objects, we need to be able to do two separate types of conversions: 1) Take a string and convert it to its hexadecimal equivalent and 2) Take that hex string and convert it to binary.

This function will take a string (i.e. “Mary a little lamb”) and return the Hexadecimal string value (“4D6172792061206C6974746C65206C616D62”):




        public string ConvertStringToHex(string input)
        {
            // Take our input and break it into an array
            char[] arrInput = input.ToCharArray();
            String result = String.Empty;

            // For each set of characters
            foreach (char element in arrInput)
            {
                // My lazy way of dealing with whether or not anything had been added yet
                if (String.IsNullOrEmpty(result))
                {
                   result = String.Format("{0:X2}", Convert.ToUInt16(element)).ToString();
                }
                else {
                    result += String.Format("{0:X2}", Convert.ToUInt16(element)).ToString();
                }

            }
            return result.ToString();
        }

The following function will take the Hex value of your data (“4D6172792061206C6974746C65206C616D62”) and convert it to binary (Type byte[]):

Reference: Glen Scales on MSDN TechNet Forums
(His blog is here – If you have ANY curiosity about in-depth EWS development – his blog is the de-facto standard)


private static byte[] HexStringToByteArray(string input) {

           byte[] Bytes;

           int ByteLength;

           string HexValue = "\x0\x1\x2\x3\x4\x5\x6\x7\x8\x9|||||||\xA\xB\xC\xD\xE\xF";

           ByteLength = input.Length / 2;

           Bytes = new byte[ByteLength];

           for (int x = 0, i = 0; i < input.Length; i += 2, x += 1)

           {
                      Bytes[x] = (byte)(HexValue[Char.ToUpper(input[i + 0]) - '0'] << 4);
                      Bytes[x] |= (byte)(HexValue[Char.ToUpper(input[i + 1]) - '0']);
           }

           return Bytes;
}

The Mystery that is the Sharing Invitation

My initial research into the subject led me to this post on StackOverflow, which, while not entirely filling in the whole picture, started me on the right track…

Ok, according to the post the “hard part” was building the invitation itself? Well, after a bit of fussing around with code I randomly found I decided to bite the bullet and spend the next three days reading the documentation for the Exchange Messaging Protocol Specifications.

BTW, We are going to be focusing on the default Calendar folder for the remainder of this series. Making this functionality work more dynamically is certainly possible, but way too involved to be worth the effort for me.

Turns out that there are two major components to the Sharing Invitation for a user’s folder.


Figure 1: Sharing Message Object (High Level) – For details, read [MS-OXSHARE] Specification on MSDN

Figure 1: Sharing Message Object (High Level)

The first component, illustrated in Figure 1, is a normal email message that has a bunch of information (in the form of Extended Properties) that tells Exchange that this is a sharing invitation and who it’s for along with a couple other pieces of information. Some of the properties are constants, or rather constant depending on the situation, but we’ll get to that later…


Figure 2: Sharing Message Attachment (High Level)

Figure 2: Sharing Message Attachment (High Level)

As you can see from Figure 2, the second component is a magic “sharing_metadata.xml” file that is attached to the message before sending. It contains information relevant to the sharing of the folder as well. There are only a few XML elements to talk about regarding the sharing_metadata.xml file, so I will conclude this post with an overview of the file, how to obtain the information necessary for the document, and how to properly encode the data so you do not get a corrupted invitation message.

 

XML Attachment: Contents of the file

Here’s the information inside a sharing_metadata.xml document (the Id’s have been changed to protect the innocent):


<?xml version="1.0"?>
<SharingMessage xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/sharing/2008">
  <DataType>calendar</DataType>
  <Initiator>
    <Name>SharedByUserDisplayName</Name>
    <SmtpAddress>SharedByUser@contoso.com</SmtpAddress>
<EntryId>00000000DCA740C8C042101AB4B908002B2FE18201000656D2D4B65697A657220507
5626C6963205363686F6F6C732F6F753D6D696E6973747261746976652047726F757020284654
9424F48463233535092F636E3D52656369704757436F6E6E00</EntryId>
  </Initiator>
  <Invitation>
    <Providers>
      <Provider Type="ms-exchange-internal" TargetRecipients="sharedToUser@contoso.com">
        <FolderId xmlns="http://schemas.microsoft.com/exchange/sharing/2008">
00000000EA272C6387A99442B4B371A18905E4DB01007F8000</FolderId>
        <MailboxId xmlns="http://schemas.microsoft.com/exchange/sharing/2008">
00000000EA272C6387A993A547A9A1B0F249B6491B00000000000038A1BB1005E5101AA1BB08002B
2A56C20000454D534D44422E444C4C00000000000000001B55FA20AA6611CD9BC800AA002FC45A0C
000000445443454D585075626C6963205363686F6F6C732F6F753D4578636867652041646D6520
</MailboxId>
      </Provider>
    </Providers>
  </Invitation>
</SharingMessage>


 

XML Attachment: Elements and Values

That’s it huh? No big deal right? Well, unless you’re a Cylon you’re probably looking at the values in that XML document going WTF is that??? Don’t feel bad…I did too…until I found some interesting documentation in Microsoft Exchange Protocol Specification Library

For starters, it looks like we need 6 values (all strings) to load into the xml at the appropriate elements. Let’s take a look at them: sharing_metadata.xml Elements and relevant information about them

Figure 3

Great, so we have three HEX values we need to figure out, and we want to constrain to the standards as strictly as possible here, because even though I just wanna do it to do it, I also want to see if I can apply this to any user. That’s when I found the MS docs that specify how each of those HEX entries are determined:

XML Attachment: Getting the Initiator EntryId Value

Initiator > EntryId
Also known as: PidTagEntryId of the AddressBook object of the user sharing the folder)

The EntryId is broken down into the following pieces (as documented on MSDN):

Canonical name: PidLidSharingInitiatorEntryId
Description: Contains the value of the PidTagEntryId property (section 2.761) for the Address Book object of the currently logged-on user.
Property set: PSETID_Sharing {00062040-0000-0000-C000-000000000046}
Property long ID (LID): 0x00008A09
Data type:PtypBinary, 0x0102
Area: Sharing
Defining reference: [MS-OXSHARE] section 2.2.2.7
Alternate names: dispidSharingInitiatorEid
Flags (4 bytes): This value MUST be set to 0x00000000. Bits in this field indicate under what circumstances a short-term EntryID is valid. However, in any EntryID stored in a property value, these 4 bytes MUST be zero, indicating a long-term EntryID.
ProviderUID (16 bytes): The identifier for the provider that created the EntryID. This value is used to route EntryIDs to the correct provider and MUST be set to %xDC.A7.40.C8.C0.42.10.1A.B4.B9.08.00.2B.2F.E1.82.
Version (4 bytes): This value MUST be set to %x01.00.00.00.
Type (4 bytes): An integer representing the type of the object. It MUST be one of the values from the following table.

Value (hex bytes) Address book EntryID type
0x00000000%x00.00.00.00 Local mail user (Note there are other values for external recipients, etc but that’s outside the scope of this article)

X500DN (variable): The X500 DN of the Address Book object. The X500DN field is a null-terminated string of 8-bit characters.

So what this told me ultimately was there is a binary property called “PidLidSharingInitiatorEntryId” value I will be needing to assign, and I can use the same value for the EntryId on the sharing_metadata.xml file, but I will need to use the hex representation of that property instead of it’s binary native datatype. (I will need the binary value as well later, but that’s for next post)…

Luckily we can “construct” this value using the following code snippets:


        public String GetMailboxDN()
        {
            string result = "error";
            AutodiscoverService autodiscoverService = new AutodiscoverService();

            // The following RedirectionUrlValidationCallback required for httpsredirection.
            autodiscoverService.RedirectionUrlValidationCallback = RedirectionUrlValidationCallback;
            autodiscoverService.Credentials = new WebCredentials(webCredentialObject);

            // Get the user settings.
            // Submit a request and get the settings. The response contains only the
            // settings that are requested, if they exist.
            GetUserSettingsResponse userresponse = autodiscoverService.GetUserSettings(
                txtImpersonatedUser.Text.ToString(),
                UserSettingName.UserDisplayName,
                UserSettingName.UserDN,
                );

                foreach (KeyValuePair usersetting in userresponse.Settings)
                {

                    if (usersetting.Key.ToString() == "UserDN")
                    {
                        result = usersetting.Value.ToString();
                    }
                }

                return result;

        }

        public String GetIntiatorEntryID()
        {
            String result = String.Empty;

            // Bind to EWS
            ExchangeService service = GetExchangeService();
service.ImpersonatedUserId =
new ImpersonatedUserId(ConnectingIdType.SmtpAddress, txtImpersonatedUser.Text.ToString());

              // Get LegacyDN Using the function above this one 
              string sharedByLegacyDN = GetMailboxDN(); 
              // A conversion function from earlier
              string legacyDNinHex = ConvertStringToHex(sharedByLegacyDN); 

            StringBuilder addBookEntryId= new StringBuilder();

	     // Note while I was debugging I logged this to a text file as well
            using (StreamWriter w = new StreamWriter("BuildAddressBookEntryIdResult.txt"))
            {

                addBookEntryId.Append("00000000"); /* Flags */
                addBookEntryId.Append("DCA740C8C042101AB4B908002B2FE182"); /* ProviderUID */
                addBookEntryId.Append("01000000"); /* Version */
                addBookEntryId.Append("00000000"); /* Type - 00 00 00 00  = Local Mail User */
                addBookEntryId.Append(legacyDNinHex); /* Returns the userDN of the impersonated user */
                addBookEntryId.Append("00"); /* terminator bit */

                //Log(addBookEntryId.ToString(), w, "GetAddBookEntryId");
                w.Close();

            }

            result = addBookEntryId.ToString();
            service.ImpersonatedUserId = null;
            return result;
        }

So if you do this you should get a string returned that looks something like the following (one long string):

00000000DCA740C8C042101AB4B908002B2FE18201000000000000002F6F
3D53SDSDSDSDFGA6572205075626C6963205SDDFSD63686F6F63D4578636
8616E67652041646D696E6973747261746976652047726F7570202846594
FDFDFE4FDFFF435350444C54292F636E3D52

Congratulations! One value down…two more to go!!!

XML Attachment: Getting the FolderId Value

Invitation > FolderId
Also known as: EWS FolderId (converted to Hex of course) of the folder being shared
The FolderId that we want to use for the sharing_metadata.xml file can be obtained with the following code:


        public String GetConvertedEWSIDinHex(ExchangeService esb, String sID, String strSMTPAdd)
        {
            // Create a request to convert identifiers.
            AlternateId objAltID = new AlternateId();
            objAltID.Format = IdFormat.EwsId;
            objAltID.Mailbox = strSMTPAdd;
            objAltID.UniqueId = sID;

            //Convert  PR_ENTRYID identifier format to an EWS identifier.
            AlternateIdBase objAltIDBase = esb.ConvertId(objAltID, IdFormat.HexEntryId);
            AlternateId objAltIDResp = (AlternateId)objAltIDBase;
            return objAltIDResp.UniqueId.ToString();
        }

// Bind to the folder
Folder folderStoreInfo;
folderStoreInfo = Folder.Bind(service, WellKnownFolderName.Calendar);
string EwsID = folderStoreInfo.Id.UniqueId;

// The value of folderidHex will be what we need to use for the FolderId in the xml file
string folderidHex = GetConvertedEWSIDinHex(service,folderid,txtImpersonatedUser.Text);

Now on to the last mystery value:

XML Attachment: Getting the MailboxId Value

Invitation > MailboxId
Also known as: Structured Value of Sharing Folder’s parent Mailbox

Flags (4 bytes): This value MUST be set to 0x00000000. Bits in this field indicate under what circumstances a short-term EntryID is valid. However, in any EntryID stored in a property value, these 4 bytes MUST be zero, indicating a long-term EntryID.
ProviderUID (16 bytes): The identifier for the provider that created the EntryID. This value is used to route EntryIDs to the correct provider and MUST be set to %x38.A1.BB.10.05.E5.10.1A.A1.BB.08.00.2B.2A.56.C2.
Version (1 byte): This value MUST be set to zero.
Flag (1 byte): This value MUST be set to zero.
DLLFileName (14 bytes): This field MUST be set to the following value, which represents “emsmdb.dll”: %x45.4D.53.4D.44.42.2E.44.4C.4C.00.00.00.00.
WrappedFlags (4 bytes): This value MUST be set to 0x00000000.
WrappedProvider UID (16 bytes): This field MUST be set to one of the values in the following table.

Store object type ProviderUID value
Mailbox Store object %x1B.55.FA.20.AA.66.11.CD.9B.C8.00.AA.00.2F.C4.5A
Public folder Store object %x1C.83.02.10.AA.66.11.CD.9B.C8.00.AA.00.2F.C4.5A

WrappedType (4 bytes): The value of this field is determined by where the folder is located. For a mailbox store this value MUST be set to %x0C.00.00.00. For a public store, this value MUST be set to %x06.00.00.00.
ServerShortname (variable): A string of single-byte characters terminated by a single zero byte, indicating the short name or NetBIOS name of the server.
MailboxDN (variable): A string of single-byte characters terminated by a single zero byte and representing the X500 DN of the mailbox, as specified in [MS-OXOAB]. This field is present only for mailbox database

Using this information we can construct the proper value by using the following code:


        public String GetInvitationMailboxId()
        {
            ExchangeService service = GetExchangeService();
            service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress, txtImpersonatedUser.Text);
            // Generate The Store Entry Id for the impersonated user
            StringBuilder MailboxIDPointer= new StringBuilder();

            // Notice again the logging for debugging the results
            using (StreamWriter w = new StreamWriter("BuildInvitationMailboxIdResult.txt"))
            {

                MailboxIDPointer.Append("00000000"); /* Flags */
                MailboxIDPointer.Append("38A1BB1005E5101AA1BB08002B2A56C2"); /* ProviderUID */
                MailboxIDPointer.Append("00"); /* Version */
                MailboxIDPointer.Append("00"); /* Flag */
                MailboxIDPointer.Append("454D534D44422E444C4C00000000"); /* DLLFileName */
                MailboxIDPointer.Append("00000000"); /* Wrapped Flags */
                MailboxIDPointer.Append("1B55FA20AA6611CD9BC800AA002FC45A"); /* WrappedProvider UID (Mailbox Store Object) */
                MailboxIDPointer.Append("0C000000"); /* Wrapped Type (Mailbox Store) */
                MailboxIDPointer.Append(ConvertStringToHex(GetMailboxServer()).ToString()); /* ServerShortname (FQDN) */
                MailboxIDPointer.Append("00"); /* termination bit */
                MailboxIDPointer.Append(ConvertStringToHex(GetMailboxDN()).ToString()); /* Returns the userDN of the impersonated user */
                MailboxIDPointer.Append("00"); /* terminator bit */

                Log(MailboxIDPointer.ToString(), w, "GetInvitiationEntryID");
                w.Close(); 

            }
            service.ImpersonatedUserId = null;
            return MailboxIDPointer.ToString();

        }

Conclusion

So THAT’s all there is to it….well…to the very first step that is…which was getting the values necessary to build our own
sharing_metadata.xml file.

Next post I will explain how I constructed the attachment, and start explaining how Extended Properties fits in to the puzzle.

Till next time…

About these ads

6 comments on “Understanding Sharing Invitation Requests – EWS Managed API 1.2 : Part 1

  1. SteveR says:

    I’ve been figuring out how to send a sharing invitation using EWS and have independently come to a somewhat similar solution. I saw your question on Stack Overflow but only thought to look to see if any answers had been posted the day after I worked out how to do it myself. :)

    A few observations:

    1) I construct the various Ids (PidLidSharingInitiatorEntryId etc) as byte arrays and convert them to hex strings later as needed using BitConverter.ToString(id).Replace(“-“, “”)

    private byte[] CombineHeaderAndData(byte[] header, params string[] data)
    {
    var enc = new ASCIIEncoding();
    byte[] ret;

    using (var memStream = new MemoryStream())
    {
    memStream.Write(header, 0, header.Length);

    foreach (string item in data)
    {
    byte[] dnBytes = enc.GetBytes(item);
    memStream.Write(dnBytes, 0, dnBytes.Length);
    memStream.WriteByte(new byte());
    }

    ret = memStream.ToArray();
    }

    return ret;
    }

    private byte[] UserEntryIdFromX500DN(string x500dn)
    {
    var header = new byte[] { 0x00, 0x00, 0x00, 0x00, 0xDC, 0xA7, 0x40, 0xC8, 0xC0, 0x42, 0x10, 0x1A, 0xB4, 0xB9, 0x08, 0x00, 0x2B, 0x2F, 0xE1, 0x82, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

    return CombineHeaderAndData(header, x500dn);
    }

    This seems simpler and perhaps more explicit.The spec says that the various pieces of data such as MailboxDN are “strings of single-byte characters” which seems to imply ASCII.

    2) The GetUserSettingsResponse.Settings is an IDictionary so you can just grab the value(s) you need straight out of it rather than loop through them looking for the one you want i.e. userresponse.Settings[UserSettingName.UserDN] as string

    3) You don’t show getting the ServerShortname needed for the value of PidLidSharingRemoteStoreUid but I’ve found that this can be derived from the UserSettingName.InternalMailboxServer value retrieved in the same way as the other settings. Since the entire process needs to retrieve several of these settings it’s more efficient to retrieve them all together rather than seperately since this results in multiple requests to the server.

    So far I haven’t had to construct and attach the sharing_metadata.xml manually for the sharing invitation to work correctly, although I’ve only tried it in OWA. It was my assumption that the message gets properly constructed on the server using the extended properties that are set on the message, and that includes the necessary metadata being created and attached. I’m not 100% certain about it but it’s working so far.

    Anyway, your S.O. question helped us on the way to figuring this all out, so thanks for that!

    • SteveR,

      Thanks for the feedback! Love it that this helped out!

      … I construct the various Ids (PidLidSharingInitiatorEntryId etc) as byte arrays…

      Makes sense!! As I had mentioned this was drafted in total proof of concept mode so I was able to go back later and cut down my EWS expense majorly, but I never looked at streamlining the data conversion that way! Thanks for the tip I’m going to see how I can plug that into my build!!

      “You don’t show getting the ServerShortname needed for the value of PidLidSharingRemoteStoreUid but I’ve found that this can be derived from the UserSettingName.InternalMailboxServer value retrieved in the same way as the other settings. Since the entire process needs to retrieve several of these settings it’s more efficient to retrieve them all together rather than seperately since this results in multiple requests to the server.”

      You’re right about the ServerShortname piece being missing I noticed that yesterday as well! I need to edit the post I will do that tonight hopefully to show that. But it’s exactly how you said and that’s how I got the value as well!

      “The GetUserSettingsResponse.Settings is an IDictionary so you can just grab the value(s) you need straight out of it rather than loop through them looking for the one you want i.e. userresponse.Settings[UserSettingName.UserDN] as string”

      Great point about that…I have combined the GetUserDN and InternalMailboxServer into one request, but I was still looping. Your point makes total sense and I am going to test that as well and see if I can modify the post after I do it to reflect that and credit you on that!

      As for the sharing_metadatafile…were you saying that you had actually gotten the attachment generated by just populating the message object with the extended properties??? I had always had this floating in the back of my head, but was too scared to try it (I’m being VERY cautious not to hose the system as I test this stuff, and didn’t know if the message with no attachment would choke a service or something)…so if you have gotten it to work (even through OWA) that’s aweseome and I’m making note. Would be interesting to see if it worked on the client as well…if that attachment is autogenerated then its a good chunk of code that doesn’t need to be written…all the better!

      Thanks again for the feedback!

      Mike

  2. SteveR says:

    Hi Mike,

    I’ve looked into the sharing_metadata.xml attachment a little more and it doesn’t seem that it’s getting auto-magically created by merely setting the extended properties on the message. However, the sharing invitation does seem to work perfectly well without it in both OWA and Outlook 2010.

    This leaves me wondering what the purpose of the attachment is, since clearly the information in the extended properties alone is sufficient (in our test environment, at least) for the recipient to use the sharing invite to add the shared calendar. One possibility is that either the attachment or the extended property values are used for reasons of backwards-compatibility: it could be that the sharing invite without the attachment wouldn’t work in an older client such as Outlook 2003/2007 (or vice versa, the invite might not work in future versions of Outlook without the attachment if the extended properties are obsoleted). However, this is pure speculation on my part and I really don’t understand the “belt-and-braces” approach they’ve taken by providing the same information as both extended properties and in the XML file. I suppose one thing to try would be to create the invite without the extended properties containing the information duplicated in the XML file and see if that still works.

    So far, we’ve only tried our sharing invitation between accounts in the same Exchange mail domain, so I guess there’s all sorts of potential scenarios where maybe it wouldn’t work without the XML attachment (federated domains etc.) but I honestly don’t know enough about them to say, although I’d be interested in knowing the reason for the attachment…

    Cheers,

    Steve

    • Interesting…I think you may be right about the federated sharing thing….may also be for clients that don’t read the properties on the message object correctly and the like as well…the legacy thing makes sense too.

      My scenario is also a single domain and organization, so I’m sure the semantics of our disoveries would need to be modified in certain scenarios (I know of one at least – I tried originally using the GetSharedMetadata SOAP Call against EWS to retrieve some properties but got denied saying that Federated Sharing was not configured or something of the sort, so thats when I dove back into trying to “parse” out the info from other sources…)

      Anyway thanks again for the comments! Glad you got stuff working!

  3. outdoor tv says:

    Hi there, simply became aware of your blog via Google, and located that it’s really informative. I’m gonna watch out for brussels. I will be grateful if you happen to continue this in future. Numerous other folks might be benefited out of your writing. Cheers!

  4. Does your website have a contact page? I’m having problems locating
    it but, I’d like to send you an e-mail. I’ve got
    some suggestions for your blog you might be interested in hearing.

    Either way, great website and I look forward to seeing it grow over time.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s