-
-
Save DevDuck/db4e7e5903d5d3d2d49a238871da4311 to your computer and use it in GitHub Desktop.
import { Buffer } from "buffer"; | |
var item: Office.MessageRead; | |
Office.onReady(() => { | |
item = Office.context.mailbox.item; | |
}); | |
/** | |
* Gets the email in EML format (IMF text file). This function executes the following steps: | |
* (1) item.getAllInternetHeadersAsync() | |
* (2) extract MIME boundary from headers to separate messages | |
* (3) item.body.getAsync(Office.CoercionType.Html) & base64 encode | |
* (4) item.body.getAsync(Office.CoercionType.Text) & base64 encode | |
* (5) iterate through item.attachments & item.getAttachmentContentAsync() | |
* You can do what you want with the file (string) once complete. | |
* Note that this code does not encode the attachments into the file. | |
* @param options - An object literal that contains one or more of the following properties:- | |
* `asyncContext`: Developers can provide any object they wish to access in the callback function. | |
* @param callback - Optional. When the method completes, the function passed in the `callback` parameter is called with a single parameter, | |
* `asyncResult`, which is an `Office.AsyncResult` object. | |
* On success, the internet headers data is provided in the `asyncResult.value` property as a string. | |
* Refer to {@link https://tools.ietf.org/html/rfc2183 | RFC 2183} for the formatting information of the returned string value. | |
* If the call fails, the `asyncResult.error` property will contain an error code with the reason for the failure. | |
*/ | |
export const getEmailContentAsync = async ( | |
options: Office.AsyncContextOptions, | |
callback?: (asyncResult: Office.AsyncResult<string>) => void | |
) => { | |
await item.getAllInternetHeadersAsync( | |
options, | |
await callbackWrapper(options, callback, async (headerResult) => { | |
let header = headerResult.value; | |
if (!header) { | |
let headers = [`From: ${item.from.displayName} <${item.from.emailAddress}>`]; | |
if (item.to && item.to.length) | |
headers.push(`To: ${item.to.map((e) => `${e.displayName} <${e.emailAddress}>`).join(", ")}`); | |
if (item.cc && item.cc.length) | |
headers.push(`CC: ${item.cc.map((e) => `${e.displayName} <${e.emailAddress}>`).join(", ")}`); | |
headers.push(`Subject: ${item.subject}`); | |
headers.push(`Date: ${item.dateTimeCreated.toUTCString()}`); | |
headers.push(`Message-ID: ${item.internetMessageId}`); | |
header = headers.join("\r\n"); | |
} | |
await item.body.getAsync( | |
Office.CoercionType.Html, | |
callbackWrapper(options, callback, async (htmlResult) => { | |
await item.body.getAsync( | |
Office.CoercionType.Text, | |
callbackWrapper(options, callback, async (result) => { | |
const boundaryMatches = header.match(/boundary="([\w'()+,-./:=?]+)"/); | |
const alternativeMatches = header.match(/Content-Type: multipart\/alternative;/); | |
let boundary: string; | |
if (boundaryMatches) { | |
boundary = boundaryMatches[1]; | |
} else { | |
boundary = `_${item.conversationId.substring(0, 68)}_`; | |
header += `Content-Type: multipart/mixed;\r\n boundary="${boundary}"\r\n`; | |
} | |
let messageBoundary = "_body_" + boundary.slice(6); | |
if (alternativeMatches) { | |
// Can skip multipart/mixed content-type header | |
messageBoundary = boundary; | |
} | |
const body = result.value; | |
const bodyHtml = htmlResult.value; | |
let content = `${header}MIME-Version: 1.0\r\n`; | |
if (!alternativeMatches) { | |
content += `\r\n--${boundary}\r\nContent-Type: multipart/alternative;\r\n boundary="${messageBoundary}"\r\n`; | |
} | |
content += `\r | |
--${messageBoundary}\r | |
Content-Type: text/plain; charset="utf-8"\r | |
Content-Transfer-Encoding: base64\r | |
\r | |
${base64Chunk(body).join("\r\n")}\r | |
\r | |
--${messageBoundary}\r | |
Content-Type: text/html; charset="utf-8"\r | |
Content-Transfer-Encoding: base64\r | |
\r | |
${base64Chunk(bodyHtml).join("\r\n")}\r | |
\r | |
--${messageBoundary}--`; | |
item.attachments.forEach((a) => { | |
content += `\r\n\r\n--${boundary}\r | |
Content-Type: ${a.contentType == "message/rfc822" ? "text/plain" : a.contentType}; name="${a.name}"\r | |
Content-Description: ${a.name}\r | |
Content-Disposition: ${a.isInline ? "inline" : "attachment"}; filename="${a.name}"; size=${a.size}`; | |
// NB: This is where you would inline each attachment. I currently don't have need | |
// to do this since I save attachments separately, but this is where you might do | |
// item.getAttachmentContentAsync(a.id, options, ...); | |
}); | |
if (!alternativeMatches) content += `\r\n\r\n--${boundary}--`; | |
callback({ | |
asyncContext: options?.asyncContext, | |
diagnostics: undefined, | |
error: undefined, | |
status: Office.AsyncResultStatus.Succeeded, | |
value: content, | |
}); | |
}) | |
); | |
}) | |
); | |
}) | |
); | |
}; | |
// Base64 encode string and chunk into 76 char lines | |
const base64Chunk = (str: string): string[] => { | |
let encoded = Buffer.from(str).toString("base64"); | |
let maxLength = 76; | |
let result: string[] = []; | |
while (encoded.length > maxLength) { | |
result.push(encoded.slice(0, maxLength)); | |
encoded = encoded.substring(maxLength); | |
} | |
result.push(encoded); | |
return result; | |
}; | |
// Wrap callback in try/catch block | |
const callbackWrapper = ( | |
options: Office.AsyncContextOptions, | |
callback?: (asyncResult: Office.AsyncResult<string>) => void, | |
processSuccess?: (asyncResult: Office.AsyncResult<string>) => Promise<void> | |
) => { | |
return async (asyncResult: Office.AsyncResult<string>) => { | |
try { | |
if (asyncResult.status != Office.AsyncResultStatus.Succeeded) { | |
if (callback) callback(asyncResult); | |
return; | |
} | |
if (processSuccess) await processSuccess(asyncResult); | |
} catch (err) { | |
if (callback) callback(wrapException(err, options.asyncContext)); | |
} | |
}; | |
}; | |
// Wrap exception in asyncResult with Office error | |
const wrapException = (err, asyncContext?): Office.AsyncResult<any> => { | |
let officeError = { | |
code: 5001, // Internal error | |
name: "getEmailContentAsync", | |
message: undefined, | |
}; | |
let asyncResult = { | |
asyncContext: asyncContext, | |
diagnostics: err, | |
error: officeError, | |
status: Office.AsyncResultStatus.Failed, | |
value: undefined, | |
}; | |
if (err instanceof Error) { | |
officeError.name = err.name; | |
officeError.message = err.message; | |
} | |
return asyncResult; | |
}; |
It looks like Microsoft has created a new method for doing this with the native Javascript API:
https://learn.microsoft.com/en-us/javascript/api/outlook/office.messageread?view=outlook-js-preview&preserve-view=true#outlook-office-messageread-getasfileasync-member(1)
Hi prmoore77
I am new to outlook add in development , i have to save the mail as .email file as you have mentioned here i used the same approach and same code that you have mentioned like this
try {
await getEmailContentAsync(
{},
async (asyncResult: Office.AsyncResult) => {
if (asyncResult.status != Office.AsyncResultStatus.Succeeded) {
// handle error
return;
}
const content = asyncResult.value;
const xx = JSON.stringify(content);
console.log('content is :' + { xx });
// upload file
//Buffer.from(content, 'utf8');
}
);
} catch (err) {
console.log('Error in saving file');
}
What ever content i am geting from this code that i am saving as .eml file but on opening the saved .eml file body part is block can you pls help me on this pls
Regards
Rajesh
i also want to get the outlook email in .eml file and upload on the server i am using getAsFileAsync
here is my code
Office.context.mailbox.item.getAsFileAsync({
},function(asyncResult) {
if (asyncResult.status === Office.AsyncResultStatus.Failed) {
return;
}
console.log(asyncResult.value);
});
it's not working
I wanted to document the way to call the code here (taken from @DevDuck's post to: https://techcommunity.microsoft.com/t5/microsoft-365-developer-platform/allow-msg-file-export-through-js-add-in-api-in-outlook-desktop/idc-p/3925786/highlight/true#M1969):
Thanks again for creating this!