Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PDF password removal #355

Open
MihaiSprinceana opened this issue May 14, 2024 · 9 comments · May be fixed by #381
Open

PDF password removal #355

MihaiSprinceana opened this issue May 14, 2024 · 9 comments · May be fixed by #381
Labels
feature A feature suggestion

Comments

@MihaiSprinceana
Copy link

I'm using the below code to remove a password from a PDF file, but the integration with convertapi can do more.
Prerequisite: secret key from convertapi.

/**
 * If the file is password protected than remove the password
 */
function createFileInsideFolderUsingRuleSettings(rule, attachment, folder) {
  if(rule.password) {
      var obj = {
        secretkey: "testKey",  // Your secret key.
        pass: rule.password, // Password of the protected PDF file.
        blob: attachment.getAs(MimeType.PDF), // pdf blob;
      }
      var blob = decrypting(obj);
      return folder.createFile(blob);
  } else {
      return folder.createFile(attachment);
  }
}

/**
 * Decrypts the PDF file
 */
function decrypting(obj) {
  var url = "https://v2.convertapi.com/convert/pdf/to/decrypt?Secret=" + obj.secretkey;
  var options = {
    method: "post",
    payload: {File: obj.blob, Password: obj.pass},
  }
  var res = UrlFetchApp.fetch(url, options);
  res = JSON.parse(res.getContentText());
  var blob = res.Files.map(function(e) {return Utilities.newBlob(Utilities.base64Decode(e.FileData), MimeType.PDF, e.FileName)});
  return blob[0];
}

Note: is based on the Gmail2Drive project and it will require an account on convertapi website.

@MihaiSprinceana MihaiSprinceana added the feature A feature suggestion label May 14, 2024
@ahochsteger
Copy link
Owner

@MihaiSprinceana that's an interesting use case which should be possible with the next feature release.
I'm currently working on introducing custom actions, which will allow to trigger any automation one can imagine.

@ahochsteger
Copy link
Owner

@MihaiSprinceana I've just released version 2.11.0 which adds support for custom actions.
Have a look at this example to see how it can be used.
It would be great to get some feedback, if you are able to migrate to Gmail Processor with this new feature.

@MikeDabrowski
Copy link

MikeDabrowski commented Jul 12, 2024

I was forced to work with encrypted pdfs as well. I managed to use pdf-lib in apps script to decrypt such pdfs and was loking for a way to 'trigger' it directly from gmail-processor. Custom actions sound promising.

For reference, to use pdf-lib you need to take the @cantoo/pdf-lib fork because it allows decryption, get the minified file and copy paste it as another .gs file. Then remove exports at the end of it.

Then

const file = DriveApp.getFileById(...);
  const fileName = file.getName();
  const newName = getNewName(fileName);

  const fileBase64 = Utilities.base64Encode(file.getBlob().getBytes());


  const pdfDoc = await PDFDocument.load(fileBase64, { password: getPassword(fileName), ignoreEncryption: true});
  const unencrypted = await pdfDoc.save();

  const bb = Utilities.newBlob(unencrypted, 'application/pdf', newName);
  DriveApp.createFile(bb);

@ahochsteger
Would be great to get some docs on what ctx has and how to get the ref to the attachment being processed.

@ahochsteger
Copy link
Owner

@MikeDabrowski you can have a look at the types AttachmentInfo and AttachmentContext in Context.ts that define the context for attachment processing.
Use ctx.attachment.object to get the currently processed attachment that is of type GmailAttachment.
Example:

(ctx, args) =>{
  ...
  const fileBase64 = Utilities.base64Encode(ctx.attachment.object.getBytes());
  ...
}

If you need more help or have a working example let me know, maybe we can even integrate it into the core functionality.

@MikeDabrowski
Copy link

Great! I did not found a location where this file is stored. Am I just blind and it is in fact provided in the ctx somewhere?

If it is not provided, I can use the location string from the config - is there any util that would transform it to folder?

In the end I need a folder object to be able to call folder.createFile

@ahochsteger
Copy link
Owner

@MikeDabrowski you may use ctx.proc.gdriveAdapter to get access to GDriveAdapter which provides the function createFile(location, fileData, conflictStrategy).
Have a look at the source code of the linked file for the details of the function and its parameters.
The fileData object encapsulates the relevant file content like the blob data you want to create.
I hope that helps - if not don't hesitate to ask.
I'll reopen this issue, since there's now more activity.
Regards, Andreas

@ahochsteger ahochsteger reopened this Jul 14, 2024
@MikeDabrowski
Copy link

MikeDabrowski commented Jul 14, 2024

Thank you. Getting closer :)
Is there a way to handle async in custom actions?

Btw, I could create a PR with pwd removal fn but it requires pdf-lib fork (quite a lengthy script) to be copied over OR converted to GAS library. I tried to peak at their code, hoping to extract only the pwd removal bit, but it requires much more time to invest. Unless npm install would work in this project and the lib would be included?

@MikeDabrowski
Copy link

const customActions = [{ name: 'decryptPdf', action: decryptPdfAction }];

...

function decryptPdfAction(ctx, args) {
  getDecryptedPdf(ctx.attachment.object, args.pwd)
    .catch(e => {
      console.error('Error while decrypting pdf', e);
    })
    .then(decryptedPdf => {
      console.info("Successfully decrypted pdf", decryptedPdf);
      console.info(`Attempting to save "${decryptedPdf.getName()}" to location "${locationFormatForDecryptedPdfs}"`);

      const {file, actionMeta} = ctx.proc.gdriveAdapter.createFileFromAction(
          ctx,
          locationFormatForDecryptedPdfs,
          decryptedPdf,
          'skip',
          '',
          'decrypted PDF',
          'custom',
          'custom.decryptPdf',
        );

      console.info(`Successfuly stored decrypted file at: "${actionMeta['custom.stored.location'].value}"`, file)
    })
    .catch(e => {
      console.error('Error while saving decrypted pdf', e);
      throw e;
    });
}

async function getDecryptedPdf(processedFileObject, password) {
  const bytes = processedFileObject.getBytes();
  const fileBase64 = Utilities.base64Encode(bytes);
  const pdfDoc = await PDFDocument.load(fileBase64, { password, ignoreEncryption: true});
  const unencrypted = await pdfDoc.save();
  return Utilities.newBlob(unencrypted, 'application/pdf', 'temp_decrypted_name.pdf');
}

Here is how you might decrypt PDFs.

I had to use createFileFromAction because it can parse location expression.

@ahochsteger
Copy link
Owner

@MikeDabrowski nice to see that you're making progress :-).

Concerning async in custom actions:
Unfortunately Google Apps Script is fully synchronous and does not support asynchronous processing, that's why custom actions cannot be asynchronous as well.
There are some hacky workarounds available like this but I fear they lead to more complexity and instability down the road.

Concerning the integration of pdf-lib:
I suggest to add @cantoo/pdf-lib to the dependencies and really integrate it as a real function.
This way it is integrated as a fully-supported dependency, uses tree-shaking of rollup and is automatically updated by renovate.

Writing custom functions with new dependencies might be too complicated.

@MikeDabrowski MikeDabrowski linked a pull request Jul 16, 2024 that will close this issue
14 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature A feature suggestion
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants