Set the timezone used for the events in Outlook and Microsoft Graph REST APIs

When you’ve been working for a while like me, you know that when a developer has to work with dates, he faces problems with timezones.

Outlook REST API allows you to get the events for a user by calling the following URL :

GET https://outlook.office365.com/api/v2.0/Me/Events

In the version 2.0 of the API and as you can see in the JSON response below for an event (the response shown here was truncated for brevity), the Start and End items contain two distinct properties for the date and timezone.

By default, all dates and times returned by the server are based on the UTC timezone.

{
  Attendees = (),
  Body = { ... },
  BodyPreview = "...",
  Categories = (),
  ChangeKey = "...",
  CreatedDateTime = "2015-12-02T16:21:57.91403Z";
  End = {
    DateTime = "2012-02-09T19:00:00.0000000",
    TimeZone = "UTC"
  },
  HasAttachments = false,
  Id = "...",
  Importance = "Normal",
  IsAllDay = false,
  IsCancelled = false,
  IsOrganizer = true,
  IsReminderOn = false,
  LastModifiedDateTime = "2015-12-02T16:21:58.7734204Z";
  Location = { ... },
  Organizer = { ... },
  OriginalEndTimeZone = "Pacific Standard Time",
  OriginalStartTimeZone = "Pacific Standard Time",
  Recurrence = { ... },
  ReminderMinutesBeforeStart = 0,
  ResponseRequested = true,
  ResponseStatus = { ... },
  Sensitivity = "Normal",
  SeriesMasterId = <null>,
  ShowAs = "Tentative",
  Start = {
    DateTime = "2012-02-09T18:00:00.0000000",
    TimeZone = "UTC"
  },
  Subject = "...",
  Type = "SeriesMaster"
  WebLink = "...",
  iCalUId = "..."
}

It’s now easier to know what timezone is used for the dates and times. But that’s not all.

If you want the server automatically convert the dates and times of the events into a different timezone, you are able to do so and it’s very easy.

You just have to specify a HTTP header like in the example below to change the timezone. The server will then use this new parameter to return the dates and times in the desired timezone.

Prefer: outlook.timezone="Central Standard Time"

Specifying this HTTP header and calling the previously mentioned URL, you will retrieve a new JSON response like this one :

{
  Attendees = (),
  Body = { ... },
  BodyPreview = "...",
  Categories = (),
  ChangeKey = "...",
  CreatedDateTime = "2015-12-02T16:21:57.91403Z";
  End = {
    DateTime = "2012-02-09T13:00:00.0000000",
    TimeZone = "Central Standard Time"
  },
  HasAttachments = false,
  Id = "...",
  Importance = "Normal",
  IsAllDay = false,
  IsCancelled = false,
  IsOrganizer = true,
  IsReminderOn = false,
  LastModifiedDateTime = "2015-12-02T16:21:58.7734204Z";
  Location = { ... },
  Organizer = { ... },
  OriginalEndTimeZone = "Pacific Standard Time",
  OriginalStartTimeZone = "Pacific Standard Time",
  Recurrence = { ... },
  ReminderMinutesBeforeStart = 0,
  ResponseRequested = true,
  ResponseStatus = { ... },
  Sensitivity = "Normal",
  SeriesMasterId = <null>,
  ShowAs = "Tentative",
  Start = {
    DateTime = "2012-02-09T12:00:00.0000000",
    TimeZone = "Central Standard Time"
  },
  Subject = "...",
  Type = "SeriesMaster"
  WebLink = "...",
  iCalUId = "..."
}

If you want more information about the names of supported timezones, you can find a list on the official documentation on MSDN.

It’s also good to know that even if it’s not mentioned (for now) on the official documentation, this HTTP header also works with Microsoft Graph.

Programmez #191 : Developing C# applications on OSX with Xamarin.Mac

You probably heard about Xamarin for mobile cross-platform development, but do you know that Xamarin also offers a solution to build OSX applications ?

Discover an overview of Xamarin.Mac in my article in the latest issue of Programmez, a magazine available in French-speaking countries (France, Belgium, Switzerland, Luxemburg, Canada…).

Programmez 191

Be careful to case sensitivity in Microsoft Graph REST API

Last week during Connect(); // 2015, Microsoft announced the general availability (GA) of Microsoft Graph (formerly known as Unified API).

In association with the announcement, a brand new website with a full documentation that explains what’s Microsoft Graph and how to use the REST API was launched here : http://graph.microsoft.io/docs

Since few days, I’m working with Microsoft Graph and I found few issues. Today, I want to talk about one of them which is common with a lot of APIs but frustrating when you encounter it.

