Server Timeout Sending Large Email Attachment

Thanks for the reply Elton.

Since we don’t have access to the database itself, we need to design the table based on Five’s limitations. I don’t see any of the choices you mention except for Text. I have it setup for Binary, and gave it a size that would hold the largest report. Would Text be better or more efficient than Binary? If so, can I just choose that type and give it the large size, so it would be equivalent to MEDIUMTEXT?

I have instituted a caching system for the reports. After the reports are cached, I then remove all records for the logged-in UserKey except for the current BatchKey that I just created, always leaving the latest batch in the cache. Then, during caching, as I’m looping through the code, I see if I can find a cached PDF that has the same metadata such as group, portfolio, etc. If so, I simply use that, and write it back to the table with the new BatchKey instead of taking the time to render the report. This speeds things up dramatically.

As you’ve noted, I could do the rendering in parallel, but to what advantage? I still need to collect and attach the proper PDFs to the email. I don’t see how the code could be re-engineered to do this.

Is there any way to improve speed of report generation and email sending on the server?

Thanks again…

The system is basically working, but the big bottleneck is emailing the reports. With BOARD emails, there are currently 11 emails if I choose to email all reports containing all groups. (Board tables are incomplete, only enough data for development and testing).

Most of the reports are fairly small, but the Member List is ~ 10MB.

With these statistics, the entire time processing is 12-14 minutes, so around one minute for large emails (several attachments), or somewhat quicker for small emails.

It’s not clear to me if the mail-merge process is taking a long time, or simply the SMTP sending is the time hog.

Thinking out of the box: How about using an online file sharing site (Dropbox, Cloudflare, etc), and simply upload the files, receive a URL in return, add that to the email body, send the email.

ChatGPT says this would work very well using Cloudflare, if you can answer this:

Can Five run npm modules or include an AWS S3-compatible SDK, or does it only provide raw HTTPS requests?

That answer would dictate whether I could use Cloudflare, or if I would need to use Dropbox or something similar that will work with standard HTTPS requests.

Advantage of Cloudflare: They will return a URL link, they can auto-delete the files after a certain period.

If you can help me with speeding up Five’s email sending process (when lots of data is attached to the email), that would be amazing. Otherwise, using a file sharing app seems like it would speed things up significantly.

Thanks!!!

Follow-Up question:
Given that the report object coming back contains this base-64 encoded text, can you show me how to convert it into a “real” PDF file, and copy it somewhere?

data:application/pdf;base64,JVBERi0xLjcKJYGBgYEKCjE5IDAgb2JqCj...

ChagGPT seems to think that if I strip out the comma and before, what is left is actual PDF data. Can you provide an example of how to save this as a PDF document?

I have been able to successfully use Microsoft Graph to create a folder on my OneDrive and add a text file to it, then retrieve that file.

the missing piece is, can I send the entire text in the request body, or do I need to convert the above format somehow? is it as simple as sending the entire response from the Five server like the above example, or do I need to trim something off of it, or convert it to binary? I’m pretty close here to being able to use OneDrive to store these reports, and send a link to the email recipients.

can you show me how to decode the base64 text into an actual pdf file, so I can send it to OneDrive?

I’ve already been able to create a text file and send it there. If you have a method that five can use, I could convert it and somehow POST it to OneDrive.

Thanks…

UPDATE:
I was able to copy the report.pdf string generated by Five, remove leading text and convert to a pdf by using this in my mac Terminal:

base64 -D -i report.b64 -o report.pdf

Now I can open the PDF file from finder. so the missing piece now is how to convert the text returned from generating the report into an actual pdf before uploading to OneDrive or whatever cloud storage I use. The conversion must be able to be done using a five functionality or pdf-lib from within JavaScript in Five.

UPDATE:

I was able to build code that converts the base64 string from five into actual PDF bytes. So far so good.
I tried converting to pdf bytes then writing into the staging table but it wouldn’t work. the error said

Could not save report PDF for GroupSchedule: Unsupported argument type : 37,80,68,70,45,49,46,55,10,37,129,129,129,129,10,10,49,57,32,48,32,111,98,106.........
argument position 14

So that column is defined as Binary on my application, and works fine to save the base-64 pdf text that Five returns. But apparently Binary won’t hold the pdf bytes.

Or, perhaps the code. I created a SQL INSERT statement with a bunch of ?'s for the field values used by the insert., then put the proper variables in the execute statement.

I don’t see any other field type in the table field designer that might hold pdfBytes.

Now that I can convert to a string of PDF digits, how can I send this to cloud storage? Can the payload of an http request be binary data? Using Microsoft Graph, I can only send text in the payload.

Please supply info on how Five can do the authentication (OAuth 2.0) or whether Five can run npm modules or include an AWS S3-compatible SDK.

Thanks so much…

Hi Ron,

Sorry for the delay, and thanks for your patience.

I’ve been reviewing the code and exploring a few possible solutions for your scenario.

I think your suggestion is the best approach. Sending a secure download link is generally a more reliable approach when working with multiple large files.

