stellamail 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/LICENSE +21 -0
- package/README.md +139 -0
- package/bin/stellamail.js +10 -0
- package/config-example.json +76 -0
- package/package.json +44 -0
- package/src/cli.js +216 -0
- package/src/commands/accounts.js +64 -0
- package/src/commands/delete.js +78 -0
- package/src/commands/flag.js +95 -0
- package/src/commands/folders.js +84 -0
- package/src/commands/forward.js +237 -0
- package/src/commands/inbox.js +135 -0
- package/src/commands/init.js +250 -0
- package/src/commands/log.js +70 -0
- package/src/commands/move.js +67 -0
- package/src/commands/read.js +125 -0
- package/src/commands/reply.js +254 -0
- package/src/commands/search.js +136 -0
- package/src/commands/send.js +232 -0
- package/src/commands/templates.js +94 -0
- package/src/commands/test.js +101 -0
- package/src/config.js +70 -0
- package/src/format.js +51 -0
- package/src/imap.js +34 -0
- package/src/logger.js +35 -0
- package/src/safety.js +99 -0
- package/src/signature.js +18 -0
- package/src/smtp.js +21 -0
- package/src/template.js +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Smart Choice Inspections LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# โ๏ธ Stellamail
|
|
2
|
+
|
|
3
|
+
**Safe email CLI for AI agents and humans.**
|
|
4
|
+
|
|
5
|
+
Built with safety rails that prevent the disasters AI agents cause with email โ duplicate sends, missing attachments, forgotten CCs. Works great as a standalone terminal email client too.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g stellamail
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Interactive setup wizard
|
|
17
|
+
stellamail init
|
|
18
|
+
|
|
19
|
+
# Check your inbox
|
|
20
|
+
stellamail inbox --account work
|
|
21
|
+
|
|
22
|
+
# Send an email
|
|
23
|
+
stellamail send --account work \
|
|
24
|
+
--to "client@example.com" \
|
|
25
|
+
--cc "boss@example.com" \
|
|
26
|
+
--subject "Report attached" \
|
|
27
|
+
--body "Hi, please find your report attached." \
|
|
28
|
+
--attachment "/path/to/report.pdf"
|
|
29
|
+
|
|
30
|
+
# Read a message
|
|
31
|
+
stellamail read 42 --account work
|
|
32
|
+
|
|
33
|
+
# Reply
|
|
34
|
+
stellamail reply 42 --account work --body "Thanks for the update!"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
| Command | Description |
|
|
40
|
+
|---------|-------------|
|
|
41
|
+
| `send` | Send an email |
|
|
42
|
+
| `reply <id>` | Reply to an email |
|
|
43
|
+
| `forward <id>` | Forward an email |
|
|
44
|
+
| `inbox` | List inbox messages |
|
|
45
|
+
| `read <id>` | Read a single email |
|
|
46
|
+
| `search` | Search emails |
|
|
47
|
+
| `folders` | List mailbox folders |
|
|
48
|
+
| `move <id>` | Move a message to another folder |
|
|
49
|
+
| `delete <id>` | Delete a message |
|
|
50
|
+
| `flag <id>` | Toggle flags on a message |
|
|
51
|
+
| `log` | View send log |
|
|
52
|
+
| `test` | Test SMTP/IMAP connections |
|
|
53
|
+
| `accounts` | List configured accounts |
|
|
54
|
+
| `templates` | List available templates |
|
|
55
|
+
| `init` | Interactive setup wizard |
|
|
56
|
+
|
|
57
|
+
## Safety Features
|
|
58
|
+
|
|
59
|
+
These aren't optional โ they're the whole point.
|
|
60
|
+
|
|
61
|
+
- ๐ **Duplicate blocking** โ Same email within 24h? Blocked. Configurable cooldown.
|
|
62
|
+
- โ
**Attachment validation** โ PDF header check, file exists, size limits.
|
|
63
|
+
- ๐ **CC enforcement** โ Per-account. Can't "forget" to CC the boss.
|
|
64
|
+
- ๐ **Send logging** โ Every send logged with timestamp, recipient, status.
|
|
65
|
+
- ๐ซ **One attempt only** โ No retries. Ever. Fail = report and stop.
|
|
66
|
+
- ๐งช **Dry run** โ Preview everything before sending with `--dry-run`.
|
|
67
|
+
|
|
68
|
+
## Templates
|
|
69
|
+
|
|
70
|
+
Define reusable email templates in your config:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
stellamail send --account work \
|
|
74
|
+
--template report-delivery \
|
|
75
|
+
--var firstName=Sarah \
|
|
76
|
+
--var reportType=Radon \
|
|
77
|
+
--var address="123 Main St" \
|
|
78
|
+
--attachment report.pdf
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Signatures
|
|
82
|
+
|
|
83
|
+
Per-account signatures with template variables:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"signature": {
|
|
88
|
+
"text": "\n--\n{{name}}\n{{title}}\n{{company}}",
|
|
89
|
+
"vars": {
|
|
90
|
+
"name": "Jane Smith",
|
|
91
|
+
"title": "Inspector",
|
|
92
|
+
"company": "Acme Inspections"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Skip with `--no-signature`.
|
|
99
|
+
|
|
100
|
+
## JSON Output
|
|
101
|
+
|
|
102
|
+
Every command supports `--output json` for scripting and AI agent integration:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
stellamail inbox --account work --output json
|
|
106
|
+
stellamail accounts --output json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
110
|
+
|
|
111
|
+
Run `stellamail init` for interactive setup, or create `~/.stellamail.config.json` manually. See [config-example.json](config-example.json) for all options.
|
|
112
|
+
|
|
113
|
+
**Security:**
|
|
114
|
+
- Config file auto-set to `chmod 600`
|
|
115
|
+
- Warning if permissions are too open
|
|
116
|
+
- Environment variable override: `STELLAMAIL_ACCOUNTNAME_PASS`
|
|
117
|
+
|
|
118
|
+
## Why Stellamail?
|
|
119
|
+
|
|
120
|
+
An AI agent sent 15 duplicate emails to a client at midnight. The home inspector woke up to an inbox full of angry replies. That's why.
|
|
121
|
+
|
|
122
|
+
Stellamail was born out of pure frustration โ built by [Leo Betancor](https://makeasmartchoice.us), a New Jersey home inspector who just wanted his AI assistant to send one email. ONE. Instead, it sent fifteen. So he built an email tool his bots literally can't break.
|
|
123
|
+
|
|
124
|
+
Duplicate blocking, attachment validation, and CC enforcement aren't features โ they're therapy.
|
|
125
|
+
|
|
126
|
+
But it's also just a really nice terminal email client for humans. Color-coded inbox, clean formatting, fast IMAP. No Electron, no bloat, no nonsense.
|
|
127
|
+
|
|
128
|
+
## Tech
|
|
129
|
+
|
|
130
|
+
- **SMTP:** nodemailer
|
|
131
|
+
- **IMAP:** imapflow
|
|
132
|
+
- **Parsing:** mailparser
|
|
133
|
+
- **UI:** chalk + cli-table3
|
|
134
|
+
- **Config:** JSON (no database)
|
|
135
|
+
- **Runtime:** Node.js (CommonJS)
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Stellamail config โ copy to stellamail.config.json and fill in your details",
|
|
3
|
+
|
|
4
|
+
"smtp": {
|
|
5
|
+
"host": "smtp.example.com",
|
|
6
|
+
"port": 465,
|
|
7
|
+
"secure": true,
|
|
8
|
+
"rejectUnauthorized": false
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
"accounts": {
|
|
12
|
+
"work": {
|
|
13
|
+
"from": "\"Your Name\" <you@example.com>",
|
|
14
|
+
"smtp": {
|
|
15
|
+
"user": "you@example.com",
|
|
16
|
+
"pass": "your-password-here"
|
|
17
|
+
},
|
|
18
|
+
"requireCc": true,
|
|
19
|
+
"signature": {
|
|
20
|
+
"text": "\n--\n{{name}}\n{{title}}\n{{company}}\nPhone: {{phone}}",
|
|
21
|
+
"html": "<br><hr><b>{{name}}</b><br>{{title}}<br>{{company}}<br>Phone: {{phone}}",
|
|
22
|
+
"vars": {
|
|
23
|
+
"name": "Your Name",
|
|
24
|
+
"title": "Your Title",
|
|
25
|
+
"company": "Your Company",
|
|
26
|
+
"phone": "(555) 123-4567"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"support": {
|
|
31
|
+
"from": "\"Support\" <support@example.com>",
|
|
32
|
+
"smtp": {
|
|
33
|
+
"user": "support@example.com",
|
|
34
|
+
"pass": "your-password-here"
|
|
35
|
+
},
|
|
36
|
+
"requireCc": false
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
"signature": {
|
|
41
|
+
"_comment": "Global fallback signature (used when account has no signature)",
|
|
42
|
+
"text": "\n--\nSent via Stellamail",
|
|
43
|
+
"html": "<br><hr><small>Sent via Stellamail</small>"
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
"templates": {
|
|
47
|
+
"report-delivery": {
|
|
48
|
+
"subject": "{{reportType}} Report - {{address}}",
|
|
49
|
+
"body": "Hi {{firstName}},\n\nPlease find attached your {{reportType}} Report for {{address}}.\n\nPlease let us know if you have any questions.",
|
|
50
|
+
"cc": "inspector@example.com"
|
|
51
|
+
},
|
|
52
|
+
"invoice": {
|
|
53
|
+
"subject": "Invoice - {{invoiceNumber}}",
|
|
54
|
+
"body": "Hi {{firstName}},\n\nPlease find your invoice #{{invoiceNumber}} attached.\n\nPayment is due within 30 days.\n\nThank you for your business."
|
|
55
|
+
},
|
|
56
|
+
"follow-up": {
|
|
57
|
+
"subject": "Following up - {{topic}}",
|
|
58
|
+
"body": "Hi {{firstName}},\n\nJust following up regarding {{topic}}.\n\nPlease let me know if you have any questions."
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
"duplicateCheck": {
|
|
63
|
+
"cooldownSeconds": 86400,
|
|
64
|
+
"lockDir": "~/.stellamail-locks"
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
"attachments": {
|
|
68
|
+
"maxSizeMB": 25,
|
|
69
|
+
"validatePdf": true
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
"logging": {
|
|
73
|
+
"file": "~/.stellamail-log.json",
|
|
74
|
+
"maxEntries": 500
|
|
75
|
+
}
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stellamail",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Safe email CLI for AI agents and humans. Built by a fed-up home inspector whose AI assistant sent 15 duplicate emails to a client. Never again. Duplicate blocking, attachment validation, CC enforcement, templates, and a color-coded terminal UI.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"stellamail": "./bin/stellamail.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"email",
|
|
10
|
+
"cli",
|
|
11
|
+
"imap",
|
|
12
|
+
"smtp",
|
|
13
|
+
"mail",
|
|
14
|
+
"terminal",
|
|
15
|
+
"ai-agent",
|
|
16
|
+
"openclaw",
|
|
17
|
+
"nodemailer",
|
|
18
|
+
"safe-email"
|
|
19
|
+
],
|
|
20
|
+
"author": "Smart Choice Inspections",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/smartchoiceinspections/stellamail"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/smartchoiceinspections/stellamail#readme",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=16.0.0"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin/",
|
|
32
|
+
"src/",
|
|
33
|
+
"config-example.json",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"nodemailer": "^6.9.0",
|
|
39
|
+
"imapflow": "^1.0.0",
|
|
40
|
+
"mailparser": "^3.6.0",
|
|
41
|
+
"chalk": "^4.1.2",
|
|
42
|
+
"cli-table3": "^0.6.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { findConfig } = require('./config');
|
|
4
|
+
const { chalk } = require('./format');
|
|
5
|
+
const Table = require('cli-table3');
|
|
6
|
+
const { version } = require('../package.json');
|
|
7
|
+
|
|
8
|
+
const COMMANDS = {
|
|
9
|
+
send: './commands/send',
|
|
10
|
+
inbox: './commands/inbox',
|
|
11
|
+
read: './commands/read',
|
|
12
|
+
search: './commands/search',
|
|
13
|
+
folders: './commands/folders',
|
|
14
|
+
log: './commands/log',
|
|
15
|
+
test: './commands/test',
|
|
16
|
+
accounts: './commands/accounts',
|
|
17
|
+
templates: './commands/templates',
|
|
18
|
+
reply: './commands/reply',
|
|
19
|
+
forward: './commands/forward',
|
|
20
|
+
move: './commands/move',
|
|
21
|
+
delete: './commands/delete',
|
|
22
|
+
flag: './commands/flag'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ALL_COMMANDS = Object.keys(COMMANDS).concat('init', 'help', 'version');
|
|
26
|
+
|
|
27
|
+
function levenshtein(a, b) {
|
|
28
|
+
const m = a.length, n = b.length;
|
|
29
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
30
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
31
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
32
|
+
for (let i = 1; i <= m; i++) {
|
|
33
|
+
for (let j = 1; j <= n; j++) {
|
|
34
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
35
|
+
? dp[i - 1][j - 1]
|
|
36
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return dp[m][n];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function suggestCommand(input) {
|
|
43
|
+
let best = null;
|
|
44
|
+
let bestDist = Infinity;
|
|
45
|
+
for (const cmd of ALL_COMMANDS) {
|
|
46
|
+
const dist = levenshtein(input.toLowerCase(), cmd);
|
|
47
|
+
if (dist < bestDist) {
|
|
48
|
+
bestDist = dist;
|
|
49
|
+
best = cmd;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return bestDist <= 3 ? best : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseArgs(args) {
|
|
56
|
+
const opts = { attachments: [], _positional: [] };
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < args.length; i++) {
|
|
59
|
+
if (!args[i].startsWith('--')) {
|
|
60
|
+
opts._positional.push(args[i]);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const key = args[i].slice(2);
|
|
64
|
+
|
|
65
|
+
// Boolean flags
|
|
66
|
+
if (['dry-run', 'no-signature', 'html', 'no-duplicate-check', 'help',
|
|
67
|
+
'unread', 'has-attachment', 'raw', 'all', 'permanent', 'version'].includes(key)) {
|
|
68
|
+
opts[key] = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const val = args[i + 1];
|
|
73
|
+
i++;
|
|
74
|
+
|
|
75
|
+
if (key === 'attachment') {
|
|
76
|
+
opts.attachments.push(val);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (key === 'var') {
|
|
81
|
+
if (!opts.vars) opts.vars = {};
|
|
82
|
+
const eq = val.indexOf('=');
|
|
83
|
+
if (eq > 0) {
|
|
84
|
+
opts.vars[val.slice(0, eq)] = val.slice(eq + 1);
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
opts[key] = val;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return opts;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function showBanner() {
|
|
96
|
+
const banner = new Table({
|
|
97
|
+
colWidths: [43],
|
|
98
|
+
style: { head: [], border: ['cyan'] },
|
|
99
|
+
chars: {
|
|
100
|
+
'top': 'โ', 'top-mid': '', 'top-left': 'โ', 'top-right': 'โ',
|
|
101
|
+
'bottom': 'โ', 'bottom-mid': '', 'bottom-left': 'โ', 'bottom-right': 'โ',
|
|
102
|
+
'left': 'โ', 'left-mid': '', 'mid': '', 'mid-mid': '',
|
|
103
|
+
'right': 'โ', 'right-mid': '', 'middle': ''
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
banner.push(
|
|
107
|
+
[{ content: chalk.bold(` โ๏ธ Stellamail v${version}`), hAlign: 'center' }],
|
|
108
|
+
[{ content: chalk.white(' Safe email CLI for AI agents & humans'), hAlign: 'center' }]
|
|
109
|
+
);
|
|
110
|
+
console.log(banner.toString());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function showHelp(commandName) {
|
|
114
|
+
if (commandName === 'init') {
|
|
115
|
+
const init = require('./commands/init');
|
|
116
|
+
console.log(init.help);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (commandName && COMMANDS[commandName]) {
|
|
120
|
+
const cmd = require(COMMANDS[commandName]);
|
|
121
|
+
console.log(cmd.help || `No help available for "${commandName}".`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
showBanner();
|
|
126
|
+
|
|
127
|
+
console.log(`
|
|
128
|
+
${chalk.yellow.bold('USAGE:')}
|
|
129
|
+
stellamail <command> [options]
|
|
130
|
+
|
|
131
|
+
${chalk.yellow.bold('COMMANDS:')}
|
|
132
|
+
${chalk.cyan.bold('send')} Send an email
|
|
133
|
+
${chalk.cyan.bold('reply')} ${chalk.red('<id>')} Reply to an email
|
|
134
|
+
${chalk.cyan.bold('forward')} ${chalk.red('<id>')} Forward an email
|
|
135
|
+
${chalk.cyan.bold('inbox')} List inbox messages (IMAP)
|
|
136
|
+
${chalk.cyan.bold('read')} ${chalk.red('<id>')} Read a single email (IMAP)
|
|
137
|
+
${chalk.cyan.bold('search')} Search emails (IMAP)
|
|
138
|
+
${chalk.cyan.bold('folders')} List mailbox folders (IMAP)
|
|
139
|
+
${chalk.cyan.bold('move')} ${chalk.red('<id>')} Move a message to another folder
|
|
140
|
+
${chalk.cyan.bold('delete')} ${chalk.red('<id>')} Delete a message
|
|
141
|
+
${chalk.cyan.bold('flag')} ${chalk.red('<id>')} Toggle flags on a message
|
|
142
|
+
${chalk.cyan.bold('log')} View send log
|
|
143
|
+
${chalk.cyan.bold('test')} Test SMTP/IMAP connections
|
|
144
|
+
${chalk.cyan.bold('accounts')} List configured accounts
|
|
145
|
+
${chalk.cyan.bold('templates')} List available templates
|
|
146
|
+
${chalk.cyan.bold('init')} Interactive setup wizard
|
|
147
|
+
${chalk.cyan.bold('help')} ${chalk.gray('[cmd]')} Show help for a command
|
|
148
|
+
|
|
149
|
+
${chalk.yellow.bold('GLOBAL OPTIONS:')}
|
|
150
|
+
${chalk.gray('--config <path>')} Path to stellamail.config.json
|
|
151
|
+
${chalk.gray('--output json')} Output as JSON (clean, no formatting)
|
|
152
|
+
${chalk.gray('--version')} Show version number
|
|
153
|
+
|
|
154
|
+
${chalk.yellow.bold('EXAMPLES:')}
|
|
155
|
+
${chalk.green('stellamail send --account work --to user@example.com --subject "Hello" --body "Hi there"')}
|
|
156
|
+
${chalk.green('stellamail reply 42 --account work --body "Thanks!"')}
|
|
157
|
+
${chalk.green('stellamail forward 42 --account work --to colleague@example.com')}
|
|
158
|
+
${chalk.green('stellamail move 42 --account work --to Archive')}
|
|
159
|
+
${chalk.green('stellamail delete 42 --account work')}
|
|
160
|
+
${chalk.green('stellamail flag 42 --account work --add flagged')}
|
|
161
|
+
${chalk.green('stellamail inbox --account work --unread')}
|
|
162
|
+
${chalk.green('stellamail accounts --output json')}
|
|
163
|
+
`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function run() {
|
|
167
|
+
const args = process.argv.slice(2);
|
|
168
|
+
|
|
169
|
+
// Handle --version anywhere in args
|
|
170
|
+
if (args.includes('--version') || (args.length === 1 && args[0] === 'version')) {
|
|
171
|
+
console.log(`stellamail v${version}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (args.length === 0 || (args.length === 1 && args[0] === '--help')) {
|
|
176
|
+
showHelp();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const command = args[0];
|
|
181
|
+
|
|
182
|
+
// Handle help
|
|
183
|
+
if (command === 'help') {
|
|
184
|
+
showHelp(args[1]);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Handle init (no config needed)
|
|
189
|
+
if (command === 'init') {
|
|
190
|
+
const init = require('./commands/init');
|
|
191
|
+
await init.run();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!COMMANDS[command]) {
|
|
196
|
+
const suggestion = suggestCommand(command);
|
|
197
|
+
if (suggestion) {
|
|
198
|
+
console.error(chalk.red(`โ Unknown command "${command}".`) + ` Did you mean ${chalk.cyan.bold('"' + suggestion + '"')}?`);
|
|
199
|
+
} else {
|
|
200
|
+
console.error(chalk.red(`โ Unknown command: "${command}"`));
|
|
201
|
+
}
|
|
202
|
+
console.error(chalk.gray(' Run "stellamail help" for available commands.'));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const opts = parseArgs(args.slice(1));
|
|
207
|
+
|
|
208
|
+
// Load config
|
|
209
|
+
const { config } = findConfig(opts.config);
|
|
210
|
+
|
|
211
|
+
// Run command
|
|
212
|
+
const cmd = require(COMMANDS[command]);
|
|
213
|
+
await cmd.run(opts, config);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { run };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { makeTable, chalk } = require('../format');
|
|
4
|
+
|
|
5
|
+
async function run(opts, config) {
|
|
6
|
+
const accounts = config.accounts ? Object.keys(config.accounts) : [];
|
|
7
|
+
const isJson = opts.output === 'json';
|
|
8
|
+
|
|
9
|
+
if (accounts.length === 0) {
|
|
10
|
+
if (isJson) {
|
|
11
|
+
console.log(JSON.stringify([]));
|
|
12
|
+
} else {
|
|
13
|
+
console.log(chalk.yellow('No accounts configured.'));
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (isJson) {
|
|
19
|
+
const jsonAccounts = accounts.map(name => {
|
|
20
|
+
const acct = config.accounts[name];
|
|
21
|
+
const smtpHost = (acct.smtp && acct.smtp.host) || (config.smtp && config.smtp.host) || null;
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
from: acct.from || null,
|
|
25
|
+
smtpHost,
|
|
26
|
+
hasImap: !!acct.imap
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
console.log(JSON.stringify(jsonAccounts, null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(chalk.cyan('๐ง Configured Accounts'));
|
|
34
|
+
|
|
35
|
+
const table = makeTable(['Account', 'From', 'SMTP Host', 'IMAP'], [12, 28, 20, 10]);
|
|
36
|
+
|
|
37
|
+
for (const name of accounts) {
|
|
38
|
+
const acct = config.accounts[name];
|
|
39
|
+
const smtpHost = (acct.smtp && acct.smtp.host) || (config.smtp && config.smtp.host) || '';
|
|
40
|
+
const hasImap = acct.imap ? chalk.green('โ
') : chalk.red('โ');
|
|
41
|
+
|
|
42
|
+
table.push([
|
|
43
|
+
chalk.cyan.bold(name),
|
|
44
|
+
chalk.green(acct.from || ''),
|
|
45
|
+
smtpHost,
|
|
46
|
+
hasImap
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(table.toString());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const help = `
|
|
54
|
+
stellamail accounts โ List configured accounts
|
|
55
|
+
|
|
56
|
+
USAGE:
|
|
57
|
+
stellamail accounts [options]
|
|
58
|
+
|
|
59
|
+
OPTIONS:
|
|
60
|
+
--output json Output as JSON
|
|
61
|
+
--config <path> Path to config file
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
module.exports = { run, help };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getAccount } = require('../config');
|
|
4
|
+
const { withImap } = require('../imap');
|
|
5
|
+
const { chalk } = require('../format');
|
|
6
|
+
|
|
7
|
+
async function run(opts, config) {
|
|
8
|
+
const id = opts._positional && opts._positional[0];
|
|
9
|
+
if (!id) {
|
|
10
|
+
console.error(chalk.red('โ Usage: stellamail delete <id> --account <name>'));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
if (!opts.account) {
|
|
14
|
+
console.error(chalk.red('โ --account is required for delete'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const acctConfig = getAccount(config, opts.account);
|
|
19
|
+
const folder = opts.folder || 'INBOX';
|
|
20
|
+
const seq = parseInt(id, 10);
|
|
21
|
+
const permanent = !!opts.permanent;
|
|
22
|
+
const isJson = opts.output === 'json';
|
|
23
|
+
|
|
24
|
+
await withImap(acctConfig, config, async (client) => {
|
|
25
|
+
const mailbox = await client.getMailboxLock(folder);
|
|
26
|
+
try {
|
|
27
|
+
if (permanent) {
|
|
28
|
+
await client.messageDelete(String(seq));
|
|
29
|
+
if (isJson) {
|
|
30
|
+
console.log(JSON.stringify({
|
|
31
|
+
status: 'deleted',
|
|
32
|
+
id: seq,
|
|
33
|
+
folder,
|
|
34
|
+
permanent: true,
|
|
35
|
+
account: opts.account
|
|
36
|
+
}, null, 2));
|
|
37
|
+
} else {
|
|
38
|
+
console.log(chalk.green(`โ
Message #${seq} permanently deleted from "${folder}"`));
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
await client.messageMove(String(seq), 'Trash');
|
|
42
|
+
if (isJson) {
|
|
43
|
+
console.log(JSON.stringify({
|
|
44
|
+
status: 'deleted',
|
|
45
|
+
id: seq,
|
|
46
|
+
folder,
|
|
47
|
+
movedTo: 'Trash',
|
|
48
|
+
permanent: false,
|
|
49
|
+
account: opts.account
|
|
50
|
+
}, null, 2));
|
|
51
|
+
} else {
|
|
52
|
+
console.log(chalk.green(`โ
Message #${seq} moved to Trash`));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
mailbox.release();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const help = `
|
|
62
|
+
stellamail delete โ Delete a message
|
|
63
|
+
|
|
64
|
+
USAGE:
|
|
65
|
+
stellamail delete <id> --account <name> [options]
|
|
66
|
+
|
|
67
|
+
REQUIRED:
|
|
68
|
+
<id> Message sequence number
|
|
69
|
+
--account <name> Account name from config
|
|
70
|
+
|
|
71
|
+
OPTIONAL:
|
|
72
|
+
--folder <name> Source folder (default INBOX)
|
|
73
|
+
--permanent Permanently delete (don't move to Trash)
|
|
74
|
+
--output json Output as JSON
|
|
75
|
+
--config <path> Path to config file
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
module.exports = { run, help };
|