During my investigations, I was trying to send a mail and by refering the documentation, it’s noted that you just have to send a JSON message like this one…

["message": {
  body = {
    content = "This message was sent with Microsoft Graph",
    contentType = "text"
  },
  importance = "high",
  subject = "Microsoft Graph REST API",
  toRecipients = ({
    emailAddress = {
      address = "john@doe.com",
      name = "John Doe"
    }
  })
},
"saveToSentItems": false]

… to the following URL.

https://graph.microsoft.com/v1.0/me/microsoft.graph.sendMail

Very easy in theory but when I execute the request with these values, I receive the following error message.

One or more parameters of the operation 'SendMail' are missing from the request payload. The missing parameters are: Message.

To understand what’s the problem, I looked at the metadata generated for the REST API (based on OData 4.0) by accessing the following URL.

https://graph.microsoft.com/v1.0/$metadata

In the metadata returned by the server, you can see this.

<Action Name="sendMail" IsBound="true" EntitySetPath="bindingParameter">
<Parameter Name="bindingParameter" Type="microsoft.graph.user" />
<Parameter Name="Message" Type="microsoft.graph.message" Nullable="false" />
<Parameter Name="SaveToSentItems" Type="Edm.Boolean" />
</Action>

As you can see, the parameters used by this method are named “Message” and “SaveToSendItems” (in PascalCase) and not “message” and “saveToSendItems” (in lowerCamelCase) as written in the documentation.

When you pay attention to metadata generated by OData, you will see that some methods, especially those existing in other parts of Office 365 REST APIs (e.g. sendMail is used in Outlook REST API), still use PascalCase for their parameter names.

So if you follow the documentation to implement Microsoft Graph but you receive an error message indicating that a parameter is missing, take a look at metadata generated by OData to validate that the method is not expected to use PascalCase instead of lowerCamelCase for its parameters.

An SSL error has occurred when trying to use the Outlook REST API on iOS 9 or OSX 10.11 (El Capitan)

In a previous article, we talked about an issue when you try to stream an Office 365 Videos from Azure Media Services in your application running on iOS 9.

It seems that Microsoft is actually doing some modifications on the infrastructure of the Office 365 platform, especially around the SSL certificates, because today I’m facing the same error when using the Outlook REST API.

I’m trying to call the following URL which should return the last 10 messages stored in the inbox  of the authenticated user :

https://outlook.office365.com/api/v2.0/Me/MailFolders('Inbox')/Messages

But I get the following error message, which indicates there’s an issue with SSL certificates :

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)

Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made."
UserInfo={NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, NSUnderlyingError=0x7d96cd90 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, _kCFNetworkCFStreamSSLErrorOriginalValue=-9802, _kCFStreamErrorCodeKey=-9802, _kCFStreamErrorDomainKey=3, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x7afb9d50>, kCFStreamPropertySSLPeerCertificates=<CFArray 0x7ae63490 [0xa97098]>{type = immutable, count = 2, values = (
 0 : <cert(0x7afb8570) s: outlook.com i: Microsoft IT SSL SHA1>
 1 : <cert(0x7afb96d0) s: Microsoft IT SSL SHA1 i: Baltimore CyberTrust Root>
)}}}, _kCFStreamErrorCodeKey=-9802, NSErrorFailingURLStringKey=https://outlook.office365.com/api/v2.0/Me/MailFolders('Inbox')/Messages, NSErrorPeerCertificateChainKey=<CFArray 0x7ae63490 [0xa97098]>{type = immutable, count = 2, values = (
 0 : <cert(0x7afb8570) s: outlook.com i: Microsoft IT SSL SHA1>
 1 : <cert(0x7afb96d0) s: Microsoft IT SSL SHA1 i: Baltimore CyberTrust Root>
)}, NSErrorClientCertificateStateKey=0, NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x7afb9d50>, NSErrorFailingURLKey=https://outlook.office365.com/api/v2.0/Me/MailFolders('Inbox')/Messages}

The cause of this error is still the same as explained in our previous article : App Transport Security (ATS).

To solve the issue, you need to add some lines in your Info.plist to configure ATS to ignore the SSL error.

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSExceptionDomains<key>
  <dict>
    <key>outlook.office365.com</key>
    <dict>
      <key>NSExceptionRequiresForwardSecrecy</key>
      <false/>
    </dict>
  </dict>
</dict>

You can also edit this file using the editor in Xcode rather than manually edit the XML file :

Office365 Outlook REST API - App Transport Security

Searching documents across multiple Office 365 groups with REST API