At the moment, the current version doesn’t support installing npm packages, but we’re already working on changes to enable that.

In the meantime, I believe the best option is to use Microsoft Graph to make the POST requests.

The code (library) below includes an example to help with the implementation. It’s not a final solution, but it should serve as a helpful guide as you build this out .

You can also use AI to understand the code:


function UploadPDFToOneDrive(five, context, result, values) {

  five.log("UploadPDFToOneDrive START");

  const EXPIRY_BUFFER_MS = 2 * 60 * 1000;

  // -----------------------------
  // 1) TOKEN CACHE INIT
  // -----------------------------
  if (!five.getVariable('tokenCache')) {
    five.setVariable('tokenCache', JSON.stringify({
      accessToken: null,
      expiresAtMs: 0
    }));
  }

  let tokenCache =
    JSON.parse(five.getVariable('tokenCache')) || {
      accessToken: null,
      expiresAtMs: 0
    };

  // -----------------------------
  // 2) GET VALID TOKEN
  // -----------------------------
  function getValidAccessToken() {

    const now = Date.now();

    const expired =
      !tokenCache.accessToken ||
      now >= tokenCache.expiresAtMs - EXPIRY_BUFFER_MS;

    if (!expired) {
      return tokenCache.accessToken;
    }

    // ---- request new token ----
    const client = five.httpClient();

    const tokenUrl =
      "https://login.microsoftonline.com/" +
      values.credentials.TENANT_ID +
      "/oauth2/v2.0/token";

    function encodeForm(data) {
      let pairs = [];
      for (let key in data) {
        pairs.push(
          encodeURIComponent(key) + "=" + encodeURIComponent(data[key])
        );
      }
      return pairs.join("&");
    }

    client.setContentType("application/x-www-form-urlencoded");

    client.setContent(encodeForm({
      client_id: values.credentials.CLIENT_ID,
      client_secret: values.credentials.CLIENT_SECRET,
      scope: "https://graph.microsoft.com/.default",
      grant_type: "client_credentials"
    }));

    const res = client.post(tokenUrl);

    if (!res || res.isOk() === false) {
      throw new Error("Token request failed: " + JSON.stringify(res));
    }

    const data =
      typeof res.response === "string"
        ? JSON.parse(res.response)
        : res.response;

    tokenCache.accessToken = data.access_token;
    tokenCache.expiresAtMs = Date.now() + (data.expires_in * 1000);

    five.setVariable('tokenCache', JSON.stringify(tokenCache));

    return tokenCache.accessToken;
  }

  // -----------------------------
  // 3) INPUT VALIDATION
  // -----------------------------
  const token = getValidAccessToken();

  const userUpn =
    values.credentials.DEFAULT_USER_UPN ||
    context?.variables?.userUpn;

  if (!userUpn) {
    throw new Error("Missing userUpn");
  }

  if (!values.pdfBase64) {
    throw new Error("Missing pdfBase64");
  }

  const fileName = "report-" + Date.now() + ".pdf";

  // Base64 → binary
  function base64ToBinary(base64) {
    return atob(base64);
  }

  const pdfBinary = base64ToBinary(values.pdfBase64);

  // -----------------------------
  // 4) UPLOAD TO ONEDRIVE
  // -----------------------------
  const uploadClient = five.httpClient();
  uploadClient.addHeader("Authorization", "Bearer " + token);
  uploadClient.setContentType("application/pdf");
  uploadClient.setContent(pdfBinary);

  const uploadUrl =
    "https://graph.microsoft.com/v1.0/users/" +
    encodeURIComponent(userUpn) +
    "/drive/root:/" +
    fileName +
    ":/content";

  const uploadRes = uploadClient.put(uploadUrl);

  let uploadData = uploadRes.response;

  if (typeof uploadData === "string") {
    uploadData = JSON.parse(uploadData);
  }

  if (!uploadData || !uploadData.id) {
    throw new Error("Upload failed: " + JSON.stringify(uploadData));
  }

  const fileId = uploadData.id;

  // -----------------------------
  // 5) CREATE SHARE LINK
  // -----------------------------
  const linkClient = five.httpClient();
  linkClient.addHeader("Authorization", "Bearer " + token);
  linkClient.setContentType("application/json");

  linkClient.setContent(JSON.stringify({
    type: "view",
    scope: "anonymous"
  }));

  const linkUrl =
    "https://graph.microsoft.com/v1.0/users/" +
    encodeURIComponent(userUpn) +
    "/drive/items/" +
    fileId +
    "/createLink";

  const linkRes = linkClient.post(linkUrl);

  let linkData = linkRes.response;

  if (typeof linkData === "string") {
    linkData = JSON.parse(linkData);
  }

  if (!linkData || !linkData.link || !linkData.link.webUrl) {
    throw new Error("Failed to create link: " + JSON.stringify(linkData));
  }

  const downloadUrl = linkData.link.webUrl;

  five.log("Download URL: " + downloadUrl);

  // -----------------------------
  // 6) RETURN RESULT
  // -----------------------------
  result = five.success(result);
  result.Results = JSON.stringify({
    ok: true,
    fileName: fileName,
    downloadUrl: downloadUrl
  });

  result.Code = five.ErrErrorOk;
  result.Message = "";

  return result;
}

