| 
									
										
										
										
											2023-06-04 08:05:50 +00:00
										 |  |  | --- | 
					
						
							|  |  |  | title: Electronic Mail | 
					
						
							|  |  |  | --- | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import current from '/version.js'; | 
					
						
							|  |  |  | import CodeBlock from '@theme/CodeBlock'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <head> | 
					
						
							|  |  |  |   <script src="/pst/pstextractor.js"></script> | 
					
						
							|  |  |  | </head> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Electronic mail ("email" or "e-mail") is an essential part of modern business | 
					
						
							|  |  |  | workflows. Spreadsheets are commonly passed around and processed. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | There are NodeJS and other server-side solutions for sending email with attached | 
					
						
							|  |  |  | spreadsheets as well as processing spreadsheets in inboxes. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This demo covers three workflows: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - [Sending mail](#sending-mail) covers libraries for sending messages | 
					
						
							|  |  |  | - [Reading mail](#reading-mail) covers libraries for reading messages | 
					
						
							|  |  |  | - [Data files](#data-files) covers mailbox file formats | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::warning | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | There are a number of caveats when dealing with live mail servers. It is advised | 
					
						
							|  |  |  | to follow connector module documentation carefully and test with new accounts | 
					
						
							|  |  |  | before integrating with important inboxes or accounts. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Live Servers
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::warning | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | It is strongly advised to use a test email address before using an important | 
					
						
							|  |  |  | address.  One small mistake could erase decades of messages or result in a block | 
					
						
							|  |  |  | or ban from Google services. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### Email Details
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### App Passwords
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Many email providers (including Fastmail, GMail, and Yahoo Mail) require "app | 
					
						
							|  |  |  | passwords" or passwords for "less secure apps". Attempting to connect and send | 
					
						
							|  |  |  | using the account password will throw errors. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### Test Account
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | It is strongly recommended to first test with an independent service provider. | 
					
						
							|  |  |  | This demo will start with a free 30-day trial of Fastmail. At the time the demo | 
					
						
							|  |  |  | was last tested, no payment details were required. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::caution | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | A valid phone number (for SMS verification) was required. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 0) Create a new Fastmail email account and verify with a mobile number. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | _Create App Password_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 1) Open the settings screen (click on the icon in the top-left corner of the | 
					
						
							|  |  |  | screen and select "Settings"). | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 2) Select "Privacy & Security" in the left pane, then click "Integrations" near | 
					
						
							|  |  |  | the top of the main page.  Click "New app password". | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 3) Select any name in the top drop-down (the default "iPhone" can be used). In | 
					
						
							|  |  |  | the second drop-down, select "Mail (IMAP/POP/SMTP)". Click "Generate password". | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | A new password will be displayed. This is the app password that will be used in | 
					
						
							|  |  |  | the demo script. **Copy the displayed password or write it down.** | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### Sending Mail
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Many SheetJS users deploy the `nodemailer` module in production. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | `nodemailer` supports NodeJS Buffer attachments generated from `XLSX.write`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | /* write workbook to buffer */ | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  | const buf = XLSX.write(workbook, { | 
					
						
							|  |  |  |   bookType: "xlsb", // <-- write XLSB file | 
					
						
							|  |  |  |   type: "buffer"    // <-- generate a buffer | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* create a message */ | 
					
						
							|  |  |  | const msg = { from: "*", to: "*", subject: "*", text: "*", | 
					
						
							|  |  |  |   attachments: [ | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       filename: "SheetJSMailExport.xlsb", // <-- filename | 
					
						
							|  |  |  |       content: buf                        // <-- data | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  |   ] | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::caution | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The file name must have the expected extension for the `bookType`! | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ["Supported Output Formats"](/docs/api/write-options#supported-output-formats) | 
					
						
							|  |  |  | includes a table showing the file extension required for each supported type. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### Send Demo
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::note | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This demo was tested in the following deployments: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | | Email Provider | Date       | Library      | Version | | 
					
						
							|  |  |  | |:---------------|:-----------|:-------------|:--------| | 
					
						
							|  |  |  | | `gmail.com`    | 2023-06-03 | `nodemailer` | `6.9.3` | | 
					
						
							|  |  |  | | `fastmail.com` | 2023-06-03 | `nodemailer` | `6.9.3` | | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 0) Create a [new account](#test-account) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 1) Create a new project and install dependencies: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <CodeBlock language="bash">{`\ | 
					
						
							|  |  |  | mkdir sheetjs-send | 
					
						
							|  |  |  | cd sheetjs-send | 
					
						
							|  |  |  | npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz nodemailer@6.9.3`} | 
					
						
							|  |  |  | </CodeBlock> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 2) Save the following script to `SheetJSend.js`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="SheetJSend.js" | 
					
						
							|  |  |  | const XLSX = require('xlsx'); | 
					
						
							|  |  |  | const nodemailer = require('nodemailer'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const transporter = nodemailer.createTransport({ | 
					
						
							|  |  |  |   service: 'fastmail', | 
					
						
							|  |  |  |   auth: { | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  |     user: '**', | 
					
						
							|  |  |  |     pass: '**' | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const wb = XLSX.utils.book_new(); | 
					
						
							|  |  |  | const ws = XLSX.utils.aoa_to_sheet([["Sheet","JS"], ["Node","Mailer"]]); | 
					
						
							|  |  |  | XLSX.utils.book_append_sheet(wb, ws, "Sheet1"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const buf = XLSX.write(wb, { bookType: "xlsb", type: "buffer" }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const mailOptions = { | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  |   from: "**", | 
					
						
							|  |  |  |   to: "**", | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  |   subject: "Attachment test", | 
					
						
							|  |  |  |   text: "if this succeeded, there will be an attachment", | 
					
						
							|  |  |  |   attachments: [{ | 
					
						
							|  |  |  |     filename: "SheetJSMailExport.xlsb", // <-- filename | 
					
						
							|  |  |  |     content: buf                        // <-- data | 
					
						
							|  |  |  |   }] | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | transporter.sendMail(mailOptions, function (err, info) { | 
					
						
							|  |  |  |   if(err) console.log(err); else console.log(info); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 3) Edit `SheetJSend.js` and replace the highlighted lines: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - `user: "**",` the value should be the sender email address | 
					
						
							|  |  |  | - `pass: "**"` the value should be the app password from earlier | 
					
						
							|  |  |  | - `from: "**",` the value should be the sender email address | 
					
						
							|  |  |  | - `to: "**",` the value should be your work or personal email address | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 4) Run the script: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | node SheetJSend.js | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | If the process succeeded, the terminal will print a JS object with fields | 
					
						
							|  |  |  | including `accepted` and `response`. The recipient inbox should receive an email | 
					
						
							|  |  |  | shortly.  The email will include an attachment `SheetJSMailExport.xlsb` which | 
					
						
							|  |  |  | can be opened in Excel. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::caution | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The app password must be entered in step 3. If the account password was used, | 
					
						
							|  |  |  | the mailer will fail with a message that includes: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | Sorry, you need to create an app password to use this service | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### Reading Mail
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | `imapflow` is a modern IMAP client library. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Parsing attachments is a multi-step dance: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 1) Fetch a message and parse the body structure: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | let m = await client.fetchOne(client.mailbox.exists, { bodyStructure: true }); | 
					
						
							|  |  |  | let children = message.bodyStructure.childNodes; | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 2) Find all attachments with relevant file extensions: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | for(let bs of message.bodyStructure.childNodes) { | 
					
						
							|  |  |  |   if(bs.disposition?.toLowerCase() != "attachment") continue; | 
					
						
							|  |  |  |   // look for attachments with certain extensions | 
					
						
							|  |  |  |   if(!/\.(numbers|xls[xbm]?)$/i.test(bs?.parameters?.name)) continue; | 
					
						
							|  |  |  |   await process_attachment(bs); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 3) Download data and collect into a NodeJS Buffer: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | /* helper function to concatenate data from a stream */ | 
					
						
							|  |  |  | const concat_RS = (stream) => new Promise((res, rej) => { | 
					
						
							|  |  |  |   var buffers = []; | 
					
						
							|  |  |  |   stream.on("data", function(data) { buffers.push(data); }); | 
					
						
							|  |  |  |   stream.on("end", function() { res(Buffer.concat(buffers)); }); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | async function process_attachment(bs) { | 
					
						
							|  |  |  |   const { content } = await client.download('*', bs.part); | 
					
						
							|  |  |  |   /* content is a stream */ | 
					
						
							|  |  |  |   const buf = await concat_RS(content); | 
					
						
							|  |  |  |   return process_buf(buf, bs.parameters.name); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 4) Parse Buffer with `XLSX.read`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | function process_buf(buf, name) { | 
					
						
							|  |  |  |   const wb = XLSX.read(buf); | 
					
						
							|  |  |  |   /* DO SOMETHING WITH wb HERE */ | 
					
						
							|  |  |  |   // print file name and CSV of first sheet | 
					
						
							|  |  |  |   const wsname = wb.SheetNames[0]; | 
					
						
							|  |  |  |   console.log(name); | 
					
						
							|  |  |  |   console.log(XLSX.utils.sheet_to_csv(wb.Sheets[wsname])); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### Receive Demo
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::note | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This demo was tested in the following deployments: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | | Email Provider | Date       | Library    | Version   | | 
					
						
							|  |  |  | |:---------------|:-----------|:-----------|:----------| | 
					
						
							|  |  |  | | `fastmail.com` | 2023-06-03 | `imapflow` | `1.0.128` | | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 0) Create a [new account](#test-account) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 1) Create a new project and install dependencies: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <CodeBlock language="bash">{`\ | 
					
						
							|  |  |  | mkdir sheetjs-recv | 
					
						
							|  |  |  | cd sheetjs-recv | 
					
						
							|  |  |  | npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz imapflow@1.0.128`} | 
					
						
							|  |  |  | </CodeBlock> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 2) Save the following script to `SheetJSIMAP.js`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="SheetJSIMAP.js" | 
					
						
							|  |  |  | const XLSX = require('xlsx'); | 
					
						
							|  |  |  | const { ImapFlow } = require('imapflow'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const client = new ImapFlow({ | 
					
						
							|  |  |  |   host: 'imap.fastmail.com', port: 993, secure: true, logger: false, | 
					
						
							|  |  |  |   auth: { | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  |     user: '**', | 
					
						
							|  |  |  |     pass: '**' | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const concat_RS = (stream)  => new Promise((res, rej) => { | 
					
						
							|  |  |  |   var buffers = []; | 
					
						
							|  |  |  |   stream.on("data", function(data) { buffers.push(data); }); | 
					
						
							|  |  |  |   stream.on("end", function() { res(Buffer.concat(buffers)); }); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | (async() => { | 
					
						
							|  |  |  |   await client.connect(); | 
					
						
							|  |  |  |   let lock = await client.getMailboxLock('INBOX'); // INBOX | 
					
						
							|  |  |  |   try { | 
					
						
							|  |  |  |     // fetch latest message source with body structure | 
					
						
							|  |  |  |     let message = await client.fetchOne(client.mailbox.exists, { bodyStructure: true }); | 
					
						
							|  |  |  |     for(let bs of message.bodyStructure.childNodes) { | 
					
						
							|  |  |  |       if(bs.disposition?.toLowerCase() != "attachment") continue; | 
					
						
							|  |  |  |       // look for attachments with certain extensions | 
					
						
							|  |  |  |       if(!/\.(numbers|xls[xbm]?)$/i.test(bs?.parameters?.name)) continue; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // download data | 
					
						
							|  |  |  |       const { content } = await client.download('*', bs.part); | 
					
						
							|  |  |  |       const buf = await concat_RS(content); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // parse | 
					
						
							|  |  |  |       const wb = XLSX.read(buf); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // print file name and CSV of first sheet | 
					
						
							|  |  |  |       const wsname = wb.SheetNames[0]; | 
					
						
							|  |  |  |       console.log(bs.parameters.name); | 
					
						
							|  |  |  |       console.log(XLSX.utils.sheet_to_csv(wb.Sheets[wsname])); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } finally { lock.release(); } | 
					
						
							|  |  |  |   await client.logout(); | 
					
						
							|  |  |  | })(); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 3) Edit `SheetJSIMAP.js` and replace the highlighted lines: | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-05 20:12:53 +00:00
										 |  |  | - `user: "**",` the value should be the account address | 
					
						
							| 
									
										
										
										
											2023-06-04 08:05:50 +00:00
										 |  |  | - `pass: "**"` the value should be the app password from earlier | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 4) Download <https://sheetjs.com/pres.numbers>.  Using a different account, send | 
					
						
							|  |  |  | an email to the test account and attach the file.  At the end of this step, the | 
					
						
							|  |  |  | test account should have an email in the inbox that has an attachment. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 5) Run the script: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | node SheetJSIMAP.js | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The output should include the file name (`pres.numbers`) and the CSV: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | pres.numbers | 
					
						
							|  |  |  | Name,Index | 
					
						
							|  |  |  | Bill Clinton,42 | 
					
						
							|  |  |  | GeorgeW Bush,43 | 
					
						
							|  |  |  | Barack Obama,44 | 
					
						
							|  |  |  | Donald Trump,45 | 
					
						
							|  |  |  | Joseph Biden,46 | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Data Files
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Electronic discovery commonly involves email spelunking. There are a number of | 
					
						
							|  |  |  | proprietary mail and email account file formats. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### PST
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | `PST` is a common file format. The `pst-extractor` library is designed for | 
					
						
							|  |  |  | extracting messages and attachments from `PST` files in NodeJS and the browser. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This demo uses [a special build](pathname:///pst/pstextractor.js) for the web. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details><summary><b>Build details</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 1) Initialize a new NodeJS project and install the dependency: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | mkdir pstextract | 
					
						
							|  |  |  | cd pstextract | 
					
						
							|  |  |  | npm init -y | 
					
						
							|  |  |  | npm i --save pst-extractor@1.9.0 | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 2) Save the following to `shim.js`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="shim.js" | 
					
						
							|  |  |  | const PSTExtractor = require("pst-extractor"); | 
					
						
							|  |  |  | module.exports = PSTExtractor; | 
					
						
							|  |  |  | module.exports.Buffer = Buffer; | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 3) Build the script: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npx browserify@17.0.0 -s PSTExtractor -o pstextractor.js shim.js | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The [test file](pathname:///pst/enron.pst) was based on the EDRM clean extract | 
					
						
							|  |  |  | from the "Enron Corpus" and includes a few XLS attachments. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```jsx live | 
					
						
							|  |  |  | function SheetJSPreviewPSTSheets() { | 
					
						
							|  |  |  |   const [ files, setFiles ] = React.useState([]); | 
					
						
							|  |  |  |   const [ __html, setHTML ] = React.useState(""); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /* recursively walk PST and collect attachments */ | 
					
						
							|  |  |  |   const walk = (f,arr) => { | 
					
						
							|  |  |  |     if(f.hasSubfolders) for(let sf of f.getSubFolders()) walk(sf,arr); | 
					
						
							|  |  |  |     if(f.contentCount > 0) for(let e = f.getNextChild(); e != null; e = f.getNextChild()) { | 
					
						
							|  |  |  |       for(var i = 0; i < e.numberOfAttachments; ++i) { | 
					
						
							|  |  |  |         var a = e.getAttachment(i); | 
					
						
							|  |  |  |         /* XLS spreadsheet test by filename */ | 
					
						
							|  |  |  |         if(a.filename.endsWith(".xls")) arr.push(a); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /* view selected attachment */ | 
					
						
							|  |  |  |   const view = (j) => { | 
					
						
							|  |  |  |     /* collect data into a "Buffer" */ | 
					
						
							|  |  |  |     const strm = files[j].fileInputStream; | 
					
						
							|  |  |  |     const data = new PSTExtractor.Buffer(strm._length.low); | 
					
						
							|  |  |  |     strm.readCompletely(data); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /* parse */ | 
					
						
							|  |  |  |     const wb = XLSX.read(data); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /* convert first sheet to HTML */ | 
					
						
							|  |  |  |     const ws = wb.Sheets[wb.SheetNames[0]]; | 
					
						
							|  |  |  |     setHTML(XLSX.utils.sheet_to_html(ws)); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /* process array buffer */ | 
					
						
							|  |  |  |   const process_ab = (ab) => { | 
					
						
							|  |  |  |     const pst = new (PSTExtractor.PSTFile)(new PSTExtractor.Buffer(ab)); | 
					
						
							|  |  |  |     const data = []; | 
					
						
							|  |  |  |     walk(pst.getRootFolder(), data); | 
					
						
							|  |  |  |     setFiles(data); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /* on click, fetch and process file */ | 
					
						
							|  |  |  |   const doit = async() => { | 
					
						
							|  |  |  |     const ab = await (await fetch("/pst/enron.pst")).arrayBuffer(); | 
					
						
							|  |  |  |     process_ab(ab); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  |   const chg = async(e) => process_ab(await e.target.files[0].arrayBuffer()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return ( <> | 
					
						
							|  |  |  |     <p>Use the file input to select a file, or click "Use a Sample PST"</p> | 
					
						
							|  |  |  |     <button onClick={doit}>Use a Sample PST!</button><br/><br/> | 
					
						
							|  |  |  |     <input type="file" accept=".pst" onChange={chg}/><br/> | 
					
						
							|  |  |  |     <b>Attachments</b> | 
					
						
							|  |  |  |     <ul>{files.map((f,j) => ( | 
					
						
							|  |  |  |       <li key={j}><a onClick={()=>view(j)}>{f.filename} (click to view)</a></li> | 
					
						
							| 
									
										
										
										
											2023-06-05 20:12:53 +00:00
										 |  |  |     ))}</ul> | 
					
						
							| 
									
										
										
										
											2023-06-04 08:05:50 +00:00
										 |  |  |     <b>Table View</b><br/> | 
					
						
							|  |  |  |     <div dangerouslySetInnerHTML={{__html}}></div> | 
					
						
							|  |  |  |   </> ); | 
					
						
							|  |  |  | } |