If you work on Office 365, you probably know what are groups, one of the latest features launched by Microsoft on its collaborative platform.

For those who don’t know what’s it, it’s a new way and mostly a simplest way, to create sites that allow users to collaborate with a minimal set of collaborative features such as storing documents, creating conversations, taking notes in a notebook and sharing events on a calendar.

Below you can see an example of a group, especially the document storage.

Office365 - Groups

For now, if you want to find a document in a given group or a set of documents across all groups, there’s no possibility to search what you’re looking for.

Indeed, when you search for documents using the search area available on the top-left (cf. screenshot), you will be redirected to the global search center of your company available in Office 365.

Fortunately, we can use the search engine with REST API to implement that missing feature.

As a reminder (we described it many times in articles on this blog), to submit a search query in Office 365 using REST API, you can send a POST request to the following URL with a well-formatted HTTP body.

https://tenant.sharepoint.com/_api/Search/PostQuery

When you want to retrieve only documents stored in SharePoint libraries, you can filter items based on the ContentClass property which exists for each items (pages, documents, tasks, events…) that were created in your sites.

In the same way, if you want to search only in a particular kind of site (e.g. Office 365 groups in our case), you can filter results based on the SiteTemplate property.

So what looks like the request to send to the REST API ?

{
  request = {
    Querytext = "ContentClass:STS_ListItem_DocumentLibrary AND SiteTemplate:GROUP",
    RowLimit = 100,
    SelectProperties = (
            Title,
            DefaultEncodingURL,
            SiteTitle,
            SPSiteURL
        ),
        StartRow = 0,
        Timeout = 30000,
        TrimDuplicates = True
    }
}

In the request above, we return the first 100 documents (excluding duplicates) stored in all Office 365 groups and we also retrieve some additional information such as the title of the documents (Title), the URL of the file (DefaultEncodingURL), the name of the group where it’s stored (SiteTitle) and the URL of the group (SPSiteURL).

If you want to be more precise on your search (e.g. search for documents that have a title which start with the word “Program“), then you can add new filters to your query such as the example below.

Title:Program* AND ContentClass:STS_ListItem_DocumentLibrary AND SiteTemplate:GROUP

As we have seen in this article, it’s pretty simple to search documents across all Office 365 groups and it’s just up to you to integrate and to use REST API in your different applications (mobile, Office add-ins, WebParts…).

Programmez #190 : Developing with Delve and Office 365 Video REST APIs

Interested in developing applications that use Delve and/or Office 365 Video using REST APIs ?

Take a look to my article in the latest issue of Programmez, a magazine available in French-speaking countries (France, Belgium, Switzerland, Luxemburg, Canada…).

Programmez 190

NSURLSession/NSURLConnection HTTP load failed when trying to stream an Office 365 Video on iOS 9

Yesterday, I was playing with Office 365 Video and I wanted to be able to play/stream videos from Office 365 into an iOS app.

The first step to be able to read a video was to retrieve the streaming URL of that video in Azure Media Services (which is used behind Office 365 to offer streaming capabilities).

It’s pretty simple using the Office 365 REST API :

https://tenant.sharepoint.com/portals/hub/_api/VideoService/Channels(guid'01234567-abcd-cded-1234-1234567890ab')/Videos(guid'01234567-abcd-cded-1234-1234567890ab')/GetPlaybackUrl(0)

When you execute that request, you retrieve the URL to play/stream the video from Azure Media Services in HLS format (HTTP Live Streaming). This URL should look something like :

https://cvprdb302v.cloudvideo.azure.net/api/ManifestProxy?playbackUrl=https://cdn-cvprdb302m01.streaming.mediaservices.windows.net/.../Manifest(format=m3u8-aapl)&token=...

To play/stream the video into an iOS app, you can use the built-in video player (AVPlayer) which is compatible with HLS format. To do that, you just have to write few lines of code :

let URL = NSURL(string: "...")  // Put here the URL retrieved from GetPlaybackUrl
let playerViewController = AVPlayerViewController()
playerViewController.player = AVPlayer(URL: URL!)
presentViewController(playerViewController, animated: true, completion: nil)
playerViewController.player?.play()

If you run this code on iOS 8, there’s no problem and everything should work fine. But if you run the same code on iOS 9, it should throw an exception like that :

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)

You have to wonder why the same code don’t work on the last version of iOS. The answer is quite simple : App Transport Security (ATS).

In iOS 9, there are new security mechanisms that have been implemented to ensure that, when an app uses network communications, the traffic is encrypted to ensure confidentiality.