Additionally, this is another code (library) sample used to get data from a Microsoft email, in case you need a further example:

// If Five keeps library globals warm, move this OUTSIDE the function
// let tokenCache = { accessToken: null, expiresAtMs: 0 };
const EXPIRY_BUFFER_MS = 2 * 60 * 1000; // refresh 2 min early
// Token lifetime from Microsoft = 60 minutes
// Your refresh buffer = 2 minutes before expiry
// So your refresh happens around minute 58

function MicrosoftAutenticationGetData(five, context, result, values) {
  five.log("MicrosoftAutenticationGetData START");

  //Create a five variable only if it does not exist, so the token can be global.
  if (!five.getVariable('tokenCache')){
      five.setVariable('tokenCache', {accessToken: null, expiresAtMs: 0});
  }

  // Read token cache
  let tokenCache = JSON.parse(five.getVariable('tokenCache')) || {
    accessToken: null,
    expiresAtMs: 0
  };


  // -----------------------------
  // Form encoder (x-www-form-urlencoded)
  // -----------------------------
  function encodeForm(data) {
    let pairs = [];
    for (let key in data) {
      pairs.push(
        encodeURIComponent(key) + "=" + encodeURIComponent(data[key])
      );
    }
    return pairs.join("&");
  }

  // -----------------------------
  // 1) Fetch a new access token
  // -----------------------------
  function fetchNewAccessToken(values) {
    const client = five.httpClient();

    const tokenUrl =
      "https://login.microsoftonline.com/" +
      values.credentials.TENANT_ID +
      "/oauth2/v2.0/token";

    client.setContentType("application/x-www-form-urlencoded");

    const body = encodeForm({
      client_id: values.credentials.CLIENT_ID,
      client_secret: values.credentials.CLIENT_SECRET,
      scope: "https://graph.microsoft.com/.default",
      grant_type: "client_credentials",
    });

    client.setContent(body);

    const res = client.post(tokenUrl);

    if (!res || res.isOk() === false) {
      throw new Error("Token request failed: " + JSON.stringify(res));
    }

    const data = res.response; // Five already parses JSON responses

    tokenCache.accessToken = data.access_token;
    tokenCache.expiresAtMs = Date.now() + data.expires_in * 1000;

    five.setVariable('tokenCache', tokenCache);

    return tokenCache.accessToken;
  }

  // -----------------------------
  // 2) Return valid token (refresh if expired)
  // -----------------------------
  function getValidAccessToken(values) {
    const now = Date.now();

    const expiredOrNearExpired =
      !tokenCache.accessToken ||
      now >= tokenCache.expiresAtMs - EXPIRY_BUFFER_MS;

    if (expiredOrNearExpired) {
      five.log("Token expired → refreshing");
      return fetchNewAccessToken(values);
    }

    return tokenCache.accessToken;
  }


//Helper to build the query string
function buildMessageQuery(filterOptions = {}) {
  const params = [];
  const filters = [];

  // ---- normalize inputs ----
  const skip = filterOptions.skip;
  const top = filterOptions.top;
  const startDate = filterOptions.startDate;
  const endDate = filterOptions.endDate || filterOptions.endDatae; 
  const orderByDesc = filterOptions.orderbyDescReceivedDateTime === true;  
  const orderByASC = filterOptions.orderbyAscReceivedDateTime === false; 
  
 

  // ---- $top ----
  // default to 10 if not set or blank
  const topNum = top !== undefined && top !== null && String(top).trim() !== ""
    ? Number(top)
    : 10;
  params.push(`$top=${topNum}`);

  // ---- $skip ----
  if (skip !== undefined && skip !== null && String(skip).trim() !== "") {
    params.push(`$skip=${Number(skip)}`);
  }

  // ---- date filters ----
  // Expect ISO strings like "2025-11-01T00:00:00Z"
  if (startDate && String(startDate).trim() !== "") {
    filters.push(`receivedDateTime ge ${startDate}`);
  }

  if (endDate && String(endDate).trim() !== "") {
    filters.push(`receivedDateTime le ${endDate}`);
  }

  if (filters.length > 0) {
    params.push(`$filter=${filters.join(" and ")}`);
  }
  // only apply the order by if one of teh field are false/ not true (we cannot roder asc and desc at teh same time)
  if (orderByDesc === false || orderByASC === false) {
      // ---- ORDER BY to get the lastest email first (only if boolean true) ----
      if (orderByDesc === true) {
        params.push(`$orderby=receivedDateTime desc`);
      }

      // ---- ORDER BY to get the oldest email first (only if boolean true) ----
      if (orderByASC === true) {
        params.push(`$orderby=receivedDateTime asc`);
      }
  }


  return params.join("&");
}

 //Get folder ID
  function getFolderId(five, token, userUpn, folderName) {
    const client = five.httpClient();
    client.addHeader("Authorization", "Bearer " + token);
    client.setContentType("application/json");

    let url =
        "https://graph.microsoft.com/v1.0/users/" +
        encodeURIComponent(userUpn) +
        "/mailFolders?$filter=displayName eq '" +
        folderName +
        "'";

    url = url.replace(/ /g, "%20");
    const res = client.get(url);

    if (!res || res.isOk() === false) {
        throw new Error("Failed to get folder list: " + JSON.stringify(res));
    }

    const data = res.response;
    if (!data.value || data.value.length === 0) {
        throw new Error("Folder not found: " + folderName);
    }

    return data.value[0].id;  // ← folderId
  }





  // -----------------------------
  // 3) Get inbox emails
  // -----------------------------

function getInboxEmails(values, userUpn) { //} top, skip) {
  const token = getValidAccessToken(values);
  five.log('-------token---------')
  five.log(token)

  const client = five.httpClient();
  client.addHeader("Authorization", "Bearer " + token);
  client.setContentType("application/json");

 const options = values.filterOptions;
 const queryString = buildMessageQuery(options);

   // Decide which folder to use
  // Priority: folderId > folderName > Inbox
  let folderID;

  if (values.filterOptions.folderName && String(values.filterOptions.folderName).trim() !== "") {
    // use the display name, e.g. "Inbox", "Archive", "Invoices 2025"
    folderID = "mailFolders/" + getFolderId(five, token, userUpn, values.filterOptions.folderName);
  } else {
    // default to Inbox
    folderID = "mailFolders/Inbox";
  }

  // let url =
  //   "https://graph.microsoft.com/v1.0/users/" +
  //   encodeURIComponent(userUpn) +
  //   "/mailFolders/Inbox/messages" +
  //   (queryString ? "?" + queryString : "") +
  //   "&$select=subject,from,receivedDateTime,bodyPreview,body";

  let url =
  "https://graph.microsoft.com/v1.0/users/" +
  encodeURIComponent(userUpn) +
  // "/mailFolders/Inbox/messages";
  "/" +
  folderID +
  "/messages";

  if (queryString && queryString.trim() !== "") {
    url += "?" + queryString + "&$select=subject,from,receivedDateTime,bodyPreview,body";
  } else {
    url += "?$select=subject,from,receivedDateTime,bodyPreview,body";
  }



  url = url.replace(/ /g, "%20");

    five.log('--------url-------')
  five.log(url)

  const res = client.get(url);


  if (!res || res.isOk() === false) {
    throw new Error("Graph request failed: " + JSON.stringify(res));
  }

  // Five sometimes stores JSON here:
  let data = res.response;

  // If response is a string, parse it
  if (typeof data === "string") {
    try {
      data = JSON.parse(data);
    } catch (e) {
      throw new Error("Could not parse Graph JSON: " + data);
    }
  }

  // If still nothing, try body/content fallbacks (Five env variance)
  if (!data && res.body) {
    data = JSON.parse(res.body);
  }
  if (!data && res.content) {
    data = JSON.parse(res.content);
  }

  // Finally, pull messages array safely
  const emails = data?.value || data?.response?.value || [];

  return emails;
}


  // -----------------------------
  // 4) Main runner
  // -----------------------------
  try {
    const vars = context?.variables || {};

    const userUpn = vars.userUpn || values.credentials.DEFAULT_USER_UPN;
    // const top = Number(vars.top || 10); //number of email you want to return

    if (!userUpn) throw new Error("Missing userUpn.");

    const emails = getInboxEmails(values, userUpn);

    const payload = {
      ok: true,
      count: Array.isArray(emails) ? emails.length : 0, // number of email returned
      emails: Array.isArray(emails) ? emails : [] // emails returned
    };

    // match frontend expectation:
    // JSON.parse(res.serverResponse.results)
    // result.serverResponse = {
    //   results: JSON.stringify(payload),
    // };

    five.log('---resultOK---')
    five.log(JSON.stringify(payload))
    result = five.success(result);
    result.Results = JSON.stringify(payload);
    result.Code = five.ErrErrorOk;
    result.Message = "";
    return result;

  } catch (err) {
    const payload = {
      ok: false,
      error: err.message,
    };

    // result.serverResponse = {
    //   results: JSON.stringify(payload),
    // };

    five.log('---resultNOK---')
    five.log(JSON.stringify(payload))
    result = five.success(result);
    result.Results = JSON.stringify(payload);    
    result.Code = five.ErrErrorOk;
    result.Message = "";
    return result;
  }
}

