Office365 Rest API via Akka Streams and Standalone Play WS client.
This is more like a prototype of a library with a limited set of features. Not ready for production.
In order to use Office365Api
, you must have a valid OAuth 2.0 access token and a way to refresh it. Create an instance of CredentialData
first:
import com.github.lpld.office365.CredentialData
import com.github.lpld.office365.TokenRefresher.{TokenResponse, TokenSuccess}
import scala.concurrent.Future
// OAuth 2.0 credential
val credential = CredentialData(
initialToken = getAccessToken,
refreshAction = refreshAccessToken
)
// get existing access token, may return None:
def getAccessToken: Option[TokenSuccess] = ???
// perform OAuth 2.0 token refresh flow:
def refreshAccessToken(): Future[TokenResponse] = ???
Now you can create an instance of Office365Api
:
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import play.api.libs.ws.ahc.StandaloneAhcWSClient
// first, create actor system and akka-streams materializer
implicit val system = ActorSystem("api-examples")
implicit val materializer = ActorMaterializer()
val wsClient: StandaloneWSClient = StandaloneAhcWSClient()
val api = Office365Api(wsClient, credential)
// ... do your work
// close all resources:
api.close()
system.terminate()
Define a case class with a set of fields that are needed. It should extend one of the traits that represent standard Outlook item types: OMailFolder, OMessage, OCalendarGroup, OCalendar, OEvent, OCalendarView, OCalendarView, OContact, OTaskGroup, OTaskFolder, OTask
:
import com.github.lpld.office365.model.{OMessage, Recipient}
import com.github.lpld.office365.Schema
import play.api.libs.json.{Json, Writes}
case class EmailMessage(Id: String,
Subject: String,
From: Option[Recipient],
ToRecipients: List[Recipient]) extends OMessage
// companion object with the Schema and Play's json Reads/Writes:
object EmailMessage {
// A Schema is just a set of fields
implicit val schema = Schema[EmailMessage] // this will generate a schema from the class fields
// Play's Reads
implicit val reads = Json.reads[EmailMessage]
}
The list of standard fields can be found in the official Office365 API documentation: https://msdn.microsoft.com/en-us/office/office365/api/api-catalog
Methods that represent queries to the API return Source[I, NotUsed]
, where I
is a type of requested items. No actuall http requests will be done until the source is materialzed.
// source containing zero or one element
val item: Source[EmailMessage, NotUsed] = api.get[EmailMessage](itemId)
item.runWith(...)
This will result in the following request:
GET /messages/{itemId}?$select=Id,Subject,From,ToRecipients
val items: Source[EmailMessage, NotUsed] =
api.query[EmailMessage](
filter = "ReceivedDateTime ge 2018-01-01T00:10:00Z",
orderby = "ReceivedDateTime"
)
items.runWith(...)
This will result in a series of requests to the API, each one loading the next page of items. The first request will look like:
GET /messages
?$select=Id,Subject,From,ToRecipients
&$filter=ReceivedDateTime ge 2018-01-01T00:10:00Z
&$orderby=ReceivedDateTime
&$top=100
&$skip=0
This method is lazy, in a sense that it does not load additional pages until they are actually needed. It means that the following example will load only the first page of data even if more items are available:
val items = api.queryAll[EmailMessage]
items.take(1).runForeach(println)
import com.github.lpld.office365.model.{OEvent, FolderType}
import com.github.lpld.office365.Schema
import play.api.libs.json.Json
// Model for an event item:
case class Event(Id: String, Subject: String) extends OEvent
object Event {
implicit val schema = Schema[Event]
implicit val reads = Json.reads[Event]
}
// querying events from a specific calendar folder:
val calendarId = "<id of the calendar>"
val events = api
.from(FolderType.Calendar, calendarId)
.queryAll[Event]
events.runWith(...)
Following HTTP request will be executed:
GET /calendars/<id of the calendar>/events&$top=100&$skip=0
, followed by requests to load rest of the pages.
import com.github.lpld.office365.model.WellKnownFolder
val items: Source[EmailMessage, NotUsed] =
api
.from(WellKnownFolder.SentItems) // mailbox folder with a well-known name
.query[EmailMessage](
filter = "ReceivedDateTime ge 2018-01-01T00:10:00Z"
)
items.runWith(...)
Following request will be executed:
GET /mailfolders/SentItems/messages
?$select=Id,Subject,From,ToRecipients
&$filter=ReceivedDateTime ge 2018-01-01T00:10:00Z
&$top=100
&$skip=0
reactive-office365 has a very limited support for Outlook Extended Properties (https://msdn.microsoft.com/en-us/office/office365/api/extended-properties-rest-operations). Currently, only reading of SingleValueExtendedProperties
is supported:
import com.github.lpld.office365.ExtendedProperty
import com.github.lpld.office365.model.{OMessage, ExtendedPropertiesSupport, SingleValueProperty}
import com.github.lpld.office365.Schema
import play.api.libs.json.Json
// Define the extended properties, for instance:
// Item class (see https://msdn.microsoft.com/en-us/vba/outlook-vba/articles/item-types-and-message-classes)
val ItemClassProp = ExtendedProperty("ItemClass", "String 0x1a")
// `In-Reply-To` internet message header
val InReplyTo = ExtendedProperty("InReplyTo", "String 0x1042")
// Define a model class that extends `ExtendedPropertiesSupport` trait and `SingleValueExtendedProperties` field:
case class MessageExtended(Id: String,
Subject: String,
SingleValueExtendedProperties: List[SingleValueProperty])
extends OMessage with ExtendedPropertiesSupport {
// optionally, you can define getters for your properties:
def itemClass: Option[String] = getProp(ItemClassProp)
}
// Companion object for the item class with extended schema, containing extended properties:
object MessageExtended {
implicit val reads = Json.reads[MessageExtended]
implicit val schema = Schema[MessageExtended](ItemClassProp, InReplyTo)
}
Now, items can be queried in a regular way:
val itemByIdSource = api.get[MessageExtended](itemId)
// or
val sentItemsSource = api.from(WellKnownFolder.SentItems).queryAll[MessageExtended]
val item: Option[MessageExtended] =
Await.result(itemByIdSource.runWith(Sink.head), 5.seconds)
// now, read the properties:
val inReplyTo: Option[String] = item.getProp(InReplyTo)
// or using previously defined getter:
val itemClass: Option[String] = item.itemClass
to be continued...