But if you look the URL of the video, it uses HTTPS so the traffic is encrypted. Then why an exception was thrown ? It seems that the ciphers used to encrypt the traffic are considered as unsafe by iOS 9.

What to do to be able to play/stream the video in that case ? It’s possible to configure how ATS works by modifying the info.plist file of your app.

There are many ways to do that and the most easiest is to add the following key/values :

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>

For in-house apps or development purposes, it’s very useful but not secured because ATS is completely disabled. If you read Apple’s documentation, it also could be a reason to reject your app if you want to publish it on the AppStore.

If you want to manage more precisely which URLs can be used without problems with ATS, you have to add exception domains in your info.plist as you can see above :

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSExceptionDomains</key>
  <dict>
    <key>mediaservices.windows.net</key>
    <dict>
      <key>NSIncludesSubdomains</key>
      <true/>
      <key>NSExceptionRequiresForwardSecrecy</key>
      <false/>
    </dict>
    <key>cloudvideo.azure.net</key>
    <dict>
      <key>NSIncludesSubdomains</key>
      <true/>
      <key>NSExceptionRequiresForwardSecrecy</key>
      <false/>
 </dict>
</dict>

With these settings, we configure ATS to allow more ciphers to encrypt HTTPS traffic, for two domains (and all sub-domains) used by Azure Media Services to stream videos.

If you prefer to use the editor available in Xcode to edit your info.plist, rather than manually editing the XML file, you just have to add key/values like :

Office365 Video - App Transport Security

Now if we execute our app, we are able to play/stream the video without any exceptions. 😉

Retrieving posts from blogs available in Delve with Office 365 REST API

Office 365 evolves every day and among the latest developments, Delve has been enriched with a new feature that allows users to create blog posts in a much simpler way that the old SharePoint blog.

If you access a user profile from Delve, you can see all the posts for this user as you can see on the screenshot below.

Delve - Blog HomePage

But what happens if you want to get an aggregated view of all posts from all users on your tenant ? At this time there’s no feature like that in Delve or even in Office 365, but you can create your own.

In the same way that there’s no feature in Delve to view all posts in a single page, there’s no official API for now to achieve our goal. But in many cases, there’s a solution based on the search engine 😉

Each posts created in Delve use a specific content type. If we submit a search query based on that content type, we’ll be able to retrieve all posts in a simple and fast way.

As a reminder, to submit a search query in Office 365, you can send a POST request to the following URL with a well-formatted HTTP body.

https://tenant.sharepoint.com/_api/Search/PostQuery

As mentioned previously, we want to retrieve only blog posts based on the content type. The body of our search request should be something like :

{
  request = {
    Querytext = "ContentTypeId:0x010100DA3A7E6E3DB34DFF8FDEDE1F4EBAF95D00B0046F2796B8374B872064182468FA7F",
    RowLimit = 10,
    SelectProperties = (
      Title,
      Author,
      Created,
      DefaultEncodingUrl,
      Path
    ),
    StartRow = 0,
    Timeout = 30000,
    TrimDuplicates = True
  }
}

As you can see above, we specified that only few properties (title, author, creation date…) will be returned in the search result.

It’s an important thing to know. All parts of a blog post (subtitle, body, cover image…) can’t be retrieved using a search request because they are not indexed by the search engine.

Indeed, content parts of a post are not stored in a list, like we generally do in SharePoint, but are serialized to a file in JSON. If you want to know what’s the URL of that file, you just have to read the value of the DefaultEncodingUrl for each items in the search result.

The value should be something like the URL you can see below (each post is a file with .pointpub extension) :

https://tenant.sharepoint.com/portals/personal/katiej/pPg/My-new-blog.pointpub

Now that we know what’s the URL, you are able to read the content of each post. At this time, there’s no documentation about the JSON format used in this file so it’s pretty difficult to understand and to use.

If you want a sample of one of that files, take a look below :

{"Version":"1.0","PostType":1,"Title":"My new blog is available","SubTitle":"My First Blog Post","ThumbnailSource":"#315f7e","Author":"anonymous author","ControlData":{"cid144411757865295382":{"Version":"1.0","IsInternalControl":true,"ControlName":"ImageHeaderControl","ControlType":4,"DataContext":{"ImageControlSize":12,"ImageSourceType":3,"ImageSource":"#315f7e","CaptionText":"","Title":"My new blog is available","Subtitle":"My First Blog Post","Author":"Katie Jordan","PublishDate":"Sun Oct 18 2015 08:04:38 GMT+0200 (CEST)","__type":"ImageHeaderControlDataContext"},"__type":"ControlData"},"cid1444117578747883681":{"Version":"1.0","IsInternalControl":true,"ControlName":"RichTextControl","ControlType":0,"DataContext":{"Subtype":1,"Value":"\u003cp\u003e​A new feature is available on Delve and now you are able to create your blog.\u003c/p\u003e","NoDefaultValue":false,"__type":"TextControlDataContext"},"__type":"ControlData"}},"ControlMap":{"Rows":[{"Columns":[{"ControlId":"cid144411757865295382","HasChildren":false}]},{"Columns":[{"HasChildren":false,"ControlId":"cid1444117578747883681"}]}],"GridSize":12,"__type":"ControlMap"},"__type":"PersistedPostModel"}