And this is how I call this library via the server:

    five.log('------MicrosoftEmailServer-----')
    const values = {
        credentials : {
          TENANT_ID : 'xxxxxxxxxxxxxxxxxxxxx',
          CLIENT_ID : 'xxxxxxxxxxxxxxxxxxxxxxxxx',
          CLIENT_SECRET : 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
          DEFAULT_USER_UPN : 'xxxxxxx@xxxxx.com' //Default mailbox UPN (email-like identity)
          // The default mailbox email address your backend will read from when the client does NOT explicitly provide a mailbox
        },
        filterOptions : {
          skip: '',      //Skip number of records. 
          top: '20',     //The number of email returns if not informed, it will be the default 10 definied in teh library.  
          startDate: '', //Start date in the format 2025-11-01T00:00:00Z
          endDate: '',   //Start date in the format 2025-11-18T00:00:00Z
          orderbyDescReceivedDateTime: true, //passing true or false to get the latest email first
          //Microsoft Graph has no guaranteed default sort order for messages unless you explicitly
          orderbyAscReceivedDateTime: false, //passing true or false to get the oldest email first
          folderName: '', // default will be inbox if not provided.

        },
    }

  // 
    const res = MicrosoftAutenticationGetData(five, context, result, values);
    five.log('------MicrosoftAutenticationGetData retun from backend-----')

    // in case you want to use in the result in the backend:
    const resdata = JSON.parse(res.Results)
    
    if (resdata?.ok){
      five.log('ok ' + resdata.ok )
      five.log('count ' + resdata.count )
      five.log('emails ' + resdata.ok )
      five.log(JSON.stringify(resdata.emails))
    }

