Come accedere agli allegati di un messaggio da un add-in di Outlook

Introduzione

Ciao a tutti,
in questo post vedremo come accedere agli allegati di una mail da un add-in per Outlook utilizzando un servizio remoto.

Il punto di partenza è che da un add-in per O365 non è possibile accedere agli allegati del messaggio in modo diretto.
Esistono però delle API, dette appunto attachments API, che forniscono una serie di informazioni che possiamo utilizzare per richiedere i files direttamente al server Exchange.

Per risolvere il probema quindi, ci appoggeremo ad un servizio remoto (nel nostro caso una applicazione ASP.NET WebApi), che si occuperà di richiedere i files al server e performare le dovute elaborazioni. 
In particolare le informazioni necessarie sono:

1) La url del server Exchange da richiamare
2) Il token di autenticazione
3) Un elenco di oggetti di tipo AttachmentDetails che contengono le informazioni relative all’allegato (id, tipo, nome, dimensione ecc..)

Per poter ottenere queste informazioni occorre utilizzare il metodo getCallbackTokenAsync dell’oggetto Office.context.mailbox. Come si evince dal nome questo metodo è asincrono, pertanto accetta come parametro la funzione di callback che verrà richiamata una volta completata la richiesta, e dove avremo a disposizione tutti i dati necessari al nostro servizio per effettuare la richiesta.
Ma andiamo con ordine.

 

Creazione dell’add-in per Outlook

Per iniziare cominciamo a creare un nuovo progetto Visual Studio di tipo “Outlook add-in” (occorre aver installato gli Office Developer Tools per VS)

image

Nella pagina MessageRead.html modifichiamo il body rimuovendo I controlli generati da VS e aggiungendo un solo bottone per lanciare la procedura

    1: <body>
    2:     <div id="content-header">
    3:         <div class="padding">
    4:             <p class="ms-font-xl ms-fontColor-themeDarkAlt ms-fontWeight-semilight">Attachments Demo</p>
    5:         </div>
    6:     </div>
    7:     <div id="content-main" class="ms-Grid">
    8:         <button id="testGetAttachments">Test Attachments</button>
    9:     </div>
   10:  
   11:     <!-- FabricUI component used for displaying notifications -->
   12:     <div class="ms-MessageBanner" style="position:absolute;bottom: 0;">
   13:         <div class="ms-MessageBanner-content">
   14:             <div class="ms-MessageBanner-text">
   15:                 <div class="ms-MessageBanner-clipper">
   16:                     <div class="ms-font-m-plus ms-fontWeight-semibold" id="notificationHeader"></div>
   17:                     <div class="ms-font-m ms-fontWeight-semilight" id="notificationBody"></div>
   18:                 </div>
   19:             </div>
   20:             <button class="ms-MessageBanner-expand" style="display:none"><i class="ms-Icon ms-Icon--chevronsDown"></i> </button>
   21:             <div class="ms-MessageBanner-action"></div>
   22:         </div>
   23:         <button class="ms-MessageBanner-close"> <i class="ms-Icon ms-Icon--x"></i> </button>
   24:     </div>
   25: </body>

Ora passiamo al file MessageRead.js dove eliminiamo tutte le funzioni tranne la Office.Initialize (necessaria) e la showNotification (utile in seguito) e aggiungiamo una nuova funzione chiamata GetAttachmentsInfo.
Impostiamo poi questa funzione come handler dell’evento click del nostro bottone nella Office.Initialize

    1: Office.initialize = function (reason) {
    2:         $(document).ready(function () {
    3:             var element = document.querySelector('.ms-MessageBanner');
    4:             messageBanner = new fabric.MessageBanner(element);
    5:             messageBanner.hideBanner();
    6:         });
    7:          
    8:         //Imposto l'handler del bottone
    9:         $("#testGetAttachments").click(GetAttachmentsInfo);
   10:     };
   11:  
   12:     function GetAttachmentsInfo() {
   13:         
   14:     }
   15:  
   16:     // Helper function for displaying notifications
   17:     function showNotification(header, content) {
   18:         $("#notificationHeader").text(header);
   19:         $("#notificationBody").text(content);
   20:         messageBanner.showBanner();
   21:         messageBanner.toggleExpansion();
   22:     }

Una volta impostate le basi dell’add-in, prima di procedere dobbiamo aggiungere alla solution il nuovo progetto del servizio remoto che si occuperà di ricevere le informazioni dall’add-in e di richiedere al server Exchange gli allegati della mail

 

Creazione del servizio remoto