But if you only want to enumerate posts, with their title, author, creation date and to redirect the user when he clicks on a link, you have all you need to achieve this goal with a simple search query.

To redirect the user to the web page corresponding to the post, you just have to read the value of the Path property. It will be something like that :

https://tenant.sharepoint.com/portals/personal/katiej/_layouts/15/PointPublishing.aspx?storyid=1

Now you are able to retrieve all blog posts available in your Office 365 tenant. But if you want to retrieve posts for a particular user, you can change the search query to add supplementary filters (e.g : use the AuthorOWSUser property).

I just hope that Microsoft will offer in a near future, an API to simplify the way we can retrieve data, especially the content of each post.

Record of visitors and awarded as Microsoft MVP

Yesterday was a great day for me because two important things happened in the same day.

Firstly, for the 6th consecutive month, the record of visitors on this blog was beaten.

In September 2015, you were 780 to visit this website with 1,100 page views.

Record of visitors - September 2015.

It’s a great pride for me to see that you appreciate the work done here, and that you are more and more numerous each month to read this blog and its content.

Secondly, probably in recognition on the work done in the past months, I’ve been awarded by Microsoft as MVP (Most Valuable Professional) on Office Development.

Microsoft MVP Award

If you don’t know what are Microsoft MVPs, this is what they say on the official website (https://mvp.microsoft.com) :

Microsoft Most Valuable Professionals, or MVPs, are community leaders who’ve demonstrated an exemplary commitment to helping others get the most out of their experience with Microsoft technologies“.

It’s a great honour for me to be recognized for my work but it’s also thanks to you that this could have happened.

So I would like to thank all of you, visitors of this website, and Microsoft people who appreciated what I’ve done in the past months.

I hope this will continue in the next months/years with a growing success for all of us 😉

Share your SharePoint sites with external users using CSOM

Yesterday on the Office 365 Yammer Network, someone asked if it was possible to add external users to a group using CSOM.

After a quick research, I found that it was a recurrent question and there’s no clear answer to that question. So I decided to investigate and I found the answer 🙂

In CSOM, there’s a namespace called Microsoft.SharePoint.Client.Sharing in which there are two useful classes called WebSharingManager and DocumentSharingManager.

These two classes are used to share a site or a document with users (internal or external).

In my case, I want to share a site with an external user so I need to use WebSharingManager.

This class has a method called UpdateWebSharingInformation with the following signature :

public static IList<UserSharingResult> UpdateWebSharingInformation(
  ClientRuntimeContext context,
  Web web,
  IList<UserRoleAssignment> userRoleAssignments,
  bool sendServerManagedNotification,
  string customMessage,
  bool additivePermission,
  bool allowExternalSharing
)

As you can see, there are many parameters which are very easy to understand but the important parts are userRoleAssignments and allowExternalSharing.

The first is a collection of users with whom you want to share the resource. The second is to enable external sharing if the users are not employed by your company.

How to use it in practice ? It’s very easy.

using(var password = new SecureString())
{
  foreach(var c in "myPassword".ToCharArray())
    password.AppendChar(c);

  using(var ctx = new ClientContext("https://tenant.sharepoint.com/"))
  {
    ctx.Credentials = new SharePointOnlineCredentials("user@tenant.onmicrosoft.com", password);

    var users = new List<UserRoleAssignment>();
    users.Add(new UserRoleAssignment()
    {
      UserId = "username@externaldomain.com",
      Role = Role.View
    });

    WebSharingManager.UpdateWebSharingInformation(ctx, ctx.Web, users, true, null, true, true);
    ctx.ExecuteQuery();
  }
}

When you execute the code above, the external user is invited as a viewer in the site associated to the CSOM context. He will receive an email to access to the site.

If you want to know if the invitation was sent successfully and if the user has accepted (or not), you just have to go to Access requests and invitations through the site settings.

External Sharing / Pending Requests

If you want more information about WebSharingManager and DocumentSharingManager, just go to the official documentation on MSDN.