Thanks so much for replying!

Not quite sure I understand everything.

1: No NPM packages yet, right?

2: Can use OneDrive using your code or some version thereof, right?

3: Is what you are describing in your code the same as OAuth validation?

4: Is it possible that I could use any service that accepts OAuth validation?
I ask this last one because ChatGPT thinks I shouldn’t use OneDrive except for proof of concept, as it is quite complex if not using via Graph (which handles all the authentication).

I ask this because there are other services out there that may use OAuth validation, or I could obtain a key and only use that in the request URL.

If possible, can you give me clarification on how five is processing the report data?

It seems like when Five renders the report on the server, it returns a base64 string which represents the PDF itself.

I’ve found code which decodes the base64 into a uint8 array, which is the PDF bytes themselves. In fact, we can decode the first five bytes of that and it shows “%PDF-”

Can you verify if this data can be sent in the request body to the cloud file store, and it will be recognized as a PDF file? I know Graph only accepts actual text for the body, but will an HTTP request accept this binary data? Or do I need to save it to a local file (or server file) , then strasfer that?

Sorry for all the questions, but it is difficult to understand what is happening behind the scenes with Five. The documentation is a bit sparse, and ChatGPT doesn’t know much about Five itself.

I hope you can help me figure this out. Not sure how much of the answer is Five-related or how much is JavaScript related.

Thanks again…

Hi Ron,

Based on my research, I have compiled the answer. Be aware that I have not implemented a similar solution to this before, but this approach is a good suggestion for a future improvement. You can also use ChatGPT to support you on this solution.

1 - Correct, not NPM functionality at the moment, unless you create a custom action in React.

2 - I believe Yes, OneDrive can be used with this approach.
The file is uploaded to OneDrive using Microsoft Graph, then Graph creates a share/download link. The current code in the document follows that pattern.

3 - Yes, this is OAuth validation/authentication.
More specifically, it uses the OAuth 2.0 client credentials flow, where the app gets an access token using client_id, client_secret, and tenant_id, then uses that token to call Microsoft Graph. OAuth 2.0 client credentials flow on the Microsoft identity platform - Microsoft identity platform | Microsoft Learn

4 - In theory, yes, any cloud service with OAuth + file upload API could be used.
But for proof of concept, OneDrive via Microsoft Graph is the best option because Graph handles the Microsoft authentication model and provides simple endpoints for upload and link creation.

This is the workflow:

1. Get OAuth token
POST Sign in to your account

Body:
client_id=…
client_secret=…
scope=https://graph.microsoft.com/.default
grant_type=client_credentials

Then use the returned token:

Authorization: Bearer ACCESS_TOKEN

Upload file:

PUT https://graph.microsoft.com/v1.0/users/{USER_UPN}/drive/root:/report.pdf:/content

Then create link:

POST https://graph.microsoft.com/v1.0/users/{USER_UPN}/drive/items/{FILE_ID}/createLink

Body:

{
  "type": "view",
  "scope": "anonymous"
}

If anonymous sharing is blocked by the Microsoft tenant, use:

{
  "type": "view",
  "scope": "organization"
}

In Five, you would normally store these as credentials/variables:

TENANT_ID
CLIENT_ID
CLIENT_SECRET
DEFAULT_USER_UPN

Then your Five action/process can return:

result.Results = JSON.stringify({
  ok: true,
  fileName: fileName,
  downloadUrl: downloadUrl
});

So the user can click the returned downloadUrl.

The main implementation point is: Five does not need NPM packages. It only needs five.httpClient() to call the OAuth token endpoint and Microsoft Graph endpoints.

You do not need to save it to a local/server file first. The decoded PDF bytes can be sent directly in the HTTP request body to Microsoft Graph.

Graph’s upload endpoint expects the request body to be the binary stream of the file, not a file path. So the flow should be:

base64 PDF string → decode to binary bytes → HTTP PUT body → Graph/OneDrive

The important part is that we must send the decoded binary data, not the original base64 text. The request should also use a PDF filename such as report.pdf and content type application/pdf.

