@vinnyum/newsletter-sync 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/bin/cli.js +90 -0
- package/index.js +166 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @vinnyum/newsletter-sync
|
|
2
|
+
|
|
3
|
+
Automated, weekly CLI tool to download newsletters from Zoho Mail (via Composio), convert them into PDF files using Puppeteer, and upload them as sources directly into a Google NotebookLM notebook.
|
|
4
|
+
|
|
5
|
+
Includes support for failure alerts sent directly to your email if a sync fails.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Within the `vinnyum-tools` monorepo root:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
To run it locally or link the bin:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm link
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## CLI Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx newsletter-sync [options]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Options
|
|
28
|
+
|
|
29
|
+
| Flag | Description |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `-c, --config <file>` | Path to a JSON configuration file |
|
|
32
|
+
| `--apiKey <key>` | Composio API Key (overrides env `COMPOSIO_API_KEY`) |
|
|
33
|
+
| `--zohoAccountId <id>` | Zoho Mail Account ID |
|
|
34
|
+
| `--zohoFolderId <id>` | Zoho Mail Folder ID (e.g. your newsletter folder) |
|
|
35
|
+
| `--senderFilter <email>` | Filter string to identify the newsletter sender |
|
|
36
|
+
| `--notebookId <id>` | Google NotebookLM ID |
|
|
37
|
+
| `--alertEmail <email>` | Destination email address for critical failures |
|
|
38
|
+
| `--fromEmail <email>` | From email address for Zoho Mail alert emails |
|
|
39
|
+
| `--processed <path>` | Path to save the processed IDs tracking list (default: `./processed_emails.json`) |
|
|
40
|
+
| `--log <path>` | Path to save the execution logs (default: `./cron.log`) |
|
|
41
|
+
|
|
42
|
+
## Setup Example
|
|
43
|
+
|
|
44
|
+
### 1. Create a `config.json` file
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"composioApiKey": "your-composio-api-key",
|
|
49
|
+
"zohoAccountId": "1988858000000008002",
|
|
50
|
+
"zohoFolderId": "1988858000000009011",
|
|
51
|
+
"senderFilter": "newsletter@jamiebrindle.io",
|
|
52
|
+
"notebookId": "17c7393b-19eb-4dcc-b497-38d05976448d",
|
|
53
|
+
"alertEmail": "vinny@vinnyum.tech",
|
|
54
|
+
"fromEmail": "vinny@vinnyum.tech",
|
|
55
|
+
"processedPath": "./processed_emails.json",
|
|
56
|
+
"logPath": "./cron.log"
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Run the command with config
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx newsletter-sync --config ./config.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Scheduling on Windows
|
|
67
|
+
|
|
68
|
+
To run this tool every week automatically:
|
|
69
|
+
|
|
70
|
+
1. Create a batch script `run-sync.bat`:
|
|
71
|
+
```bat
|
|
72
|
+
@echo off
|
|
73
|
+
cd /d "%~dp0"
|
|
74
|
+
npx newsletter-sync --config ./config.json
|
|
75
|
+
```
|
|
76
|
+
2. Open **Windows Task Scheduler** (`taskschd.msc`).
|
|
77
|
+
3. Select **Create Basic Task** and set it to **Weekly** (e.g., Mondays at 9:00 AM).
|
|
78
|
+
4. For action, choose **Start a program**, select `run-sync.bat`, and set the **Start in (optional)** field to the folder containing your `config.json` and bat script.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { runSync } from '../index.js';
|
|
7
|
+
|
|
8
|
+
const options = {
|
|
9
|
+
config: { type: 'string', short: 'c' },
|
|
10
|
+
apiKey: { type: 'string' },
|
|
11
|
+
zohoAccountId: { type: 'string' },
|
|
12
|
+
zohoFolderId: { type: 'string' },
|
|
13
|
+
senderFilter: { type: 'string' },
|
|
14
|
+
notebookId: { type: 'string' },
|
|
15
|
+
alertEmail: { type: 'string' },
|
|
16
|
+
fromEmail: { type: 'string' },
|
|
17
|
+
processed: { type: 'string' },
|
|
18
|
+
log: { type: 'string' },
|
|
19
|
+
help: { type: 'boolean', short: 'h' }
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function printUsage() {
|
|
23
|
+
console.log(`
|
|
24
|
+
Usage: npx newsletter-sync [options]
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
-c, --config <file> Path to a JSON configuration file
|
|
28
|
+
--apiKey <key> Composio API Key (can be set as COMPOSIO_API_KEY env var)
|
|
29
|
+
--zohoAccountId <id> Zoho Mail Account ID
|
|
30
|
+
--zohoFolderId <id> Zoho Mail Folder ID
|
|
31
|
+
--senderFilter <filter> Sender filter string (e.g. newsletter@sender.com)
|
|
32
|
+
--notebookId <id> Google NotebookLM ID
|
|
33
|
+
--alertEmail <email> Email destination for failure notifications
|
|
34
|
+
--fromEmail <email> From email for error alert emails (must be verified in Zoho)
|
|
35
|
+
--processed <path> Path to processed_emails.json (default: ./processed_emails.json)
|
|
36
|
+
--log <path> Path to execution log file (default: ./cron.log)
|
|
37
|
+
-h, --help Show help
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
try {
|
|
43
|
+
const { values } = parseArgs({ options, allowPositionals: true });
|
|
44
|
+
|
|
45
|
+
if (values.help) {
|
|
46
|
+
printUsage();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let configData = {};
|
|
51
|
+
|
|
52
|
+
// 1. Read from config JSON if provided
|
|
53
|
+
if (values.config) {
|
|
54
|
+
const configPath = path.resolve(process.cwd(), values.config);
|
|
55
|
+
if (!fs.existsSync(configPath)) {
|
|
56
|
+
console.error(`❌ Error: Config file not found at ${configPath}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error(`❌ Error: Failed to parse JSON config file: ${e.message}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Resolve merged configurations (CLI flags override config JSON)
|
|
68
|
+
const resolvedOptions = {
|
|
69
|
+
apiKey: values.apiKey || configData.composioApiKey || process.env.COMPOSIO_API_KEY,
|
|
70
|
+
zohoAccountId: values.zohoAccountId || configData.zohoAccountId,
|
|
71
|
+
zohoFolderId: values.zohoFolderId || configData.zohoFolderId,
|
|
72
|
+
senderFilter: values.senderFilter || configData.senderFilter,
|
|
73
|
+
notebookId: values.notebookId || configData.notebookId,
|
|
74
|
+
alertEmail: values.alertEmail || configData.alertEmail,
|
|
75
|
+
fromEmail: values.fromEmail || configData.fromEmail,
|
|
76
|
+
processedPath: values.processed || configData.processedPath || './processed_emails.json',
|
|
77
|
+
logPath: values.log || configData.logPath || './cron.log'
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// 3. Run Sincronizador
|
|
81
|
+
await runSync(resolvedOptions);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error(`❌ Execution Failed: ${err.message}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
main();
|
package/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import puppeteer from 'puppeteer';
|
|
5
|
+
import ComposioModule from '@composio/client';
|
|
6
|
+
|
|
7
|
+
const Composio = ComposioModule.default || ComposioModule.Composio || ComposioModule;
|
|
8
|
+
|
|
9
|
+
function sanitize(filename) {
|
|
10
|
+
return filename.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function runSync(options = {}) {
|
|
14
|
+
const {
|
|
15
|
+
apiKey = process.env.COMPOSIO_API_KEY,
|
|
16
|
+
zohoAccountId,
|
|
17
|
+
zohoFolderId,
|
|
18
|
+
senderFilter,
|
|
19
|
+
notebookId,
|
|
20
|
+
alertEmail,
|
|
21
|
+
fromEmail,
|
|
22
|
+
processedPath = './processed_emails.json',
|
|
23
|
+
logPath = './cron.log'
|
|
24
|
+
} = options;
|
|
25
|
+
|
|
26
|
+
// Helper for logging
|
|
27
|
+
const log = (msg) => {
|
|
28
|
+
const time = new Date().toISOString();
|
|
29
|
+
const line = `[${time}] ${msg}\n`;
|
|
30
|
+
console.log(msg);
|
|
31
|
+
if (logPath) {
|
|
32
|
+
fs.appendFileSync(path.resolve(process.cwd(), logPath), line);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Validation
|
|
37
|
+
if (!apiKey) throw new Error("Missing Composio API Key. Pass --apiKey or set COMPOSIO_API_KEY environment variable.");
|
|
38
|
+
if (!zohoAccountId) throw new Error("Missing Zoho Account ID. Pass --zohoAccountId.");
|
|
39
|
+
if (!zohoFolderId) throw new Error("Missing Zoho Folder ID. Pass --zohoFolderId.");
|
|
40
|
+
if (!senderFilter) throw new Error("Missing Sender Filter. Pass --senderFilter.");
|
|
41
|
+
if (!notebookId) throw new Error("Missing Notebook ID. Pass --notebookId.");
|
|
42
|
+
|
|
43
|
+
const processedFile = path.resolve(process.cwd(), processedPath);
|
|
44
|
+
let processed = [];
|
|
45
|
+
if (fs.existsSync(processedFile)) {
|
|
46
|
+
try {
|
|
47
|
+
processed = JSON.parse(fs.readFileSync(processedFile, 'utf8'));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
log(`Warning: Failed to parse processed list, starting fresh.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sendErrorEmail = async (error) => {
|
|
54
|
+
if (!alertEmail || !fromEmail) {
|
|
55
|
+
log("No alert/from email configured. Skipping email notification.");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
log("Sending error email notification via Zoho Mail...");
|
|
60
|
+
const client = new Composio({ apiKey });
|
|
61
|
+
await client.tools.execute('ZOHO_MAIL_MESSAGES_SEND_EMAIL', {
|
|
62
|
+
accountId: zohoAccountId,
|
|
63
|
+
fromAddress: fromEmail,
|
|
64
|
+
toAddress: alertEmail,
|
|
65
|
+
subject: `[ALERT] Newsletter Sync Failure`,
|
|
66
|
+
content: `Newsletter Sync encountered an error:\n\n${error.stack || error.message || error}`,
|
|
67
|
+
mailFormat: 'plaintext'
|
|
68
|
+
});
|
|
69
|
+
log("Notification email sent successfully.");
|
|
70
|
+
} catch (emailErr) {
|
|
71
|
+
log(`CRITICAL: Failed to send error email: ${emailErr.message}`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
log("Starting Newsletter Sync execution...");
|
|
76
|
+
const client = new Composio({ apiKey });
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// 1. Fetch recent emails in the folder
|
|
80
|
+
log(`Fetching emails from folder ${zohoFolderId}...`);
|
|
81
|
+
const listResponse = await client.tools.execute('ZOHO_MAIL_MESSAGES_LIST_EMAILS', {
|
|
82
|
+
account_id: zohoAccountId,
|
|
83
|
+
folder_id: zohoFolderId,
|
|
84
|
+
limit: 100
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!listResponse || !listResponse.data || !listResponse.data.data) {
|
|
88
|
+
throw new Error(`Failed to list emails: ${JSON.stringify(listResponse)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const emails = listResponse.data.data;
|
|
92
|
+
log(`Retrieved ${emails.length} emails from Zoho folder.`);
|
|
93
|
+
|
|
94
|
+
// 2. Filter for new matching emails
|
|
95
|
+
const pending = emails.filter(e => {
|
|
96
|
+
const matchesSender = e.fromAddress.includes(senderFilter);
|
|
97
|
+
const alreadyProcessed = processed.includes(e.messageId);
|
|
98
|
+
return matchesSender && !alreadyProcessed;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
log(`Found ${pending.length} new matching emails.`);
|
|
102
|
+
if (pending.length === 0) {
|
|
103
|
+
log("Execution completed. Nothing to sync.");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. Launch browser for PDF generation
|
|
108
|
+
log("Launching Puppeteer...");
|
|
109
|
+
const browser = await puppeteer.launch();
|
|
110
|
+
const page = await browser.newPage();
|
|
111
|
+
|
|
112
|
+
for (const email of pending) {
|
|
113
|
+
try {
|
|
114
|
+
log(`Processing: "${email.subject}" (${email.messageId})...`);
|
|
115
|
+
|
|
116
|
+
// Get content
|
|
117
|
+
const contentResponse = await client.tools.execute('ZOHO_MAIL_MESSAGES_GET_MESSAGE_CONTENT', {
|
|
118
|
+
account_id: zohoAccountId,
|
|
119
|
+
folder_id: zohoFolderId,
|
|
120
|
+
message_id: email.messageId
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!contentResponse || !contentResponse.data || !contentResponse.data.data) {
|
|
124
|
+
throw new Error(`Failed to retrieve content for email ${email.messageId}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const htmlContent = contentResponse.data.data.content;
|
|
128
|
+
const pdfFilename = sanitize(email.subject) + '.pdf';
|
|
129
|
+
const pdfPath = path.resolve(process.cwd(), pdfFilename);
|
|
130
|
+
|
|
131
|
+
// Render HTML to PDF
|
|
132
|
+
log(`Generating PDF: ${pdfFilename}...`);
|
|
133
|
+
await page.setContent(htmlContent, { waitUntil: 'load', timeout: 60000 });
|
|
134
|
+
await page.pdf({ path: pdfPath, format: 'A4' });
|
|
135
|
+
|
|
136
|
+
// Upload to NotebookLM
|
|
137
|
+
log(`Uploading PDF to NotebookLM...`);
|
|
138
|
+
const cmd = `nlm source add "${notebookId}" --file "${pdfPath}" --wait`;
|
|
139
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
140
|
+
log(`Successfully uploaded: ${pdfFilename}`);
|
|
141
|
+
|
|
142
|
+
// Clean up local PDF
|
|
143
|
+
if (fs.existsSync(pdfPath)) {
|
|
144
|
+
fs.unlinkSync(pdfPath);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Track as processed
|
|
148
|
+
processed.push(email.messageId);
|
|
149
|
+
fs.writeFileSync(processedFile, JSON.stringify(processed, null, 2));
|
|
150
|
+
log(`Logged ${email.messageId} as processed.`);
|
|
151
|
+
|
|
152
|
+
} catch (itemError) {
|
|
153
|
+
log(`Error processing email ${email.messageId}: ${itemError.message}`);
|
|
154
|
+
// Continue with other items
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await browser.close();
|
|
159
|
+
log("Newsletter Sync execution finished successfully.");
|
|
160
|
+
|
|
161
|
+
} catch (error) {
|
|
162
|
+
log(`CRITICAL ERROR: ${error.stack}`);
|
|
163
|
+
await sendErrorEmail(error);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vinnyum/newsletter-sync",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Weekly automated newsletter to PDF converter and Google NotebookLM source synchronizer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"newsletter-sync": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18.0.0"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@composio/client": "^0.1.0-alpha.75",
|
|
15
|
+
"puppeteer": "^25.3.0"
|
|
16
|
+
},
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Vinny Jimenez",
|
|
19
|
+
"email": "hello@vinnyum.tech",
|
|
20
|
+
"url": "https://www.linkedin.com/in/vinny-jimenez"
|
|
21
|
+
},
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"composio",
|
|
25
|
+
"notebooklm",
|
|
26
|
+
"zoho",
|
|
27
|
+
"newsletter",
|
|
28
|
+
"pdf",
|
|
29
|
+
"synchronizer"
|
|
30
|
+
]
|
|
31
|
+
}
|