Come anticipato nell’introduzione, per il nostro servizio utilizzeremo ASP.NET, e in particolare un progetto MVC WebApi che esporrà una semplice Api REST.
Per dialogare con il server di Exchange utilizzeremo Microsoft.Exchange.WebServices, un apposito SDK disponibile come pacchetto NuGet, che ci mette a disposizione una serie di oggetti che semplificano la gestione della comunicazione.

Aggiungiamo quindi un nuovo progetto alla solution, selezionando il template Web API, che chiameremo OutlookAttachmentsDemoService (ai fini della demo non utilizzeremo nessuna autenticazione)

image

Come prima cosa aggiungiamo i modelli.
Nella cartella Models creiamo una nuova classe chiamata AttachmentDetail che espone esattamente le stesse proprietà dell’oggetto attachment della collection Office.context.mailbox.item.attachments presente nell’add-in

    1: namespace OutlookAttachmentsDemoService.Models
    2: {
    3:     public class AttachmentDetail
    4:     {
    5:         public string attachmentType { get; set; }
    6:         public string contentType { get; set; }
    7:         public string id { get; set; }
    8:         public bool isInline { get; set; }
    9:         public string name { get; set; }
   10:         public int size { get; set; }
   11:     }
   12: }

Poi aggiungiamo la classe per la richiesta, che chiameremo AttachmentRequest. Questa classe ci permetterà di passare tutte le informazioni (come l’url, il token ecc..) attraverso una semplice chiamata POST

    1: namespace OutlookAttachmentsDemoService.Models
    2: {
    3:     public class AttachmentRequest
    4:     {
    5:         public string attachmentToken { get; set; }
    6:         public string ewsUrl { get; set; }
    7:         public string service { get; set; }
    8:         public AttachmentDetail[] attachments { get; set; }
    9:     }
   10: }

e la classe AttachmentResponse per la risposta. Per questo esempio, faremo ritornare all’add-in un array con il nome degli allegati e un intero con il numero totale degli allegati “elaborati” dal servizio

    1: namespace OutlookAttachmentsDemoService.Models
    2: {
    3:     public class AttachmentResponse
    4:     {
    5:         public string[] attachmentNames { get; set; }
    6:         public int attachmentsProcessed { get; set; }
    7:     }
    8: }

Una volta creati i modelli aggiungiamo un nuovo controller selezionando il template Web API 2 Controller – Empty e chiamandolo AttachmentsController

image     image

Prima di aggiungere i metodi al controller, dobbiamo installare il pacchetto NuGet del quale abbiamo accennato all’inizio, lanciando questo comando dalla console di NuGet image

Adesso che tutte le dipendenze sono a posto, apriamo il file AttachmentsController e aggiungiamo i metodi necessari a soddisfare la richiesta del client. Il primo metodo è il metodo POST che accetta come parametro un oggetto di tipo AttachmentRequest (contenente le info necessarie al server Exchange), e restituisce un oggetto di tipo AttachmentResponse.
Il secondo metodo (privato), è in effetti quello dove viene effettuata la chiamata al server per ottenere gli allegati

    1: public AttachmentResponse Post([FromBody]AttachmentRequest value)
    2: {
    3:     //riceve le informazioni dalla chiamata REST attraverso l'oggetto AttachmentRequest e lo passa
    4:     //al metodo che effettua la chiamata al server Exchange
    5:     var resp = GetAttachmentsFromExchangeServerUsingEWSManagedApi(value);
    6:     return resp;
    7: }
    8:  
    9: private AttachmentResponse GetAttachmentsFromExchangeServerUsingEWSManagedApi(AttachmentRequest request)
   10: {
   11:     var attachmentsProcessedCount = 0;
   12:     var attachmentNames = new List<string>();
   13:  
   14:     // Creo un oggetto di tipo ExchangeService
   15:     ExchangeService service = new ExchangeService();
   16:     //imposto il token di autenticazione ricevuto dall'add-in
   17:     service.Credentials = new OAuthCredentials(request.attachmentToken);
   18:     //imposto la url del server Exchange
   19:     service.Url = new Uri(request.ewsUrl);
   20:  
   21:     // Richiede gli allegati al server
   22:     var getAttachmentsResponse = service.GetAttachments(
   23:                                         request.attachments.Select(a => a.id).ToArray(),
   24:                                         null,
   25:                                         new PropertySet(BasePropertySet.FirstClassProperties,
   26:                                         ItemSchema.MimeContent));
   27:  
   28:     if (getAttachmentsResponse.OverallResult == ServiceResult.Success)
   29:     {
   30:         foreach (var attachmentResponse in getAttachmentsResponse)
   31:         {
   32:             attachmentNames.Add(attachmentResponse.Attachment.Name);
   33:  
   34:             if (attachmentResponse.Attachment is FileAttachment)
   35:             {
   36:                 //mette il contenuto dell'allegato in uno stream
   37:                 FileAttachment fileAttachment = attachmentResponse.Attachment as FileAttachment;
   38:                 Stream s = new MemoryStream(fileAttachment.Content);
   39:                 
   40:                 // Qui è possibile processare il contenuto dell'allegato
   41:             }
   42:  
   43:             if (attachmentResponse.Attachment is ItemAttachment)
   44:             {
   45:                 //mette il contenuto dell'allegato in uno stream
   46:                 ItemAttachment itemAttachment = attachmentResponse.Attachment as ItemAttachment;
   47:                 Stream s = new MemoryStream(itemAttachment.Item.MimeContent.Content);
   48:  
   49:                 // Qui è possibile processare il contenuto dell'allegato
   50:             }
   51:  
   52:             attachmentsProcessedCount++;
   53:         }
   54:     }
   55:  
   56:     // La risposta contiene il nome ed il numero degli allegati che sono stati 
   57:     // processati dal servizio
   58:     var response = new AttachmentResponse();
   59:     response.attachmentNames = attachmentNames.ToArray();
   60:     response.attachmentsProcessed = attachmentsProcessedCount;
   61:  
   62:     return response;
   63: }