For small files, Microsoft Graph supports uploading the file content directly with PUT /drive/root:/filename:/content, up to 250 MB. For larger files, we would need an upload session/chunked upload Upload small files - Microsoft Graph v1.0 | Microsoft Learn

This is a sample code for a starting point. You can refer to the AI to help you understand it:

function UploadBase64PDFToOneDrive(five, context, result, values) {
  five.log("UploadBase64PDFToOneDrive START");

  const EXPIRY_BUFFER_MS = 2 * 60 * 1000;

  const TENANT_ID = values.credentials.TENANT_ID;
  const CLIENT_ID = values.credentials.CLIENT_ID;
  const CLIENT_SECRET = values.credentials.CLIENT_SECRET;
  const USER_UPN = values.credentials.DEFAULT_USER_UPN;

  if (!TENANT_ID || !CLIENT_ID || !CLIENT_SECRET || !USER_UPN) {
    throw new Error("Missing Microsoft Graph credentials.");
  }

  if (!values.pdfBase64) {
    throw new Error("Missing pdfBase64.");
  }

  // Remove data URL prefix if present:
  // data:application/pdf;base64,JVBERi0x...
  let pdfBase64 = values.pdfBase64;
  if (pdfBase64.indexOf(",") >= 0) {
    pdfBase64 = pdfBase64.split(",")[1];
  }

  pdfBase64 = pdfBase64.replace(/\s/g, "");

  // -----------------------------
  // 1) Get cached or new token
  // -----------------------------
  function getValidAccessToken() {
    let tokenCacheRaw = five.getVariable("graphTokenCache");

    let tokenCache = tokenCacheRaw
      ? JSON.parse(tokenCacheRaw)
      : {
          accessToken: null,
          expiresAtMs: 0
        };

    const now = Date.now();

    if (
      tokenCache.accessToken &&
      now < tokenCache.expiresAtMs - EXPIRY_BUFFER_MS
    ) {
      return tokenCache.accessToken;
    }

    const tokenClient = five.httpClient();

    const tokenUrl =
      "https://login.microsoftonline.com/" +
      encodeURIComponent(TENANT_ID) +
      "/oauth2/v2.0/token";

    function encodeForm(data) {
      let pairs = [];

      for (let key in data) {
        pairs.push(
          encodeURIComponent(key) + "=" + encodeURIComponent(data[key])
        );
      }

      return pairs.join("&");
    }

    tokenClient.setContentType("application/x-www-form-urlencoded");

    tokenClient.setContent(
      encodeForm({
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET,
        scope: "https://graph.microsoft.com/.default",
        grant_type: "client_credentials"
      })
    );

    const tokenRes = tokenClient.post(tokenUrl);

    if (!tokenRes || tokenRes.isOk() === false) {
      throw new Error("Token request failed: " + JSON.stringify(tokenRes));
    }

    let tokenData = tokenRes.response;

    if (typeof tokenData === "string") {
      tokenData = JSON.parse(tokenData);
    }

    if (!tokenData.access_token) {
      throw new Error("No access token returned: " + JSON.stringify(tokenData));
    }

    tokenCache = {
      accessToken: tokenData.access_token,
      expiresAtMs: Date.now() + tokenData.expires_in * 1000
    };

    five.setVariable("graphTokenCache", JSON.stringify(tokenCache));

    return tokenData.access_token;
  }

  const accessToken = getValidAccessToken();

  // -----------------------------
  // 2) Decode base64 PDF to binary
  // -----------------------------
  function base64ToBinary(base64) {
    if (typeof atob === "function") {
      return atob(base64);
    }

    throw new Error(
      "Base64 decoding failed. This Five runtime does not support atob()."
    );
  }

  const pdfBinary = base64ToBinary(pdfBase64);

  // Quick PDF validation
  if (pdfBinary.substring(0, 5) !== "%PDF-") {
    throw new Error(
      "Decoded file does not look like a PDF. First bytes: " +
        pdfBinary.substring(0, 10)
    );
  }

  // -----------------------------
  // 3) Upload binary PDF to OneDrive
  // -----------------------------
  const fileName = "report-" + Date.now() + ".pdf";

  const uploadUrl =
    "https://graph.microsoft.com/v1.0/users/" +
    encodeURIComponent(USER_UPN) +
    "/drive/root:/" +
    encodeURIComponent(fileName) +
    ":/content";

  const uploadClient = five.httpClient();

  uploadClient.addHeader("Authorization", "Bearer " + accessToken);
  uploadClient.setContentType("application/pdf");

  // Important:
  // This is the decoded PDF binary, not the original base64 string.
  uploadClient.setContent(pdfBinary);

  const uploadRes = uploadClient.put(uploadUrl);

  if (!uploadRes || uploadRes.isOk() === false) {
    throw new Error("PDF upload failed: " + JSON.stringify(uploadRes));
  }

  let uploadData = uploadRes.response;

  if (typeof uploadData === "string") {
    uploadData = JSON.parse(uploadData);
  }

  if (!uploadData || !uploadData.id) {
    throw new Error("Upload did not return file id: " + JSON.stringify(uploadData));
  }

  const fileId = uploadData.id;

  // -----------------------------
  // 4) Create sharing link
  // -----------------------------
  const linkUrl =
    "https://graph.microsoft.com/v1.0/users/" +
    encodeURIComponent(USER_UPN) +
    "/drive/items/" +
    encodeURIComponent(fileId) +
    "/createLink";

  const linkClient = five.httpClient();

  linkClient.addHeader("Authorization", "Bearer " + accessToken);
  linkClient.setContentType("application/json");

  linkClient.setContent(
    JSON.stringify({
      type: "view",
      scope: "anonymous"
    })
  );

  let linkRes = linkClient.post(linkUrl);

  let linkData = linkRes ? linkRes.response : null;

  if (typeof linkData === "string") {
    linkData = JSON.parse(linkData);
  }

  // If anonymous links are blocked, try organisation link
  if (!linkData || !linkData.link || !linkData.link.webUrl) {
    five.log("Anonymous link failed. Trying organization link.");

    const orgLinkClient = five.httpClient();

    orgLinkClient.addHeader("Authorization", "Bearer " + accessToken);
    orgLinkClient.setContentType("application/json");

    orgLinkClient.setContent(
      JSON.stringify({
        type: "view",
        scope: "organization"
      })
    );

    linkRes = orgLinkClient.post(linkUrl);

    if (!linkRes || linkRes.isOk() === false) {
      throw new Error("Create link failed: " + JSON.stringify(linkRes));
    }

    linkData = linkRes.response;

    if (typeof linkData === "string") {
      linkData = JSON.parse(linkData);
    }
  }

  if (!linkData || !linkData.link || !linkData.link.webUrl) {
    throw new Error("No share link returned: " + JSON.stringify(linkData));
  }

  const downloadUrl = linkData.link.webUrl;

  five.log("PDF uploaded successfully: " + fileName);
  five.log("Download URL: " + downloadUrl);

  // -----------------------------
  // 5) Return result to Five
  // -----------------------------
  result = five.success(result);

  result.Results = JSON.stringify({
    ok: true,
    fileName: fileName,
    fileId: fileId,
    downloadUrl: downloadUrl
  });

  result.Code = five.ErrErrorOk;
  result.Message = "";

  return result;
}

