I'm trying to send an email with an attachment bigger than 4MB through Microsoft Graph.
Searching around the web I came to the conclusion that for files bigger than 4MB, I would need to upload the file to onedrive through an upload session, and then add it to the email as a "reference attachment".
So far my code looks like this, first I upload the file to one drive
var graphClient = GetGraphClient();
var rootItem = graphClient.Users[userEmail].Drive.Root.Request().GetAsync().Result;
uploadSession = graphClient.Users[userEmail].Drive.Items[rootItem.Id].ItemWithPath(fullFileName).CreateUploadSession().Request().PostAsync().Result;
fullFileByteArray = Convert.FromBase64String(content);
stream = new MemoryStream(fullFileByteArray);
provider = new ChunkedUploadProvider(uploadSession, graphClient, stream);
driveItem = provider.UploadAsync(3).Result;
This works just fine, I upload the file to one drive and I get the id of said file. Next step is creating the email as a draft.
var email = await graphClient.Users[fromEmail].Messages.Request().AddAsync(message);
This also works fine, I create the email as a draft (I can see it on outlook), and I get the Id of said attachment.
Now onto the problematic part, when I try to add the attachments to the draft email.
foreach (var att in fileAttachments)
{
var attachment = new ReferenceAttachment();
attachment.Name = att.FullFileName;
attachment.ContentType = att.MIMEType;
attachment.Id = att.FileId;
attachment.ODataType = "#microsoft.graph.referenceAttachment";
attachment.Size = att.Size;
attachmentsParsed.Add(attachment);
await graphClient.Users[fromEmail].Messages[email.Id].Attachments.Request().AddAsync(attachment);
}
When I try to execute the AddAsync() it responds with:
Message: The property 'SourceUrl' is required when creating the entity.
Inner error:
AdditionalData:
request-id: {{someId}}
date: {{someDate}}
ClientRequestId: {{someId}}
) ---> Microsoft.Graph.ServiceException: Code: ErrorInvalidProperty
Message: The property 'SourceUrl' is required when creating the entity.
Inner error:
AdditionalData:
request-id: {{someId}}
date: {{someDate}}
ClientRequestId: {{someId}}
The thing is ReferenceAttachment does not have a SourceUrl property, neither do I find a sourceUrl parameter in the response from uploading the file. I tried adding it to the AdditionalData property of the attachment, which is a Dictionary, but it didn't work.
Also tried to send the request through Postman like this (tried to send it to the beta version of the API too):
{
"name":"{{someName}}",
"#odata.type": "#microsoft.graph.referenceAttachment",
"sourceUrl": "{{someUrl}}",
"contentType":"text/plain",
"id":"{{someId}}",
"size":"1553",
}
And the response was
{
"error": {
"code": "ErrorInvalidProperty",
"message": "The property 'SourceUrl' is required when creating the entity.",
"innerError": {
"request-id": "{{someId}}",
"date": "{{someDate}}"
}
}
}
Where do I add this SourceUrl property and where do I get it from?
I would suggest using the uploading of large file attachments directly to the Message without having to upload to onedrive first. This is described in the link below.
https://learn.microsoft.com/en-us/graph/outlook-large-attachments?tabs=http
There now exists support for this scenario in the latest version of the .Net SDK in the form of a LargeFileUploadTask.
Essentially, you will need to do the following steps
create an upload session for the attachment as follows. (Note that this available in the beta library for now https://www.nuget.org/packages/Microsoft.Graph.Beta/0.10.0-preview)
var fileStream; // file stream you wish to upload to the attachment
var attachmentItem = new AttachmentItem
{
AttachmentType = AttachmentType.File,
Name = "flower",
Size = fileStream.Length
};
await graphClient.Me.Messages[email.Id].Attachments
.CreateUploadSession(attachmentItem)
.Request()
.PostAsync();
Create the task using the stream you wish to upload.
// Create task
LargeFileUploadTask<FileAttachment> largeFileUploadTask = new LargeFileUploadTask<FileAttachment>(uploadSession, fileStream);
(Optional) You can setup monitoring of the upload progress as by creating an instance of IProgress
// Setup the progress monitoring
IProgress<long> progress = new Progress<long>(progress =>
{
Console.WriteLine($"Uploaded {progress} bytes of {stream.Length} bytes");
});
Upload the attachment to the message!
UploadResult<DriveItem> uploadResult = null;
try
{
uploadResult = await largeFileUploadTask.UploadAsync(progress);
if (uploadResult.UploadSucceeded)
{
//File attachement uplaod only returns the location URI on successful upload
Console.WriteLine($"File Uploaded {uploadResult.Location}");//Sucessful Upload
}
}
catch (ServiceException e)
{
Console.WriteLine(e.Message);
}
If you query the attachment of the message you should now view the attachement you added without any worries
var attachements = await graphClient.Me.Messages[email.Id].Attachments.Request().GetAsync();
Related
We have two applications: A C# REST-API, and a Kotlin Android application, we are using Google Platform Cloud Bucket to host the images.
A picture will be uploaded on the Android application, but the C# REST-API needs to upload it to the Google Cloud Platform.
This is the working C# code to upload a file to the Google Cloud Buckets:
[HttpPost]
[Route("upload")]
public IActionResult Upload()
{
var storageClient = StorageClient.Create(google_credentials);
string fileToUpload ="/Users/niel/Downloads/new_cat.jpg";
using (var fileStream = new FileStream(fileToUpload, FileMode.Open,
FileAccess.Read, FileShare.Read))
{
storageClient.UploadObject("test_storage_fotos", "new_cat", "image/jpeg", fileStream);
}
Console.WriteLine("uploaded the file successfully");
return Ok();
}
Now I need to replace fileToUpload with the content from a POST-request. Is there a way to do this? Picture from Android app > C# API > Google Buckets? The link from the C# API to Google Buckets is already working.
Is there a way in Kotlin to somehow get the byte-string of an image, post it to my C# API who takes the content and puts it in a FileStream? I than can upload the FileStream using storageClient.UploadObject? Is this a possibility?
Thanks!
Yes, you can definitely do this. Just send the file over to the server via http protocol with multipart/form-data content type.
In kotlin you can use ktor or any other http library to do that.
For ktor you'll need to add an implementation dependency
implementation "io.ktor:ktor-client-android:1.5.4"
And you might also need to add additional permission in AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
Then you can send a file with this snippet. Notice that imageUri is a content uri, for file uri the code would be a bit different
private fun getFileName(resolver: ContentResolver, uri: Uri): String {
val returnCursor: Cursor = resolver.query(uri, null, null, null, null)!!
val nameIndex: Int = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
returnCursor.moveToFirst()
val name: String = returnCursor.getString(nameIndex)
returnCursor.close()
return name
}
suspend fun postAndImage(imageUri: Uri, uploadEndPoint: String) {
val client = HttpClient(Android)
val cr = applicationContext.contentResolver
if(cr.getType(imageUri) == null) {
//process error
return
}
val stream = cr.openInputStream(imageUri)
if(stream == null) {
//process error
return
}
val response: HttpResponse = client.submitFormWithBinaryData(
url = uploadEndPoint,
formData = formData {
append("image", InputProvider { stream.asInput() }, Headers.build {
append(HttpHeaders.ContentType, cr.getType(imageUri)!!)
append(HttpHeaders.ContentDisposition, "filename=${getFileName(cr, imageUri)}")
})
}
)
stream.close()
//process response
}
And you'll need to modify you upload function slightly
[HttpPost]
[Route("upload")]
//the name of the argument must match the key that you pass in "append" function
public async Task<IActionResult> Post(IFormFile image)
{
var storageClient = StorageClient.Create(google_credentials);
using (var stream = image.OpenReadStream())
{
//it's also possible to get original file name from file name property
var fileName = Guid.NewGuid() + "." + Path.GetExtension(image.FileName);
//assuming bucket is already created
var storageObject = await storageClient
.UploadObjectAsync("test_storage_fotos", fileName, "image/jpeg", stream);
//save information about a storage object in database
}
return Ok();
}
I am attempting to do step 1 in C#, according to Attach large files to Outlook messages as attachments from the Microsoft Graph documentation.
The GraphServiceClient and message id I'm using works fine when uploading small attachments. When uploading large attachments, however, I received this error:
The OData request is not supported.
Based on my code below, I believe I have followed the documentation correctly.
Does anyone have any idea why I would get the error message I do?
var attachmentItem = new AttachmentItem
{
AttachmentType = AttachmentType.File,
Name = file.Name,
Size = file.Length //3.5MB test file
};
UploadSession session = await GraphAPIConnection.GraphClient
.Me
.Messages[message.Id]
.Attachments
.CreateUploadSession(attachmentItem)
.Request()
.PostAsync();
I have validated that the file, message, and attachmentItem properties have all been created properly prior to attempting the call.
Using Packages:
Microsoft.Graph.Core.1.20.0
Microsoft.Graph.Beta.0.12.0-preview
UPDATE 1:
I discovered that my endpoint when creating my GraphServiceClient was using v1.0 rather than beta. Upon changing it, I was met with the following error further into my code:
InvalidAudience
using (Stream fileStream = System.IO.File.OpenRead(file.FullName))
{
if (session != null)
{
int maxSizeChunk = (320 * 1024) * 4;
List<Exception> exceptions = new List<Exception>();
byte[] readBuffer = new byte[maxSizeChunk];
ChunkedUploadProvider uploadProvider =
new ChunkedUploadProvider
(session, GraphAPIConnection.GraphClient, fileStream, maxSizeChunk);
IEnumerable<UploadChunkRequest> chunkRequests =
uploadProvider
.GetUploadChunkRequests();
foreach (UploadChunkRequest request in chunkRequests)
{
UploadChunkResult result =
await uploadProvider
.GetChunkRequestResponseAsync
(request, readBuffer, exceptions);
//ERROR HERE
}
}
}
My impression is that Graph does not need to use the alternative Outlook API. Yet, the upload session seems to use it regardless:
request.RequestUrl:
https://outlook.office.com:443/api/beta/...
request.Client.BaseUrl:
https://graph.microsoft.com/beta
Does this mean that I need additional scopes to properly access that API?
Do I need to generate a new client just to do so?
I am trying to send a user model from dart to the api where my file is set as "IFormFile" data type in my c# backend.
I tried using the multipart request but all i get is the error stated , i can't understand why it cannot retrieve the length of file.
This is my code:
updateUser() async {
var uri = Uri.parse('http://myIP:8070/api/Users');
var request = http.MultipartRequest('Put', uri);
request.fields['id']="07bb2a17-7cd5-471b-973a-4b77d239b6c3";
request.fields['username']="beeso";
request.fields['email']="jake-username2#gmail.com";
request.fields['password']="Jake123-";
request.fields["oldPassword"]="Jake124-";
request.fields["gender"]="Male";
request.fields["dateOfBirth"]=DateTime.now().toString();
request.fields["name"]="dsjnss";
request.fields["languages"]=["Language1","Language2"].toString();
request.fields["nationalities"]=["Nationality1","Nationality2"].toString();
request.fields["phoneNumber"]="70502030";
request.fields["biography"]="jdnknkdas";
request.fields["info"]="asndasnkdas";
request.fields["religion"]="Christian";
request.fields["location"]="LA";
File imageFile = new File('Anatomy_of_a_Sunset-2.jpg');
var stream = new http.ByteStream(DelegatingStream.typed(imageFile.openRead()));
var length = await imageFile.length();
var multipartFile = new http.MultipartFile('file', stream, length,
filename: basename(imageFile.path));
request.files.add(multipartFile);
Map<String, String> headers = {
"content-type": "application/json"
};
request.headers.addAll(headers);
http.StreamedResponse response = await request.send();
print(response.statusCode);
}
Any help would be appreciated.
This picture shows where my file is located
In Flutter, you can't access files directly from the project. You need to add them to an assets folder (typically assets) and also to pubspec.yaml. Then, instead of using File to read them, you use rootBundle (or one of the Asset classes).
var multipartFile = http.MultipartFile.fromBytes(
'file',
(await rootBundle.load('assets/anatomy.jpg')).buffer.asUint8List(),
filename: 'anatomy.jpg', // use the real name if available, or omit
contentType: MediaType('image', 'jpg'),
);
request.files.add(multipartFile);
While testing your API, you may find it easier to just create a Dart command line project, where you do have access to Files in the project folders.
my case was image uploading problem and I solved it by using
xfile.path that image picker returned
XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 50,
maxHeight: 500,
maxWidth: 500);
#flutter
#dio
I am currently trying to allow a user to upload a file to the bot during a dialog flow. From there the bot will take the file and upload it to blob storage. When the file comes in the content property is null, however the content url, name, and type all have the correct values.
public virtual async Task StackTraceGathered(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
FileName = message.Attachments[0].Name;
HttpPostedFileBase file = (HttpPostedFileBase)message.Attachments[0].Content;
string filePath = HttpContext.Current.Server.MapPath("~/Files/" + file.FileName);
file.SaveAs(filePath);
if (message.Attachments != null && message.Attachments.Any())
{
var attachment = message.Attachments.First();
using (HttpClient httpClient = new HttpClient())
{
// Skype & MS Teams attachment URLs are secured by a JwtToken, so we need to pass the token from our bot.
if ((message.ChannelId.Equals("skype", StringComparison.InvariantCultureIgnoreCase) || message.ChannelId.Equals("msteams", StringComparison.InvariantCultureIgnoreCase))
&& new Uri(attachment.ContentUrl).Host.EndsWith("skype.com"))
{
var token = await new MicrosoftAppCredentials().GetTokenAsync();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
var responseMessage = await httpClient.GetAsync(attachment.ContentUrl);
var contentLenghtBytes = responseMessage.Content.Headers.ContentLength;
await context.PostAsync($"Attachment of {attachment.ContentType} type and size of {contentLenghtBytes} bytes received.");
}
}
else
{
await context.PostAsync("Hi there! I'm a bot created to show you how I can receive message attachments, but no attachment was sent to me. Please, try again sending a new message including an attachment.");
}
PromptDialog.Text(context, ProblemStartDuration, "How long has this been an issue? (Provide answer in days, if issue has been occurring for less than one day put 1).");
context.Wait(this.StackTraceGathered);
}
I don't see the issue, but I guess you are expecting the Content property to have something. It won't but you just need the Url. Two alternatives:
Download the attachment in the bot (as the code you are using in the question) and upload to blob storage
Try to upload the attachment directly from the Url using something like StartCopyFromBlob (check this)
I have published my bot on Microsoft teams. Now I want to include a functionality in which, a user can upload a file as an attachment & bot will upload it on blob storage, How to handle this in bot framework?
The attachments sent by the user will end up in the Attachments collection of the IMessageActivity. There you will find the URL of the attachment the user sent.
Then, you will have to download the attachment and add your logic to upload it to Blob storage or any other storage you would like to use.
Here is a C# example showing how to access and download the attachments sent by the user. Added the code below for your reference:
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
if (message.Attachments != null && message.Attachments.Any())
{
var attachment = message.Attachments.First();
using (HttpClient httpClient = new HttpClient())
{
// Skype attachment URLs are secured by a JwtToken, so we need to pass the token from our bot.
if (message.ChannelId.Equals("skype", StringComparison.InvariantCultureIgnoreCase) && new Uri(attachment.ContentUrl).Host.EndsWith("skype.com"))
{
var token = await new MicrosoftAppCredentials().GetTokenAsync();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
var responseMessage = await httpClient.GetAsync(attachment.ContentUrl);
var contentLenghtBytes = responseMessage.Content.Headers.ContentLength;
await context.PostAsync($"Attachment of {attachment.ContentType} type and size of {contentLenghtBytes} bytes received.");
}
}
else
{
await context.PostAsync("Hi there! I'm a bot created to show you how I can receive message attachments, but no attachment was sent to me. Please, try again sending a new message including an attachment.");
}
context.Wait(this.MessageReceivedAsync);
}