Analizzando il codice vediamo che la procedura è piuttosto semplice. Viene istanziato un oggetto EschangeService al quale vengono passate le informazioni per la connessione (l’url del server e il token di autenticazione), dopo di che viene richiamato il metodo GetAttachments, il quale restituisce una collezione di oggetti GetAttachmentResponse contenenti gli allegati veri e propri (oltre ad una serie di informazioni aggiuntive). A questo link trovate maggiori informazioni sulle EWS Managed API

A questo punto va fatta una considerazione importante.

Ogni chiamata effettuata dall’interno di un add-in O365 deve essere verso un indirizzo https, altrimenti viene immediatamente restituito un errore di tipo “Access denied” e la chiamata fallisce immediatamente.

Per velocizzare lo sviluppo del nostro progetto, la cosa più semplice da fare a questo punto è quella di pubblicare subito su Azure la nostra Web App. Infatti Azure ci permette di avere una url https a “costo zero”, senza doverci preoccupare di generare un certificato al solo fine di testare l’applicazione.

E’ possibile effettuare la pubblicazione in pochi semplici passi direttamente da Visual Studio,

Clicchiamo con il tasto destro sul progetto web e selezioniamo la voce Publish

image

Selezioniamo Microsoft Azure App Service

image

Nel wizard selezionare l’account legato alla sottoscrizione da utilizzare e cliccare su New per creare una nuova webapp

image

Nella finestra di creazione, specificare il nome della Web App, la sottoscrizione da utilizzare (nel caso ce ne fosse più di una legata all’account), e il nome del Resource Group (tipicamente uguale al nome dell’app). Infine cliccare su Create

image

Una volta completata la procedura di creazione della Web App, viene mostrato nel wizard il riepilogo delle informazioni, tra cui l’indirizzo del servizio.
A questo punto è sufficiente cliccare su Publish per completare il processo di pubblicazione

image

 

Integrazione del servizio

Una volta pubblicata la Web App su Azure torniamo al progetto dell’add-in per effettuare la chiamata al servizio appena creato.