Regards,
Elton S

I don’t want to be tied to Microsoft if we don’t have to. I found another solution called FilePost. They can supply me with an API key, and I only need to include it in the request, so no continuous validation or credential each request.

It is VERY cheap, with a free tier and a $9 per month paid tier. I would really like to make that work.

Unfortunately, they want a FormData object in the request body. Here is some AI-generated code, based on the examples from their site:

const form = new FormData();

form.append(
    "file",
    pdfFileOrBlob,
    "GroupSchedule.pdf"
);

const response = await fetch(
    "https://filepost.dev/v1/upload",
    {
        method: "POST",
        headers: {
            "X-API-Key": "YOUR_API_KEY"
        },
        body: form
    }
);

const result = await response.json();

console.log(result);

Doing some testing in Five, I tried the following:

function SendToFilePost(five, attachments) {

    five.log("typeof fetch = " + typeof fetch);
    five.log("typeof FormData = " + typeof FormData);
    five.log("typeof Blob = " + typeof Blob);
    five.log("typeof File = " + typeof File);

    if (!attachments || attachments.length === 0) {
        five.log("No attachments found");
        return;
    }

    const a = attachments[0];
    const base64 = a.Pdf.replace("data:application/pdf;base64,", "");
    const pdfBytes = Base64ToUint8Array(base64);

    five.log("Decoded in GenerateEmailsServer");
    five.log("PdfBytes constructor = " + pdfBytes.constructor.name);
    five.log("PdfBytes length = " + pdfBytes.length);
    five.log("AttachmentName = " + a.AttachmentName);

    five.log("PdfBytes constructor = " + pdfBytes.constructor.name);
    five.log("PdfBytes length = " + pdfBytes.length);
    five.log("PdfBytes header = " + BytesToHeader(pdfBytes));

    try {

        const form = new FormData();
        five.log("FormData created successfully");

        const blob = new Blob([pdfBytes], { type: "application/pdf" });
        five.log("Blob created successfully");
        five.log("blob.size = " + blob.size);
        five.log("blob.type = " + blob.type);

        form.append("file", blob, "Test.pdf");
        five.log("form.append succeeded");

    } catch (e) {

        five.log("ERROR: " + e.message);

    }
}

So far, this was just to test for the presence of object types. All of the logs generated from this say “undefined” for these types, and your documentation only mentions text for request body.

Is it possible that you can support FormData somehow, even if it is not mentioned in the documentation?

I am concurrently inquiring of FilePost whether they can accept a base64 text string in the request body instead of FormData.

My collaboration with ChatGPT has already accomplished a lot. I’ve been able to successfully transform your base64 text string into a Uint8Array, which is the exact PDF data needed. If I manually save this to a file using Terminal, I can open the PDF file properly. But I really need a dependable way of sending the file to them.

