0

I am trying to upload a large (> 4mb) attachment to an existing message in Office 365 using Microsoft Graph Java SDK 2.10. I am following these instructions: https://learn.microsoft.com/en-us/graph/outlook-large-attachments?tabs=java

I have successfully created the upload session, and obtained a uploadUrl value that looks like the example in the documentation. Then I start my PUT to this url using ChunkedUploadProvider.

        // Create an upload session
        UploadSession uploadSession = client.me()
                .messages(messageId).attachments()
                .createUploadSession(attachmentItem)
                .buildRequest()
                .post();

        ChunkedUploadProvider<AttachmentItem> chunkedUploadProvider =
                new ChunkedUploadProvider<AttachmentItem>
                        (uploadSession, client, fileStream, attachmentItem.size, AttachmentItem.class);

        // Config parameter is an array of integers
        // customConfig[0] indicates the max slice size
        // Max slice size must be a multiple of 320 KiB
        int[] customConfig = { 12 * 320 * 1024 }; // still < 4MB as API recommended

        // Do the upload
        try {
            chunkedUploadProvider.upload(callback, customConfig);
        } catch (Exception e) {
            log.error("Upload attachment file name {} for message id {}", fileAttachment.name, messageId, e);
        }

My problem is that I get http 401 (Unauthorized) in response:

{
  "error": {
    "code": "InvalidMsaTicket",
    "message": "ErrorCode: \u0027PP_E_RPS_CERT_NOT_FOUND\u0027. Message: \u0027 Internal error: spRPSTicket-\u003eProcessToken failed. Failed to call CRPSDataCryptImpl::UnpackData: Internal error: Failed to decrypt data. :Failed to get session key. RecipientId\u003d293577. spCache-\u003eGetCacheItem returns error.:Cert Name: (null). SKI: 3bd72187c709b1c40b994f8b496a5b9ebd2f9b0c...\u0027",
    "innerError": {
      "requestId": "7276a164-9c13-41cc-b46a-4a86303017a6",
      "date": "2020-09-17T04:55:15"
    }
  }
}

I noticed that the request to create upload session is:

https://graph.microsoft.com/v1.0/me/messages/AQMkADAwATM3ZmYAZS0xNWU2LTc4N1agAAAA==/attachments

while the uploadUrl is:

https://outlook.office.com/api/v2.0/Users('00037ffe-15e6-787e-0000-00000')/Messages('AQMkADAwATM3ZmYAZS0xNVtUUgAAAA==')/AttachmentSessions('AQMkADAwwAAAA=')?authtoken=eyJhbGciOiJSUzI1NiIsImtpZCI6IktmYUNIUlN6bllHMmNIdDRobk9JQnpndlU5MD0iL

which is a different API (Graph vs Outlook).

I already have mail read.write scope added, and that allows me to create a < 4mb attachment. I tried to put "https://outlook.office.com/Mail.ReadWrite" into the scopes when getting the access token but got the same invalid_token issue. What should I do to resolve the issue?

Any help would be appreciated. Thank you.

Duy Ngo
  • 21
  • 7
  • I saw a similar question at https://stackoverflow.com/questions/59726497/uploading-a-large-attachment-using-microsoft-graph which got a different error message ("The audience claim value is invalid for current resource...") asked by @JeffMcKay. I am not sure how he resolved his issue. – Duy Ngo Sep 17 '20 at 08:15
  • How did you request the token? You need to set the scope to your audience, and your audience must be your token recipient. – Carl Zhao Sep 17 '20 at 08:21
  • Check your 'aud' to ensure that it is the api you want to call, and the reason for the 401 You may have used the wrong token or you have used a token that does not belong to the api to call the api. – Carl Zhao Sep 17 '20 at 08:25
  • @CarlZhao The uploadUrl was generated and returned in the response of the createUploadSession API. The authtoken param in uploadUrl has 'aud' is "https://outlook.office.com/api/" which I assume it's correct. I've put "https://outlook.office.com/Mail.ReadWrite" into the scopes when getting the access token from "https://login.microsoftonline.com/common/oauth2/v2.0/token" API as user consent but it didn't help. Is it possible to get an access token for a combination of Graph and Outlook scopes, e.g.: Mail.ReadWrite and https://outlook.office.com/Mail.ReadWrite, that works for both API? – Duy Ngo Sep 17 '20 at 09:50

2 Answers2

1

My bad. The request should not contain Authorization header:

Do not specify an Authorization request header. The PUT query uses a pre-authenticated URL from the uploadUrl property, that allows access to the https://outlook.office.com domain.

Removed the Authorization header and the request works properly.

Duy Ngo
  • 21
  • 7
  • How did you remove the authorization header from the graph client ? I'm facing the same problem with chunkedUploadProvider.upload. Thx – timp Feb 17 '21 at 15:36
  • @timp Just do not specify it in the PUT query. – Duy Ngo Apr 27 '21 at 03:26
1

Just use plain cURL PUT request to that URL and it will work... In PHP it will be something like this (using the php-curl-class/php-curl-class and microsoft/microsoft-graph composer libraries):

function uploadLargeFileData(Graph $graph, string $messageId, string $fileName, string $fileContentType, string $fileData) : bool {

  $fileSize = strlen($fileData);
  // create upload session
  $attachmentItem = (new AttachmentItem())
    ->setAttachmentType('file')
    ->setName($fileName)
    ->setSize($fileSize)
    ->setContentType($fileContentType);

  /** @var UploadSession $uploadSession */
  try {
    $uploadSession = $graph->createRequest('POST', "/me/messages/{$messageId}/attachments/createUploadSession")
      ->addHeaders(['Content-Type' => 'application/json'])
      ->attachBody(['AttachmentItem' => $attachmentItem])
      ->setReturnType(UploadSession::class)
      ->execute();
  }
  catch (GraphException $e) {
    return false;
  }

  $fileData = str_split($fileData, self::LargeFileChunkSize);
  $fileDataCount = count($fileData);

  foreach ($fileData as $chunkIndex => $chunkData) {
    $chunkSize = strlen($chunkData);
    $rangeStart = $chunkIndex * self::LargeFileChunkSize;
    $rangeEnd = $rangeStart + $chunkSize - 1;

    try {
      // we need to use this plain access, because calling the API via the Graph provider leads to strange error of invalid audience
      $curl = new Curl();
      $curl->setHeaders([
          'Content-Length' => $chunkSize,
          'Content-Range' => "bytes {$rangeStart}-{$rangeEnd}/{$fileSize}",
          'Content-Type' => 'application/octet-stream',
        ]);

      $curl->put($uploadSession->getUploadUrl(), $chunkData);

      if ($chunkIndex < $fileDataCount - 1 && $curl->httpStatusCode != 200) { // partial are responding with 200
        return false;
      }
      elseif ($chunkIndex == $fileDataCount - 1 && $curl->httpStatusCode != 201) { // last response should have code 201
        return false;
      }
    }
    catch (\Exception $e) {
      return false;
    }
  }

  return true;
}
Richard Toth
  • 781
  • 6
  • 7