@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.
Files changed (4) hide show
  1. package/README.md +78 -0
  2. package/bin/cli.js +90 -0
  3. package/index.js +166 -0
  4. 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
+ }