Thanks…

Hi Ron,

You can try to create a multipart/form-data HTTP body with boundaries, headers, and the file bytes inside the request body.

So we may still be able to support FilePost by manually building the multipart/form-data request body and sending it through five.httpClient().

Additionally, Five does support setting the content type, so we can manually build a multipart/form-data request without FormData. The only part we still need to verify is whether Five’s setContent() sends the decoded PDF binary exactly as-is. If it does, FilePost should work.

This is an example function from my research to test whether the file is uploaded correctly.

function SendPDFToFilePost(five, context, result, values) {
  five.log("SendPDFToFilePost START");

  try {
    const apiKey = values.filePostApiKey || values.credentials?.FILEPOST_API_KEY;

    if (!apiKey) {
      throw new Error("Missing FilePost API key.");
    }

    if (!values.pdfBase64) {
      throw new Error("Missing pdfBase64.");
    }

    let fileName = values.fileName || "GroupSchedule.pdf";

    let base64 = values.pdfBase64;

    // Remove data URL prefix if present
    base64 = base64.replace("data:application/pdf;base64,", "");
    base64 = base64.replace(/\s/g, "");

    // Decode base64 to binary string
    const pdfBinary = Base64ToBinaryString(base64);

    five.log("PDF binary length = " + pdfBinary.length);
    five.log("PDF header = " + pdfBinary.substring(0, 5));

    if (pdfBinary.substring(0, 5) !== "%PDF-") {
      throw new Error("Decoded data does not look like a PDF.");
    }

    // -----------------------------
    // Build multipart/form-data body manually
    // -----------------------------
    const boundary = "----FiveBoundary" + Date.now();
    const CRLF = "\r\n";

    const multipartBody =
      "--" + boundary + CRLF +
      'Content-Disposition: form-data; name="file"; filename="' + fileName + '"' + CRLF +
      "Content-Type: application/pdf" + CRLF +
      CRLF +
      pdfBinary + CRLF +
      "--" + boundary + "--" + CRLF;

    five.log("Multipart body length = " + multipartBody.length);

    // -----------------------------
    // Send to FilePost
    // -----------------------------
    const client = five.httpClient();

    client.addHeader("X-API-Key", apiKey);
    client.setContentType("multipart/form-data; boundary=" + boundary);
    client.setContent(multipartBody);

    const uploadUrl = "https://filepost.dev/v1/upload";

    const res = client.post(uploadUrl);

    five.log("FilePost raw response = " + JSON.stringify(res));

    if (!res || res.isOk() === false) {
      throw new Error("FilePost upload failed: " + JSON.stringify(res));
    }

    let data = res.response;

    if (typeof data === "string") {
      data = JSON.parse(data);
    }

    const payload = {
      ok: true,
      fileName: fileName,
      response: data
    };

    result = five.success(result);
    result.Results = JSON.stringify(payload);
    result.Code = five.ErrErrorOk;
    result.Message = "";

    return result;

  } catch (err) {
    const payload = {
      ok: false,
      error: err.message
    };

    five.log("SendPDFToFilePost ERROR = " + JSON.stringify(payload));

    result = five.success(result);
    result.Results = JSON.stringify(payload);
    result.Code = five.ErrErrorOk;
    result.Message = "";

    return result;
  }
}


// -----------------------------
// Helper: base64 to binary string
// -----------------------------
function Base64ToBinaryString(base64) {
  if (typeof atob === "function") {
    return atob(base64);
  }

  throw new Error("This Five runtime does not support atob().");
}

Use values like this:

values = {
  filePostApiKey: "YOUR_API_KEY",
  fileName: "GroupSchedule.pdf",
  pdfBase64: "data:application/pdf;base64,JVBERi0x..."
};

The test should be: if FilePost accepts it and the downloaded PDF opens correctly, then Five is preserving the binary body. **Please let me know how it goes.

Regards,**

Elton S

Thanks so much for the quick response.

ChatGPT says this will fail:

 // -----------------------------
  // 2) Decode base64 PDF to binary
  // -----------------------------
  function base64ToBinary(base64) {
    if (typeof atob === "function") {
      return atob(base64);
    }

    throw new Error(
      "Base64 decoding failed. This Five runtime does not support atob()."
    );
  }

because we already tried to use atob and it came up undefined. they suggested:

function Uint8ArrayToBinaryString(bytes) {
    let s = "";
    const chunkSize = 8192;

    for (let i = 0; i < bytes.length; i += chunkSize) {
        const chunk = bytes.slice(i, i + chunkSize);
        s += String.fromCharCode.apply(null, chunk);
    }

    return s;
}

function Base64ToBinaryString(base64) {
    const bytes = Base64ToUint8Array(base64);
    return Uint8ArrayToBinaryString(bytes);
}

What do you think about that?

We can stop pursuing this issue for now, as FilePost has notified me that they modified their API so we can send the base64 text in the HTTP request.

Thanks so much for your efforts!!!

Thank you for the update, Ron,

Let me know please, what this goes.

Regards,

Elton S