Nel file MessageRead.js aggiungiamo la funzione di callback per il metodo asincrono di cui abbiamo accennato nella prima parte

    1: function attachmentTokenCallback(asyncResult, userContext) {
    2:     //creo un oggetto per memorizzare i dati degli allegati
    3:     //da passare al servizio
    4:     var serviceRequest = new Object();
    5:     serviceRequest.attachmentToken = "";
    6:     serviceRequest.ewsUrl = "";
    7:     serviceRequest.state = -1;
    8:     serviceRequest.attachments = new Array();
    9:  
   10:     if (asyncResult.status === "succeeded") {
   11:         //valorizzo le proprietà (token, url e info sugli allegati)
   12:         serviceRequest.attachmentToken = asyncResult.value;
   13:         serviceRequest.state = 3;
   14:         serviceRequest.ewsUrl = Office.context.mailbox.ewsUrl;
   15:         serviceRequest.attachments = new Array();
   16:         for (var i = 0; i < Office.context.mailbox.item.attachments.length; i++) {
   17:             serviceRequest.attachments[i] = JSON.parse(JSON.stringify(Office.context.mailbox.item.attachments[i]._data$p$0));
   18:         }
   19:  
   20:         //richiamo il metodo del controller passando l'oggetto serviceRequest
   21:         $.ajax({
   22:             url: 'https://outlookattachmentsdemoservice.azurewebsites.net/api/attachments',
   23:             type: 'POST',
   24:             contentType: 'application/json; charset=utf-8',
   25:             cache: false,
   26:             data: JSON.stringify(serviceRequest)
   27:         }).done(function (response) {
   28:             var names = "";
   29:             for (var v = 0; v < response.attachmentNames.length; v++) {
   30:                 names = names + ", " + response.attachmentNames[v];
   31:             }
   32:  
   33:             showNotification("Attachments processed", "Number of attachments: " + response.attachmentsProcessed +
   34:                                                        " (" + names + ")");
   35:         }).fail(function (status) {
   36:             showNotification("Error", status.statusText);
   37:         });
   38:  
   39:     } else {
   40:         showNotification("Error", "Could not get callback token: " + asyncResult.error.message);
   41:     }
   42: }

In questa funzione viene creato un oggetto (serviceRequest) che come potete notare ha le stesse proprietà della classe AttachmentRequest del progetto web. In questo oggetto vengono memorizzate le informazioni di accesso al server Exchange e un array di attachments, ognuno dei quali contiene le informazioni dell’allegato (id, nome ecc..)

Infine viene effettuata una chiamata al metodo POST del nostro servizio utilizzando l’indirizzo https.

Nell’handler done abbiamo l’oggetto response che conterrà le informazioni relative al numero e al nome degli allegati elaborati (il nostro oggetto AttachmentResponse) e viene semplicemente composto un messaggio da mostrare a video tramite showNotification.

Infine aggiungiamo la chiamata a questa funzione nell’handler del bottone

    1: function GetAttachmentsInfo() {
    2:     //richiamo l'API per ottenere le informazioni sugli allegati della mail
    3:     Office.context.mailbox.getCallbackTokenAsync(attachmentTokenCallback);
    4: }

(Qui trovate la documentazione ufficiale con maggiori informazioni sugli attachments)

 

A questo punto va fatta la seconda importante considerazione.

Proviamo infatti a mandare in esecuzione il progetto. Per prima cosa ci viene richiesto l’account O365 al quale collegarsi (e fin qui tutto ok)

image

e successivamente viene aperto il client Outlook (o il client web a seconda di cosa è stato impostato nelle proprietà del progetto) dove selezionando un messaggio di posta troviamo il bottone che apre il task panel del nostro add-in

image

Se clicchiamo sul bottone Test Attachments però, ci viene restituito un messaggio di errore. Questo errore è dovuto al fatto che siccome stiamo richiamando un servizio che non è sullo stesso dominio dell’add-in (che nel nostro caso è localhost) la chiamata risulta essere cross-domain e per ragioni di sicurezza viene bloccata.

Per ovviare a questo problema ci sono 2 possibilità:

1) Utilizzare JSONP

2) Abilitare il CORS (cross-origin resource sharing) lato server

Nel nostro esempio non possiamo utilizzare JSONP in quanto è supportato solo per le chiamate GET, per cui vediamo come abilitare il CORS nel nostro servizio remoto.

 

Abilitazione CORS

Per abilitare il CORS utilizzeremo il pacchetto NuGet Microsoft.AspNet.WebApi.Cors che va installato nel progetto Web Api.

Dalla console di NuGet lanciare il seguente comando sul progetto Web

image

 

Una volta installato il pacchetto possiamo abilitare il cors in 2 semplici passi. Per prima cosa occorre richiamare il metodo config.EnableCors() all’interno del metodo Register nel WebApiConfig

image

Infine occorre aggiungere l’attributo EnableCors al controller, in modo da poter specificare le origini consentite. Nel nostro esempio utilizziamo l’asterisco (*), ma è possibile elencare i singoli domini separati dalla virgola

    1: namespace OutlookAttachmentsDemoService.Controllers
    2: {
    3:     [System.Web.Http.Cors.EnableCors(origins: "*", headers: "*", methods: "*")]
    4:     public class AttachmentsController : ApiController
    5:     {

 

A questo punto possiamo ripubblicare il servizio e tornare a testare il nostro add-in, che questa volta funzionerà correttamente mostrando le informazioni degli allegati “elaborati”

image

Per maggiori informazioni sulle same-origin policy potete fare riferimento a questa documentazione

La solution completa è disponibile su GitHub a questo indirizzo

Good coding!