pkg-track 1.0.2 → 2.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 +26 -53
- package/package.json +1 -1
- package/src/gmail-old.js +397 -0
- package/src/gmail.js +45 -105
package/README.md
CHANGED
|
@@ -54,51 +54,21 @@ npm install -g git+https://github.com/vvanessaww/pkg-track.git
|
|
|
54
54
|
|
|
55
55
|
> ⚠️ **Installing from git requires build tools** for compiling native modules (better-sqlite3). On macOS, install Xcode Command Line Tools: `xcode-select --install`. On Ubuntu/Debian: `sudo apt-get install build-essential python3`
|
|
56
56
|
|
|
57
|
-
### 2.
|
|
58
|
-
|
|
59
|
-
**Quick Google Cloud setup** (sounds scary, but it's just clicking through):
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
# 1. Create the config directory
|
|
63
|
-
mkdir -p ~/.pkg-tracker
|
|
64
|
-
|
|
65
|
-
# 2. Follow these steps to get your credentials:
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
**Google Cloud Console** (one-time):
|
|
69
|
-
1. Go to https://console.cloud.google.com/apis/credentials
|
|
70
|
-
2. **Create Project** → Name: `pkg-track` (or anything) → **Create**
|
|
71
|
-
3. **Enable APIs** → Search **"Gmail API"** → **Enable**
|
|
72
|
-
4. **Configure Consent Screen**:
|
|
73
|
-
- User Type: **External** → **Create**
|
|
74
|
-
- App name: `pkg-track`
|
|
75
|
-
- User support email: (your email)
|
|
76
|
-
- Developer contact: (your email)
|
|
77
|
-
- **Save and Continue** (skip scopes/test users)
|
|
78
|
-
5. **Credentials** → **Create Credentials** → **OAuth client ID**:
|
|
79
|
-
- Application type: **Desktop app**
|
|
80
|
-
- Name: `pkg-track`
|
|
81
|
-
- **Create**
|
|
82
|
-
6. **Download JSON** (click the download icon ⬇️)
|
|
83
|
-
7. **Move the file**:
|
|
84
|
-
```bash
|
|
85
|
-
mv ~/Downloads/client_secret_*.json ~/.pkg-tracker/credentials.json
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
**Why this setup?** Google requires each user to create their own OAuth app for Gmail access. This ensures your data stays private and you control the permissions. Your credentials never leave your computer.
|
|
89
|
-
|
|
90
|
-
### 3. Initialize
|
|
57
|
+
### 2. Initialize
|
|
91
58
|
|
|
92
59
|
```bash
|
|
93
60
|
pkg init
|
|
94
61
|
```
|
|
95
62
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
63
|
+
**What happens:**
|
|
64
|
+
1. Opens your browser to a secure authorization page
|
|
65
|
+
2. You click "Sign in with Gmail"
|
|
66
|
+
3. Grant read-only access to your Gmail
|
|
67
|
+
4. Token is saved locally on your computer
|
|
68
|
+
|
|
69
|
+
**That's it!** Simple one-click setup, no Google Cloud configuration needed.
|
|
100
70
|
|
|
101
|
-
**
|
|
71
|
+
**Privacy:** We use read-only Gmail access (`gmail.readonly` scope). Your credentials and package data stay on your computer. You can revoke access anytime at https://myaccount.google.com/permissions
|
|
102
72
|
|
|
103
73
|
## Usage
|
|
104
74
|
|
|
@@ -256,20 +226,6 @@ If you want to stop using pkg-track:
|
|
|
256
226
|
|
|
257
227
|
**Why it's safe:** You created the OAuth credentials yourself - it's YOUR app accessing YOUR Gmail. No one else has access.
|
|
258
228
|
|
|
259
|
-
### "Gmail credentials not found"
|
|
260
|
-
|
|
261
|
-
**Problem:** You see `⚠️ Gmail OAuth credentials not found.`
|
|
262
|
-
|
|
263
|
-
**Solution:**
|
|
264
|
-
1. Make sure you've created OAuth credentials in Google Cloud Console (see [Quick Start](#-quick-start))
|
|
265
|
-
2. Download the credentials JSON file
|
|
266
|
-
3. Save it to the correct location:
|
|
267
|
-
```bash
|
|
268
|
-
mkdir -p ~/.pkg-tracker
|
|
269
|
-
mv ~/Downloads/client_secret_*.json ~/.pkg-tracker/credentials.json
|
|
270
|
-
```
|
|
271
|
-
4. Run `pkg init` again
|
|
272
|
-
|
|
273
229
|
### "Gmail not authorized"
|
|
274
230
|
|
|
275
231
|
**Problem:** Error says Gmail not authorized or token missing
|
|
@@ -384,6 +340,23 @@ pkg sync
|
|
|
384
340
|
- Visit: https://github.com/vvanessaww/pkg-track/issues
|
|
385
341
|
- Include: OS, Node.js version, error message, steps to reproduce
|
|
386
342
|
|
|
343
|
+
## Known Issues
|
|
344
|
+
|
|
345
|
+
### npm deprecation warning during installation
|
|
346
|
+
|
|
347
|
+
**What you might see:**
|
|
348
|
+
```
|
|
349
|
+
npm warn deprecated prebuild-install@7.1.3: No longer maintained
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**What it means:**
|
|
353
|
+
- This warning comes from a dependency (`better-sqlite3`) that we use for local database storage
|
|
354
|
+
- It's a cosmetic warning and **does not affect functionality**
|
|
355
|
+
- The package works perfectly fine
|
|
356
|
+
- We're waiting for the upstream maintainer to update their dependencies
|
|
357
|
+
|
|
358
|
+
**Action required:** None. You can safely ignore this warning.
|
|
359
|
+
|
|
387
360
|
## Roadmap
|
|
388
361
|
|
|
389
362
|
- [ ] **Live tracking updates** via AfterShip/TrackingMore API
|
package/package.json
CHANGED
package/src/gmail-old.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
const { google } = require('googleapis');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const open = require('open');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const url = require('url');
|
|
10
|
+
const { extractTrackingNumbers, detectRetailer, extractShippedDate, extractProductDetails } = require('./parser');
|
|
11
|
+
const { upsertPackage } = require('./db');
|
|
12
|
+
|
|
13
|
+
const CONFIG_DIR = path.join(os.homedir(), '.pkg-tracker');
|
|
14
|
+
const TOKEN_PATH = path.join(CONFIG_DIR, 'gmail-token.json');
|
|
15
|
+
const CREDENTIALS_PATH = path.join(CONFIG_DIR, 'credentials.json');
|
|
16
|
+
|
|
17
|
+
const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize Gmail OAuth
|
|
21
|
+
*/
|
|
22
|
+
async function initGmail() {
|
|
23
|
+
console.log(chalk.blue('\n📧 Gmail OAuth Setup\n'));
|
|
24
|
+
console.log(chalk.gray('Secure, read-only access to scan your email for shipping confirmations'));
|
|
25
|
+
console.log(chalk.gray('Your credentials are stored locally - no data leaves your computer\n'));
|
|
26
|
+
|
|
27
|
+
// Check if credentials exist
|
|
28
|
+
if (!fs.existsSync(CREDENTIALS_PATH)) {
|
|
29
|
+
console.log(chalk.yellow('⚠️ Gmail OAuth credentials not found.'));
|
|
30
|
+
console.log('\nTo set up Gmail access:');
|
|
31
|
+
console.log('1. Go to https://console.cloud.google.com/apis/credentials');
|
|
32
|
+
console.log('2. Create OAuth 2.0 Client ID (Desktop app)');
|
|
33
|
+
console.log('3. Download the JSON file');
|
|
34
|
+
console.log(`4. Save it as: ${CREDENTIALS_PATH}\n`);
|
|
35
|
+
|
|
36
|
+
// Ensure directory exists with secure permissions
|
|
37
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
38
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw new Error('Please set up Gmail credentials first');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
45
|
+
|
|
46
|
+
// Handle different credential formats
|
|
47
|
+
let clientInfo;
|
|
48
|
+
if (credentials.installed) {
|
|
49
|
+
clientInfo = credentials.installed;
|
|
50
|
+
} else if (credentials.web) {
|
|
51
|
+
clientInfo = credentials.web;
|
|
52
|
+
} else {
|
|
53
|
+
// Direct format (sometimes happens with newer Google Cloud Console)
|
|
54
|
+
clientInfo = credentials;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { client_secret, client_id, redirect_uris } = clientInfo;
|
|
58
|
+
|
|
59
|
+
if (!client_secret || !client_id || !redirect_uris || !redirect_uris[0]) {
|
|
60
|
+
throw new Error('Invalid credentials.json format. Please download a fresh OAuth client credentials file from Google Cloud Console.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parse the redirect URI to get the port
|
|
64
|
+
const redirectUri = redirect_uris[0];
|
|
65
|
+
const redirectUrl = new URL(redirectUri);
|
|
66
|
+
const port = parseInt(redirectUrl.port) || 3000;
|
|
67
|
+
|
|
68
|
+
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
|
69
|
+
|
|
70
|
+
// Generate auth URL
|
|
71
|
+
const authUrl = oAuth2Client.generateAuthUrl({
|
|
72
|
+
access_type: 'offline',
|
|
73
|
+
scope: SCOPES,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log(chalk.blue('Opening browser for Gmail authorization...'));
|
|
77
|
+
console.log(chalk.gray(`Starting local server on port ${port}...\n`));
|
|
78
|
+
|
|
79
|
+
// Create a promise that resolves when we get the code
|
|
80
|
+
const code = await new Promise((resolve, reject) => {
|
|
81
|
+
const server = http.createServer(async (req, res) => {
|
|
82
|
+
try {
|
|
83
|
+
console.log(chalk.gray(`Received request: ${req.url}`));
|
|
84
|
+
const queryParams = url.parse(req.url, true).query;
|
|
85
|
+
|
|
86
|
+
if (queryParams.code) {
|
|
87
|
+
// Send success page
|
|
88
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
89
|
+
res.end(`
|
|
90
|
+
<!DOCTYPE html>
|
|
91
|
+
<html>
|
|
92
|
+
<head>
|
|
93
|
+
<meta charset="UTF-8">
|
|
94
|
+
<title>Authorization Successful</title>
|
|
95
|
+
<style>
|
|
96
|
+
body {
|
|
97
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
height: 100vh;
|
|
102
|
+
margin: 0;
|
|
103
|
+
background: #f5f5f5;
|
|
104
|
+
}
|
|
105
|
+
.container {
|
|
106
|
+
text-align: center;
|
|
107
|
+
background: white;
|
|
108
|
+
padding: 3rem;
|
|
109
|
+
border-radius: 12px;
|
|
110
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
111
|
+
}
|
|
112
|
+
h1 { color: #22c55e; margin: 0 0 1rem; }
|
|
113
|
+
p { color: #666; margin: 0; }
|
|
114
|
+
</style>
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
<div class="container">
|
|
118
|
+
<h1>✓ Authorization Successful!</h1>
|
|
119
|
+
<p>You can close this window and return to the terminal.</p>
|
|
120
|
+
</div>
|
|
121
|
+
</body>
|
|
122
|
+
</html>
|
|
123
|
+
`);
|
|
124
|
+
|
|
125
|
+
console.log(chalk.green('\n✓ Received authorization code'));
|
|
126
|
+
|
|
127
|
+
// Give time for response to be sent
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
server.close();
|
|
130
|
+
resolve(queryParams.code);
|
|
131
|
+
}, 100);
|
|
132
|
+
} else if (queryParams.error) {
|
|
133
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
134
|
+
res.end(`
|
|
135
|
+
<!DOCTYPE html>
|
|
136
|
+
<html>
|
|
137
|
+
<head>
|
|
138
|
+
<meta charset="UTF-8">
|
|
139
|
+
<title>Authorization Failed</title>
|
|
140
|
+
<style>
|
|
141
|
+
body {
|
|
142
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
justify-content: center;
|
|
146
|
+
height: 100vh;
|
|
147
|
+
margin: 0;
|
|
148
|
+
background: #f5f5f5;
|
|
149
|
+
}
|
|
150
|
+
.container {
|
|
151
|
+
text-align: center;
|
|
152
|
+
background: white;
|
|
153
|
+
padding: 3rem;
|
|
154
|
+
border-radius: 12px;
|
|
155
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
156
|
+
}
|
|
157
|
+
h1 { color: #ef4444; margin: 0 0 1rem; }
|
|
158
|
+
p { color: #666; margin: 0; }
|
|
159
|
+
</style>
|
|
160
|
+
</head>
|
|
161
|
+
<body>
|
|
162
|
+
<div class="container">
|
|
163
|
+
<h1>✗ Authorization Failed</h1>
|
|
164
|
+
<p>Error: ${queryParams.error}</p>
|
|
165
|
+
</div>
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
168
|
+
`);
|
|
169
|
+
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
server.close();
|
|
172
|
+
reject(new Error(`OAuth error: ${queryParams.error}`));
|
|
173
|
+
}, 100);
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
server.close();
|
|
177
|
+
reject(err);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Handle server errors
|
|
182
|
+
server.on('error', (err) => {
|
|
183
|
+
if (err.code === 'EADDRINUSE') {
|
|
184
|
+
console.log(chalk.red(`\n✗ Port ${port} is already in use`));
|
|
185
|
+
console.log(chalk.yellow('Please close any other applications using this port and try again.\n'));
|
|
186
|
+
}
|
|
187
|
+
reject(err);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
server.listen(port, '127.0.0.1', () => {
|
|
191
|
+
console.log(chalk.green(`✓ Server listening on http://127.0.0.1:${port}`));
|
|
192
|
+
console.log(chalk.gray('Opening browser...\n'));
|
|
193
|
+
|
|
194
|
+
// Wait a bit for server to be fully ready, then open browser
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
open(authUrl);
|
|
197
|
+
}, 500);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Timeout after 5 minutes
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
server.close();
|
|
203
|
+
reject(new Error('Authorization timeout - no response received after 5 minutes'));
|
|
204
|
+
}, 5 * 60 * 1000);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const { tokens } = await oAuth2Client.getToken(code);
|
|
208
|
+
oAuth2Client.setCredentials(tokens);
|
|
209
|
+
|
|
210
|
+
// Save token with secure permissions (user read/write only)
|
|
211
|
+
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens), { mode: 0o600 });
|
|
212
|
+
console.log(chalk.green('\n✓ Gmail access granted and saved!'));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get authorized Gmail client
|
|
217
|
+
*/
|
|
218
|
+
function getGmailClient() {
|
|
219
|
+
if (!fs.existsSync(CREDENTIALS_PATH)) {
|
|
220
|
+
throw new Error('Gmail credentials not found. Run `pkg init` first.');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!fs.existsSync(TOKEN_PATH)) {
|
|
224
|
+
throw new Error('Gmail not authorized. Run `pkg init` first.');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
228
|
+
const tokens = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8'));
|
|
229
|
+
|
|
230
|
+
// Handle different credential formats
|
|
231
|
+
let clientInfo;
|
|
232
|
+
if (credentials.installed) {
|
|
233
|
+
clientInfo = credentials.installed;
|
|
234
|
+
} else if (credentials.web) {
|
|
235
|
+
clientInfo = credentials.web;
|
|
236
|
+
} else {
|
|
237
|
+
clientInfo = credentials;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const { client_secret, client_id, redirect_uris } = clientInfo;
|
|
241
|
+
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
|
|
242
|
+
|
|
243
|
+
oAuth2Client.setCredentials(tokens);
|
|
244
|
+
|
|
245
|
+
return google.gmail({ version: 'v1', auth: oAuth2Client });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Sync packages from Gmail
|
|
250
|
+
*/
|
|
251
|
+
async function syncPackages(days = 30) {
|
|
252
|
+
console.log(chalk.blue('\n📧 Gmail Package Scanner\n'));
|
|
253
|
+
console.log(chalk.gray('Securely scanning your Gmail for shipping confirmations...'));
|
|
254
|
+
console.log(chalk.gray(`Searching the last ${days} days for packages from UPS, FedEx, USPS, and Amazon`));
|
|
255
|
+
console.log(chalk.gray('💡 Tip: Use --days <number> to customize the date range (max 30 days)\n'));
|
|
256
|
+
|
|
257
|
+
const spinner = ora('Scanning Gmail for shipping confirmations...').start();
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const gmail = getGmailClient();
|
|
261
|
+
|
|
262
|
+
// Expanded search for shipping-related emails
|
|
263
|
+
// Search both subject and body for common shipping terms
|
|
264
|
+
const query = `(
|
|
265
|
+
shipped OR
|
|
266
|
+
"has shipped" OR
|
|
267
|
+
tracking OR
|
|
268
|
+
delivered OR
|
|
269
|
+
"out for delivery" OR
|
|
270
|
+
"order confirmation" OR
|
|
271
|
+
"shipment confirmation" OR
|
|
272
|
+
"your order" OR
|
|
273
|
+
"on its way" OR
|
|
274
|
+
"package update" OR
|
|
275
|
+
"delivery update" OR
|
|
276
|
+
"1Z" OR
|
|
277
|
+
"TBA" OR
|
|
278
|
+
"fedex" OR
|
|
279
|
+
"usps" OR
|
|
280
|
+
"ups" OR
|
|
281
|
+
"amazon"
|
|
282
|
+
) newer_than:${days}d`.replace(/\s+/g, ' ');
|
|
283
|
+
|
|
284
|
+
console.log(chalk.gray(`\nSearch query: ${query.substring(0, 100)}...`));
|
|
285
|
+
|
|
286
|
+
const response = await gmail.users.messages.list({
|
|
287
|
+
userId: 'me',
|
|
288
|
+
q: query,
|
|
289
|
+
maxResults: 200, // Increased from 100
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const messages = response.data.messages || [];
|
|
293
|
+
|
|
294
|
+
if (messages.length === 0) {
|
|
295
|
+
spinner.succeed(chalk.yellow('No shipping emails found'));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
spinner.text = `Found ${messages.length} emails, extracting tracking numbers...`;
|
|
300
|
+
console.log(chalk.gray(`Processing ${messages.length} emails (last ${days} days)...\n`));
|
|
301
|
+
|
|
302
|
+
let foundCount = 0;
|
|
303
|
+
let emailsWithTracking = 0;
|
|
304
|
+
const skippedEmails = [];
|
|
305
|
+
|
|
306
|
+
for (const message of messages) {
|
|
307
|
+
const msg = await gmail.users.messages.get({
|
|
308
|
+
userId: 'me',
|
|
309
|
+
id: message.id,
|
|
310
|
+
format: 'full',
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Extract headers
|
|
314
|
+
const headers = msg.data.payload.headers;
|
|
315
|
+
const subject = headers.find(h => h.name === 'Subject')?.value || '';
|
|
316
|
+
const from = headers.find(h => h.name === 'From')?.value || '';
|
|
317
|
+
const date = headers.find(h => h.name === 'Date')?.value || '';
|
|
318
|
+
|
|
319
|
+
// Get email body
|
|
320
|
+
let body = '';
|
|
321
|
+
if (msg.data.payload.body.data) {
|
|
322
|
+
body = Buffer.from(msg.data.payload.body.data, 'base64').toString();
|
|
323
|
+
} else if (msg.data.payload.parts) {
|
|
324
|
+
const textPart = msg.data.payload.parts.find(p => p.mimeType === 'text/plain');
|
|
325
|
+
if (textPart && textPart.body.data) {
|
|
326
|
+
body = Buffer.from(textPart.body.data, 'base64').toString();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Extract tracking numbers
|
|
331
|
+
const trackingNumbers = extractTrackingNumbers(subject + ' ' + body);
|
|
332
|
+
|
|
333
|
+
if (trackingNumbers.length > 0) {
|
|
334
|
+
emailsWithTracking++;
|
|
335
|
+
const emailData = { from, subject, body, date };
|
|
336
|
+
const retailer = detectRetailer(emailData);
|
|
337
|
+
const shippedDate = extractShippedDate(emailData);
|
|
338
|
+
const productDetails = extractProductDetails(emailData);
|
|
339
|
+
|
|
340
|
+
// Parse email date to timestamp
|
|
341
|
+
let emailTimestamp = null;
|
|
342
|
+
try {
|
|
343
|
+
if (date) {
|
|
344
|
+
emailTimestamp = Math.floor(new Date(date).getTime() / 1000);
|
|
345
|
+
}
|
|
346
|
+
} catch (e) {
|
|
347
|
+
// Use current time if parsing fails
|
|
348
|
+
emailTimestamp = Math.floor(Date.now() / 1000);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const { trackingNumber, carrier } of trackingNumbers) {
|
|
352
|
+
upsertPackage({
|
|
353
|
+
trackingNumber,
|
|
354
|
+
carrier,
|
|
355
|
+
retailer,
|
|
356
|
+
description: productDetails,
|
|
357
|
+
emailId: message.id,
|
|
358
|
+
shippedDate,
|
|
359
|
+
lastEmailDate: emailTimestamp,
|
|
360
|
+
});
|
|
361
|
+
foundCount++;
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
// Track emails without tracking numbers for debugging
|
|
365
|
+
skippedEmails.push({ subject, from });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
spinner.succeed(chalk.green(`✓ Found ${foundCount} tracking numbers from ${emailsWithTracking}/${messages.length} emails`));
|
|
370
|
+
|
|
371
|
+
if (foundCount > 0) {
|
|
372
|
+
console.log(chalk.gray(`\n💡 Run ${chalk.cyan('pkg open')} to track your packages\n`));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Show sample of skipped emails if verbose
|
|
376
|
+
if (skippedEmails.length > 0 && skippedEmails.length < 10) {
|
|
377
|
+
console.log(chalk.yellow(`⚠️ ${skippedEmails.length} emails had no tracking numbers:`));
|
|
378
|
+
skippedEmails.forEach(({ subject, from }) => {
|
|
379
|
+
console.log(chalk.gray(` - ${subject.substring(0, 60)}...`));
|
|
380
|
+
console.log(chalk.gray(` from: ${from.substring(0, 40)}`));
|
|
381
|
+
});
|
|
382
|
+
console.log();
|
|
383
|
+
} else if (skippedEmails.length >= 10) {
|
|
384
|
+
console.log(chalk.gray(`${skippedEmails.length} emails had no tracking numbers (likely order confirmations)\n`));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
} catch (error) {
|
|
388
|
+
spinner.fail(chalk.red('Failed to sync'));
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
module.exports = {
|
|
394
|
+
initGmail,
|
|
395
|
+
syncPackages,
|
|
396
|
+
getGmailClient,
|
|
397
|
+
};
|
package/src/gmail.js
CHANGED
|
@@ -7,67 +7,57 @@ const chalk = require('chalk');
|
|
|
7
7
|
const ora = require('ora');
|
|
8
8
|
const http = require('http');
|
|
9
9
|
const url = require('url');
|
|
10
|
+
const net = require('net');
|
|
10
11
|
const { extractTrackingNumbers, detectRetailer, extractShippedDate, extractProductDetails } = require('./parser');
|
|
11
12
|
const { upsertPackage } = require('./db');
|
|
12
13
|
|
|
13
14
|
const CONFIG_DIR = path.join(os.homedir(), '.pkg-tracker');
|
|
14
15
|
const TOKEN_PATH = path.join(CONFIG_DIR, 'gmail-token.json');
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
// Web-based OAuth URL
|
|
18
|
+
const AUTH_WEB_URL = process.env.PKG_TRACK_AUTH_URL || 'https://pkg-track-auth.vercel.app';
|
|
16
19
|
|
|
17
20
|
const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'];
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
|
-
*
|
|
23
|
+
* Find an available port
|
|
24
|
+
*/
|
|
25
|
+
async function getAvailablePort() {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const server = net.createServer();
|
|
28
|
+
server.listen(0, '127.0.0.1', () => {
|
|
29
|
+
const port = server.address().port;
|
|
30
|
+
server.close(() => resolve(port));
|
|
31
|
+
});
|
|
32
|
+
server.on('error', reject);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Initialize Gmail OAuth using web-based flow
|
|
21
38
|
*/
|
|
22
39
|
async function initGmail() {
|
|
23
40
|
console.log(chalk.blue('\n📧 Gmail OAuth Setup\n'));
|
|
24
41
|
console.log(chalk.gray('Secure, read-only access to scan your email for shipping confirmations'));
|
|
25
42
|
console.log(chalk.gray('Your credentials are stored locally - no data leaves your computer\n'));
|
|
26
43
|
|
|
27
|
-
//
|
|
28
|
-
if (!fs.existsSync(
|
|
29
|
-
|
|
30
|
-
console.log('\nTo set up Gmail access:');
|
|
31
|
-
console.log('1. Go to https://console.cloud.google.com/apis/credentials');
|
|
32
|
-
console.log('2. Create OAuth 2.0 Client ID (Desktop app)');
|
|
33
|
-
console.log('3. Download the JSON file');
|
|
34
|
-
console.log(`4. Save it as: ${CREDENTIALS_PATH}\n`);
|
|
35
|
-
|
|
36
|
-
// Ensure directory exists with secure permissions
|
|
37
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
38
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
throw new Error('Please set up Gmail credentials first');
|
|
44
|
+
// Ensure config directory exists with secure permissions
|
|
45
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
46
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
// Parse the redirect URI to get the port
|
|
48
|
-
const redirectUri = redirect_uris[0];
|
|
49
|
-
const redirectUrl = new URL(redirectUri);
|
|
50
|
-
const port = parseInt(redirectUrl.port) || 3000;
|
|
51
|
-
|
|
52
|
-
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
|
53
|
-
|
|
54
|
-
// Generate auth URL
|
|
55
|
-
const authUrl = oAuth2Client.generateAuthUrl({
|
|
56
|
-
access_type: 'offline',
|
|
57
|
-
scope: SCOPES,
|
|
58
|
-
});
|
|
49
|
+
// Start local callback server
|
|
50
|
+
const port = await getAvailablePort();
|
|
51
|
+
console.log(chalk.green(`✓ Starting local callback server on port ${port}`));
|
|
59
52
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Create a promise that resolves when we get the code
|
|
64
|
-
const code = await new Promise((resolve, reject) => {
|
|
65
|
-
const server = http.createServer(async (req, res) => {
|
|
53
|
+
const tokens = await new Promise((resolve, reject) => {
|
|
54
|
+
const server = http.createServer((req, res) => {
|
|
66
55
|
try {
|
|
67
|
-
console.log(chalk.gray(`Received request: ${req.url}`));
|
|
68
56
|
const queryParams = url.parse(req.url, true).query;
|
|
69
57
|
|
|
70
|
-
if (queryParams.
|
|
58
|
+
if (queryParams.token) {
|
|
59
|
+
const tokens = JSON.parse(queryParams.token);
|
|
60
|
+
|
|
71
61
|
// Send success page
|
|
72
62
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
73
63
|
res.end(`
|
|
@@ -99,57 +89,22 @@ async function initGmail() {
|
|
|
99
89
|
</head>
|
|
100
90
|
<body>
|
|
101
91
|
<div class="container">
|
|
102
|
-
<h1>✓ Authorization
|
|
92
|
+
<h1>✓ Authorization Complete!</h1>
|
|
103
93
|
<p>You can close this window and return to the terminal.</p>
|
|
104
94
|
</div>
|
|
105
95
|
</body>
|
|
106
96
|
</html>
|
|
107
97
|
`);
|
|
108
98
|
|
|
109
|
-
console.log(chalk.green('\n✓ Received authorization
|
|
99
|
+
console.log(chalk.green('\n✓ Received authorization token'));
|
|
110
100
|
|
|
111
|
-
// Give time for response to be sent
|
|
112
101
|
setTimeout(() => {
|
|
113
102
|
server.close();
|
|
114
|
-
resolve(
|
|
103
|
+
resolve(tokens);
|
|
115
104
|
}, 100);
|
|
116
105
|
} else if (queryParams.error) {
|
|
117
|
-
res.writeHead(400, { 'Content-Type': 'text/
|
|
118
|
-
res.end(
|
|
119
|
-
<!DOCTYPE html>
|
|
120
|
-
<html>
|
|
121
|
-
<head>
|
|
122
|
-
<meta charset="UTF-8">
|
|
123
|
-
<title>Authorization Failed</title>
|
|
124
|
-
<style>
|
|
125
|
-
body {
|
|
126
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
127
|
-
display: flex;
|
|
128
|
-
align-items: center;
|
|
129
|
-
justify-content: center;
|
|
130
|
-
height: 100vh;
|
|
131
|
-
margin: 0;
|
|
132
|
-
background: #f5f5f5;
|
|
133
|
-
}
|
|
134
|
-
.container {
|
|
135
|
-
text-align: center;
|
|
136
|
-
background: white;
|
|
137
|
-
padding: 3rem;
|
|
138
|
-
border-radius: 12px;
|
|
139
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
140
|
-
}
|
|
141
|
-
h1 { color: #ef4444; margin: 0 0 1rem; }
|
|
142
|
-
p { color: #666; margin: 0; }
|
|
143
|
-
</style>
|
|
144
|
-
</head>
|
|
145
|
-
<body>
|
|
146
|
-
<div class="container">
|
|
147
|
-
<h1>✗ Authorization Failed</h1>
|
|
148
|
-
<p>Error: ${queryParams.error}</p>
|
|
149
|
-
</div>
|
|
150
|
-
</body>
|
|
151
|
-
</html>
|
|
152
|
-
`);
|
|
106
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
107
|
+
res.end('Authorization failed');
|
|
153
108
|
|
|
154
109
|
setTimeout(() => {
|
|
155
110
|
server.close();
|
|
@@ -162,20 +117,20 @@ async function initGmail() {
|
|
|
162
117
|
}
|
|
163
118
|
});
|
|
164
119
|
|
|
165
|
-
// Handle server errors
|
|
166
120
|
server.on('error', (err) => {
|
|
167
121
|
if (err.code === 'EADDRINUSE') {
|
|
168
122
|
console.log(chalk.red(`\n✗ Port ${port} is already in use`));
|
|
169
|
-
console.log(chalk.yellow('Please
|
|
123
|
+
console.log(chalk.yellow('Please try again in a moment.\n'));
|
|
170
124
|
}
|
|
171
125
|
reject(err);
|
|
172
126
|
});
|
|
173
127
|
|
|
174
128
|
server.listen(port, '127.0.0.1', () => {
|
|
175
|
-
console.log(chalk.
|
|
176
|
-
|
|
129
|
+
console.log(chalk.blue('Opening browser for Gmail authorization...\n'));
|
|
130
|
+
|
|
131
|
+
// Open web app with callback port
|
|
132
|
+
const authUrl = `${AUTH_WEB_URL}/?port=${port}`;
|
|
177
133
|
|
|
178
|
-
// Wait a bit for server to be fully ready, then open browser
|
|
179
134
|
setTimeout(() => {
|
|
180
135
|
open(authUrl);
|
|
181
136
|
}, 500);
|
|
@@ -188,10 +143,7 @@ async function initGmail() {
|
|
|
188
143
|
}, 5 * 60 * 1000);
|
|
189
144
|
});
|
|
190
145
|
|
|
191
|
-
|
|
192
|
-
oAuth2Client.setCredentials(tokens);
|
|
193
|
-
|
|
194
|
-
// Save token with secure permissions (user read/write only)
|
|
146
|
+
// Save tokens with secure permissions
|
|
195
147
|
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens), { mode: 0o600 });
|
|
196
148
|
console.log(chalk.green('\n✓ Gmail access granted and saved!'));
|
|
197
149
|
}
|
|
@@ -200,20 +152,15 @@ async function initGmail() {
|
|
|
200
152
|
* Get authorized Gmail client
|
|
201
153
|
*/
|
|
202
154
|
function getGmailClient() {
|
|
203
|
-
if (!fs.existsSync(CREDENTIALS_PATH)) {
|
|
204
|
-
throw new Error('Gmail credentials not found. Run `pkg init` first.');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
155
|
if (!fs.existsSync(TOKEN_PATH)) {
|
|
208
156
|
throw new Error('Gmail not authorized. Run `pkg init` first.');
|
|
209
157
|
}
|
|
210
158
|
|
|
211
|
-
const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
212
159
|
const tokens = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8'));
|
|
213
160
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
161
|
+
// Create a temporary OAuth client just for making API calls
|
|
162
|
+
// We don't need client_id/secret after we have the tokens
|
|
163
|
+
const oAuth2Client = new google.auth.OAuth2();
|
|
217
164
|
oAuth2Client.setCredentials(tokens);
|
|
218
165
|
|
|
219
166
|
return google.gmail({ version: 'v1', auth: oAuth2Client });
|
|
@@ -234,7 +181,6 @@ async function syncPackages(days = 30) {
|
|
|
234
181
|
const gmail = getGmailClient();
|
|
235
182
|
|
|
236
183
|
// Expanded search for shipping-related emails
|
|
237
|
-
// Search both subject and body for common shipping terms
|
|
238
184
|
const query = `(
|
|
239
185
|
shipped OR
|
|
240
186
|
"has shipped" OR
|
|
@@ -255,12 +201,10 @@ async function syncPackages(days = 30) {
|
|
|
255
201
|
"amazon"
|
|
256
202
|
) newer_than:${days}d`.replace(/\s+/g, ' ');
|
|
257
203
|
|
|
258
|
-
console.log(chalk.gray(`\nSearch query: ${query.substring(0, 100)}...`));
|
|
259
|
-
|
|
260
204
|
const response = await gmail.users.messages.list({
|
|
261
205
|
userId: 'me',
|
|
262
206
|
q: query,
|
|
263
|
-
maxResults: 200,
|
|
207
|
+
maxResults: 200,
|
|
264
208
|
});
|
|
265
209
|
|
|
266
210
|
const messages = response.data.messages || [];
|
|
@@ -271,7 +215,6 @@ async function syncPackages(days = 30) {
|
|
|
271
215
|
}
|
|
272
216
|
|
|
273
217
|
spinner.text = `Found ${messages.length} emails, extracting tracking numbers...`;
|
|
274
|
-
console.log(chalk.gray(`Processing ${messages.length} emails (last ${days} days)...\n`));
|
|
275
218
|
|
|
276
219
|
let foundCount = 0;
|
|
277
220
|
let emailsWithTracking = 0;
|
|
@@ -318,7 +261,6 @@ async function syncPackages(days = 30) {
|
|
|
318
261
|
emailTimestamp = Math.floor(new Date(date).getTime() / 1000);
|
|
319
262
|
}
|
|
320
263
|
} catch (e) {
|
|
321
|
-
// Use current time if parsing fails
|
|
322
264
|
emailTimestamp = Math.floor(Date.now() / 1000);
|
|
323
265
|
}
|
|
324
266
|
|
|
@@ -335,7 +277,6 @@ async function syncPackages(days = 30) {
|
|
|
335
277
|
foundCount++;
|
|
336
278
|
}
|
|
337
279
|
} else {
|
|
338
|
-
// Track emails without tracking numbers for debugging
|
|
339
280
|
skippedEmails.push({ subject, from });
|
|
340
281
|
}
|
|
341
282
|
}
|
|
@@ -346,7 +287,6 @@ async function syncPackages(days = 30) {
|
|
|
346
287
|
console.log(chalk.gray(`\n💡 Run ${chalk.cyan('pkg open')} to track your packages\n`));
|
|
347
288
|
}
|
|
348
289
|
|
|
349
|
-
// Show sample of skipped emails if verbose
|
|
350
290
|
if (skippedEmails.length > 0 && skippedEmails.length < 10) {
|
|
351
291
|
console.log(chalk.yellow(`⚠️ ${skippedEmails.length} emails had no tracking numbers:`));
|
|
352
292
|
skippedEmails.forEach(({ subject, from }) => {
|