forked from sheetjs/docs.sheetjs.com
This commit is contained in:
parent
b62d074900
commit
19dfa57120
464
docz/docs/03-demos/03-net/03-email.md
Normal file
464
docz/docs/03-demos/03-net/03-email.md
Normal file
@ -0,0 +1,464 @@
|
||||
---
|
||||
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:
|
||||
|
||||
- `user: "**",` the value should be the sender email address
|
||||
- `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>
|
||||
))}
|
||||
</ul>
|
||||
<b>Table View</b><br/>
|
||||
<div dangerouslySetInnerHTML={{__html}}></div>
|
||||
</> );
|
||||
}
|
@ -632,6 +632,23 @@ function process_RS(stream, cb) {
|
||||
cb(workbook);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
In recent versions of NodeJS, Promises are preferred:
|
||||
|
||||
```js
|
||||
var XLSX = require("xlsx");
|
||||
|
||||
/* async_RS reads a stream and returns a Promise resolving to a workbook */
|
||||
const async_RS = (stream) => new Promise((res, rej) => {
|
||||
var buffers = [];
|
||||
stream.on("data", function(data) { buffers.push(data); });
|
||||
stream.on("end", function() {
|
||||
const buf = Buffer.concat(buffers);
|
||||
const wb = XLSX.read(buf);
|
||||
res(wb);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
BIN
docz/static/pst/enron.pst
Normal file
BIN
docz/static/pst/enron.pst
Normal file
Binary file not shown.
30048
docz/static/pst/pstextractor.js
Normal file
30048
docz/static/pst/pstextractor.js
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user