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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getAccount } = require('../config');
|
|
4
|
+
const { withImap } = require('../imap');
|
|
5
|
+
const { chalk } = require('../format');
|
|
6
|
+
|
|
7
|
+
const FLAG_MAP = {
|
|
8
|
+
seen: '\\Seen',
|
|
9
|
+
flagged: '\\Flagged',
|
|
10
|
+
answered: '\\Answered',
|
|
11
|
+
draft: '\\Draft'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async function run(opts, config) {
|
|
15
|
+
const id = opts._positional && opts._positional[0];
|
|
16
|
+
if (!id) {
|
|
17
|
+
console.error(chalk.red('❌ Usage: stellamail flag <id> --account <name> --add|--remove <flag>'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
if (!opts.account) {
|
|
21
|
+
console.error(chalk.red('❌ --account is required for flag'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
if (!opts.add && !opts.remove) {
|
|
25
|
+
console.error(chalk.red('❌ --add <flag> or --remove <flag> is required'));
|
|
26
|
+
console.error(chalk.gray(' Valid flags: seen, flagged, answered, draft'));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const acctConfig = getAccount(config, opts.account);
|
|
31
|
+
const folder = opts.folder || 'INBOX';
|
|
32
|
+
const seq = parseInt(id, 10);
|
|
33
|
+
const isJson = opts.output === 'json';
|
|
34
|
+
|
|
35
|
+
const flagName = opts.add || opts.remove;
|
|
36
|
+
const imapFlag = FLAG_MAP[flagName.toLowerCase()];
|
|
37
|
+
if (!imapFlag) {
|
|
38
|
+
console.error(chalk.red(`❌ Unknown flag: "${flagName}"`));
|
|
39
|
+
console.error(chalk.gray(' Valid flags: seen, flagged, answered, draft'));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const adding = !!opts.add;
|
|
44
|
+
|
|
45
|
+
await withImap(acctConfig, config, async (client) => {
|
|
46
|
+
const mailbox = await client.getMailboxLock(folder);
|
|
47
|
+
try {
|
|
48
|
+
if (adding) {
|
|
49
|
+
await client.messageFlagsAdd(String(seq), [imapFlag]);
|
|
50
|
+
} else {
|
|
51
|
+
await client.messageFlagsRemove(String(seq), [imapFlag]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isJson) {
|
|
55
|
+
console.log(JSON.stringify({
|
|
56
|
+
status: 'updated',
|
|
57
|
+
id: seq,
|
|
58
|
+
folder,
|
|
59
|
+
action: adding ? 'added' : 'removed',
|
|
60
|
+
flag: flagName.toLowerCase(),
|
|
61
|
+
account: opts.account
|
|
62
|
+
}, null, 2));
|
|
63
|
+
} else {
|
|
64
|
+
const action = adding ? 'Added' : 'Removed';
|
|
65
|
+
console.log(chalk.green(`✅ ${action} flag "${flagName}" on message #${seq}`));
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
mailbox.release();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const help = `
|
|
74
|
+
stellamail flag — Toggle flags on a message
|
|
75
|
+
|
|
76
|
+
USAGE:
|
|
77
|
+
stellamail flag <id> --account <name> --add|--remove <flag>
|
|
78
|
+
|
|
79
|
+
REQUIRED:
|
|
80
|
+
<id> Message sequence number
|
|
81
|
+
--account <name> Account name from config
|
|
82
|
+
--add <flag> Add a flag (seen, flagged, answered, draft)
|
|
83
|
+
--remove <flag> Remove a flag
|
|
84
|
+
|
|
85
|
+
OPTIONAL:
|
|
86
|
+
--folder <name> Folder name (default INBOX)
|
|
87
|
+
--output json Output as JSON
|
|
88
|
+
--config <path> Path to config file
|
|
89
|
+
|
|
90
|
+
EXAMPLES:
|
|
91
|
+
stellamail flag 142 --account work --add flagged
|
|
92
|
+
stellamail flag 142 --account work --remove seen
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
module.exports = { run, help };
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
if (!opts.account) {
|
|
9
|
+
console.error(chalk.red('❌ --account is required for folders'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const acctConfig = getAccount(config, opts.account);
|
|
14
|
+
const isJson = opts.output === 'json';
|
|
15
|
+
|
|
16
|
+
await withImap(acctConfig, config, async (client) => {
|
|
17
|
+
const folders = await client.list();
|
|
18
|
+
|
|
19
|
+
if (isJson) {
|
|
20
|
+
const jsonFolders = [];
|
|
21
|
+
for (const f of folders) {
|
|
22
|
+
let messageCount = null;
|
|
23
|
+
try {
|
|
24
|
+
const status = await client.status(f.path, { messages: true });
|
|
25
|
+
messageCount = status.messages;
|
|
26
|
+
} catch (e) {}
|
|
27
|
+
jsonFolders.push({
|
|
28
|
+
path: f.path,
|
|
29
|
+
name: f.name || f.path,
|
|
30
|
+
messages: messageCount
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
console.log(JSON.stringify(jsonFolders, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(chalk.cyan(`📁 Folders — ${opts.account}`));
|
|
38
|
+
|
|
39
|
+
const spamTrash = ['spam', 'trash', 'junk', '[gmail]/spam', '[gmail]/trash'];
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < folders.length; i++) {
|
|
42
|
+
const f = folders[i];
|
|
43
|
+
const isLast = i === folders.length - 1;
|
|
44
|
+
const prefix = isLast ? '└── ' : '├── ';
|
|
45
|
+
|
|
46
|
+
let countStr = '';
|
|
47
|
+
try {
|
|
48
|
+
const status = await client.status(f.path, { messages: true });
|
|
49
|
+
countStr = ` (${chalk.yellow(status.messages)})`;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
countStr = '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const folderPath = f.path;
|
|
55
|
+
const isInbox = folderPath.toUpperCase() === 'INBOX';
|
|
56
|
+
const isSpamTrash = spamTrash.includes(folderPath.toLowerCase());
|
|
57
|
+
|
|
58
|
+
let folderDisplay;
|
|
59
|
+
if (isInbox) {
|
|
60
|
+
folderDisplay = chalk.cyan.bold(folderPath);
|
|
61
|
+
} else if (isSpamTrash) {
|
|
62
|
+
folderDisplay = chalk.red(folderPath);
|
|
63
|
+
} else {
|
|
64
|
+
folderDisplay = chalk.cyan(folderPath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`${chalk.gray(prefix)}${folderDisplay}${countStr}`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const help = `
|
|
73
|
+
stellamail folders — List mailbox folders
|
|
74
|
+
|
|
75
|
+
USAGE:
|
|
76
|
+
stellamail folders --account <name> [options]
|
|
77
|
+
|
|
78
|
+
OPTIONS:
|
|
79
|
+
--account <name> Account name (required)
|
|
80
|
+
--output json Output as JSON
|
|
81
|
+
--config <path> Path to config file
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
module.exports = { run, help };
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { simpleParser } = require('mailparser');
|
|
5
|
+
const { getAccount } = require('../config');
|
|
6
|
+
const { withImap } = require('../imap');
|
|
7
|
+
const { getSignature } = require('../signature');
|
|
8
|
+
const { validateAttachments, checkDuplicate } = require('../safety');
|
|
9
|
+
const { logSend } = require('../logger');
|
|
10
|
+
const { createTransport } = require('../smtp');
|
|
11
|
+
const { chalk } = require('../format');
|
|
12
|
+
|
|
13
|
+
async function run(opts, config) {
|
|
14
|
+
const id = opts._positional && opts._positional[0];
|
|
15
|
+
if (!id) {
|
|
16
|
+
console.error(chalk.red('❌ Usage: stellamail forward <id> --account <name> --to <email>'));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
if (!opts.account) {
|
|
20
|
+
console.error(chalk.red('❌ --account is required for forward'));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
if (!opts.to) {
|
|
24
|
+
console.error(chalk.red('❌ --to is required for forward'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Handle body-file
|
|
29
|
+
if (opts['body-file']) {
|
|
30
|
+
if (!fs.existsSync(opts['body-file'])) {
|
|
31
|
+
console.error(chalk.red(`❌ Body file not found: ${opts['body-file']}`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
opts.body = fs.readFileSync(opts['body-file'], 'utf8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const acctConfig = getAccount(config, opts.account);
|
|
38
|
+
const folder = opts.folder || 'INBOX';
|
|
39
|
+
const seq = parseInt(id, 10);
|
|
40
|
+
const isJson = opts.output === 'json';
|
|
41
|
+
|
|
42
|
+
// Fetch original message via IMAP
|
|
43
|
+
const original = await withImap(acctConfig, config, async (client) => {
|
|
44
|
+
const mailbox = await client.getMailboxLock(folder);
|
|
45
|
+
try {
|
|
46
|
+
const msg = await client.fetchOne(String(seq), {
|
|
47
|
+
source: true,
|
|
48
|
+
envelope: true
|
|
49
|
+
});
|
|
50
|
+
return simpleParser(msg.source);
|
|
51
|
+
} finally {
|
|
52
|
+
mailbox.release();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Build forward subject
|
|
57
|
+
const origSubject = original.subject || '';
|
|
58
|
+
const subject = /^fwd:/i.test(origSubject) ? origSubject : `Fwd: ${origSubject}`;
|
|
59
|
+
opts.subject = subject;
|
|
60
|
+
|
|
61
|
+
// Build forwarded body
|
|
62
|
+
const isHtml = !!opts.html;
|
|
63
|
+
const origFrom = original.from ? original.from.text : '';
|
|
64
|
+
const origTo = original.to ? original.to.text : '';
|
|
65
|
+
const origDate = original.date ? original.date.toISOString() : '';
|
|
66
|
+
const origText = original.text || original.html || '';
|
|
67
|
+
|
|
68
|
+
let fullBody;
|
|
69
|
+
const separator = '---------- Forwarded message ----------';
|
|
70
|
+
const fwdHeader = `From: ${origFrom}\nDate: ${origDate}\nSubject: ${origSubject}\nTo: ${origTo}`;
|
|
71
|
+
|
|
72
|
+
if (isHtml) {
|
|
73
|
+
const coverMsg = opts.body || '';
|
|
74
|
+
fullBody = coverMsg + '<br><br>' + separator + '<br>' +
|
|
75
|
+
fwdHeader.replace(/\n/g, '<br>') + '<br><br>' +
|
|
76
|
+
(original.html || origText);
|
|
77
|
+
} else {
|
|
78
|
+
const coverMsg = opts.body || '';
|
|
79
|
+
fullBody = coverMsg + '\n\n' + separator + '\n' +
|
|
80
|
+
fwdHeader + '\n\n' + origText;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Signature
|
|
84
|
+
const signature = opts['no-signature'] ? '' : getSignature(acctConfig, config, isHtml);
|
|
85
|
+
const bodyWithSig = fullBody + signature;
|
|
86
|
+
|
|
87
|
+
// Gather attachments: original + additional
|
|
88
|
+
const origAttachments = (original.attachments || []).map(att => ({
|
|
89
|
+
filename: att.filename || 'attachment',
|
|
90
|
+
content: att.content,
|
|
91
|
+
contentType: att.contentType
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
if (!opts.attachments) opts.attachments = [];
|
|
95
|
+
const additionalAttachments = opts.attachments.length
|
|
96
|
+
? validateAttachments(opts.attachments, config)
|
|
97
|
+
: [];
|
|
98
|
+
const allAttachments = origAttachments.concat(additionalAttachments);
|
|
99
|
+
|
|
100
|
+
// Duplicate check
|
|
101
|
+
let lockFile = null;
|
|
102
|
+
if (!opts['no-duplicate-check']) {
|
|
103
|
+
const dup = checkDuplicate(opts, config);
|
|
104
|
+
lockFile = dup.lockFile;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build mail options
|
|
108
|
+
const mailOptions = {
|
|
109
|
+
from: acctConfig.from,
|
|
110
|
+
to: opts.to,
|
|
111
|
+
subject,
|
|
112
|
+
attachments: allAttachments
|
|
113
|
+
};
|
|
114
|
+
if (opts.cc) mailOptions.cc = opts.cc;
|
|
115
|
+
if (isHtml) {
|
|
116
|
+
mailOptions.html = bodyWithSig;
|
|
117
|
+
} else {
|
|
118
|
+
mailOptions.text = bodyWithSig;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Dry run
|
|
122
|
+
if (opts['dry-run']) {
|
|
123
|
+
if (isJson) {
|
|
124
|
+
console.log(JSON.stringify({
|
|
125
|
+
dryRun: true,
|
|
126
|
+
from: mailOptions.from,
|
|
127
|
+
to: mailOptions.to,
|
|
128
|
+
cc: mailOptions.cc || null,
|
|
129
|
+
subject: mailOptions.subject,
|
|
130
|
+
format: isHtml ? 'html' : 'text',
|
|
131
|
+
attachments: allAttachments.map(a => a.filename),
|
|
132
|
+
bodyPreview: bodyWithSig.slice(0, 200)
|
|
133
|
+
}, null, 2));
|
|
134
|
+
} else {
|
|
135
|
+
console.log('🧪 DRY RUN — Forward would be sent:');
|
|
136
|
+
console.log(` ${chalk.cyan('From:')} ${mailOptions.from}`);
|
|
137
|
+
console.log(` ${chalk.cyan('To:')} ${mailOptions.to}`);
|
|
138
|
+
if (opts.cc) console.log(` ${chalk.cyan('CC:')} ${opts.cc}`);
|
|
139
|
+
console.log(` ${chalk.cyan('Subject:')} ${subject}`);
|
|
140
|
+
console.log(` ${chalk.cyan('Format:')} ${chalk.yellow(isHtml ? 'HTML' : 'Plain text')}`);
|
|
141
|
+
console.log(` ${chalk.cyan('Attachments:')} ${allAttachments.length ? allAttachments.map(a => a.filename).join(', ') : 'none'}`);
|
|
142
|
+
console.log(` ${chalk.cyan('Body preview:')}`);
|
|
143
|
+
console.log(chalk.gray(' ' + bodyWithSig.slice(0, 200).replace(/\n/g, '\n ') + (bodyWithSig.length > 200 ? '...' : '')));
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Write lock BEFORE sending
|
|
149
|
+
if (lockFile) {
|
|
150
|
+
fs.writeFileSync(lockFile, String(Math.floor(Date.now() / 1000)));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const transporter = createTransport(acctConfig, config);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await transporter.verify();
|
|
157
|
+
if (!isJson) console.log(chalk.green('✅ SMTP connection verified'));
|
|
158
|
+
} catch (e) {
|
|
159
|
+
if (lockFile && fs.existsSync(lockFile)) fs.unlinkSync(lockFile);
|
|
160
|
+
console.error(chalk.red('❌ SMTP connection failed: ' + e.message));
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const info = await transporter.sendMail(mailOptions);
|
|
166
|
+
|
|
167
|
+
logSend({
|
|
168
|
+
timestamp: new Date().toISOString(),
|
|
169
|
+
account: opts.account,
|
|
170
|
+
to: opts.to,
|
|
171
|
+
cc: opts.cc || null,
|
|
172
|
+
subject,
|
|
173
|
+
attachments: allAttachments.map(a => a.filename),
|
|
174
|
+
messageId: info.messageId,
|
|
175
|
+
type: 'forward',
|
|
176
|
+
status: 'sent'
|
|
177
|
+
}, config);
|
|
178
|
+
|
|
179
|
+
if (isJson) {
|
|
180
|
+
console.log(JSON.stringify({
|
|
181
|
+
status: 'sent',
|
|
182
|
+
messageId: info.messageId,
|
|
183
|
+
to: opts.to,
|
|
184
|
+
cc: opts.cc || null,
|
|
185
|
+
subject,
|
|
186
|
+
type: 'forward'
|
|
187
|
+
}, null, 2));
|
|
188
|
+
} else {
|
|
189
|
+
console.log(chalk.green('✅ Forward sent successfully!'));
|
|
190
|
+
console.log(` ${chalk.cyan('To:')} ${opts.to}`);
|
|
191
|
+
if (opts.cc) console.log(` ${chalk.cyan('CC:')} ${opts.cc}`);
|
|
192
|
+
console.log(` ${chalk.cyan('Subject:')} ${subject}`);
|
|
193
|
+
console.log(` ${chalk.cyan('Attachments:')} ${allAttachments.length ? allAttachments.map(a => a.filename).join(', ') : 'none'}`);
|
|
194
|
+
console.log(` ${chalk.cyan('Message ID:')} ${info.messageId}`);
|
|
195
|
+
}
|
|
196
|
+
} catch (e) {
|
|
197
|
+
logSend({
|
|
198
|
+
timestamp: new Date().toISOString(),
|
|
199
|
+
account: opts.account,
|
|
200
|
+
to: opts.to,
|
|
201
|
+
subject,
|
|
202
|
+
type: 'forward',
|
|
203
|
+
status: 'failed',
|
|
204
|
+
error: e.message
|
|
205
|
+
}, config);
|
|
206
|
+
|
|
207
|
+
console.error(chalk.red('❌ Forward FAILED: ' + e.message));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const help = `
|
|
213
|
+
stellamail forward — Forward an email
|
|
214
|
+
|
|
215
|
+
USAGE:
|
|
216
|
+
stellamail forward <id> --account <name> --to <email> [options]
|
|
217
|
+
|
|
218
|
+
REQUIRED:
|
|
219
|
+
<id> Message sequence number
|
|
220
|
+
--account <name> Account name from config
|
|
221
|
+
--to <email> Recipient email address
|
|
222
|
+
|
|
223
|
+
OPTIONAL:
|
|
224
|
+
--body <text> Cover message
|
|
225
|
+
--body-file <path> Read cover message from a file
|
|
226
|
+
--folder <name> Folder name (default INBOX)
|
|
227
|
+
--cc <email> CC recipient
|
|
228
|
+
--attachment <path> Additional attachment (repeatable)
|
|
229
|
+
--html Treat body as HTML
|
|
230
|
+
--no-signature Skip auto-appending signature
|
|
231
|
+
--no-duplicate-check Skip duplicate blocking
|
|
232
|
+
--dry-run Preview without sending
|
|
233
|
+
--output json Output as JSON
|
|
234
|
+
--config <path> Path to config file
|
|
235
|
+
`;
|
|
236
|
+
|
|
237
|
+
module.exports = { run, help };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getAccount } = require('../config');
|
|
4
|
+
const { withImap } = require('../imap');
|
|
5
|
+
const { formatDate, truncate, messageFlags, makeTable, chalk } = require('../format');
|
|
6
|
+
|
|
7
|
+
async function run(opts, config) {
|
|
8
|
+
if (!opts.account) {
|
|
9
|
+
console.error(chalk.red('❌ --account is required for inbox'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const acctConfig = getAccount(config, opts.account);
|
|
14
|
+
const folder = opts.folder || 'INBOX';
|
|
15
|
+
const limit = parseInt(opts.limit, 10) || 20;
|
|
16
|
+
const unreadOnly = !!opts.unread;
|
|
17
|
+
const isJson = opts.output === 'json';
|
|
18
|
+
|
|
19
|
+
await withImap(acctConfig, config, async (client) => {
|
|
20
|
+
const mailbox = await client.getMailboxLock(folder);
|
|
21
|
+
try {
|
|
22
|
+
const total = client.mailbox.exists;
|
|
23
|
+
let unreadCount = 0;
|
|
24
|
+
|
|
25
|
+
// Count unread
|
|
26
|
+
try {
|
|
27
|
+
const status = await client.status(folder, { unseen: true });
|
|
28
|
+
unreadCount = status.unseen || 0;
|
|
29
|
+
} catch (e) {
|
|
30
|
+
unreadCount = 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!isJson) {
|
|
34
|
+
const unreadLabel = unreadCount > 0 ? chalk.yellow.bold(`${unreadCount} unread`) : chalk.green.bold(`0 unread`);
|
|
35
|
+
console.log(chalk.cyan.bold(`\n \uD83D\uDCEC Inbox`) + chalk.white.bold(` \u2014 ${opts.account}`) + chalk.gray(` (`) + unreadLabel + chalk.gray(`, ${total} total)`));
|
|
36
|
+
console.log();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (total === 0) {
|
|
40
|
+
if (isJson) {
|
|
41
|
+
console.log(JSON.stringify([]));
|
|
42
|
+
} else {
|
|
43
|
+
console.log(' No messages.');
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const query = unreadOnly ? { seen: false } : { all: true };
|
|
49
|
+
const range = `${Math.max(1, total - limit + 1)}:${total}`;
|
|
50
|
+
|
|
51
|
+
const messages = [];
|
|
52
|
+
for await (const msg of client.fetch(range, {
|
|
53
|
+
envelope: true,
|
|
54
|
+
flags: true,
|
|
55
|
+
bodyStructure: true
|
|
56
|
+
})) {
|
|
57
|
+
messages.push(msg);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
messages.sort((a, b) => b.seq - a.seq);
|
|
61
|
+
|
|
62
|
+
if (unreadOnly) {
|
|
63
|
+
messages.splice(0, messages.length, ...messages.filter(m => !m.flags.has('\\Seen')));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const display = messages.slice(0, limit);
|
|
67
|
+
|
|
68
|
+
if (isJson) {
|
|
69
|
+
const jsonMessages = display.map(msg => {
|
|
70
|
+
const env = msg.envelope;
|
|
71
|
+
const from = env.from && env.from[0]
|
|
72
|
+
? { name: env.from[0].name || null, address: env.from[0].address || null }
|
|
73
|
+
: null;
|
|
74
|
+
const hasAttach = msg.bodyStructure && msg.bodyStructure.type === 'multipart';
|
|
75
|
+
return {
|
|
76
|
+
seq: msg.seq,
|
|
77
|
+
subject: env.subject || null,
|
|
78
|
+
from,
|
|
79
|
+
date: env.date ? new Date(env.date).toISOString() : null,
|
|
80
|
+
flags: Array.from(msg.flags || []),
|
|
81
|
+
hasAttachment: hasAttach
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
console.log(JSON.stringify(jsonMessages, null, 2));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const table = makeTable(['#', 'FLAGS', 'SUBJECT', 'FROM', 'DATE'], [6, 8, 35, 25, 18]);
|
|
89
|
+
|
|
90
|
+
for (const msg of display) {
|
|
91
|
+
const env = msg.envelope;
|
|
92
|
+
const from = env.from && env.from[0]
|
|
93
|
+
? (env.from[0].name || env.from[0].address || '')
|
|
94
|
+
: '';
|
|
95
|
+
const hasAttach = msg.bodyStructure && msg.bodyStructure.type === 'multipart';
|
|
96
|
+
const flags = messageFlags(msg.flags, hasAttach);
|
|
97
|
+
const isUnread = msg.flags && !msg.flags.has('\\Seen');
|
|
98
|
+
|
|
99
|
+
const numColor = isUnread ? chalk.cyan.bold : chalk.white;
|
|
100
|
+
const subjColor = isUnread ? chalk.white.bold : chalk.white;
|
|
101
|
+
const fromColor = isUnread ? chalk.green.bold : chalk.green;
|
|
102
|
+
const dateColor = isUnread ? chalk.yellow.bold : chalk.yellow;
|
|
103
|
+
|
|
104
|
+
table.push([
|
|
105
|
+
numColor(String(msg.seq)),
|
|
106
|
+
flags,
|
|
107
|
+
subjColor(truncate(env.subject || '(no subject)', 33)),
|
|
108
|
+
fromColor(truncate(from, 23)),
|
|
109
|
+
dateColor(formatDate(env.date))
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(table.toString());
|
|
114
|
+
} finally {
|
|
115
|
+
mailbox.release();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const help = `
|
|
121
|
+
stellamail inbox — List inbox messages
|
|
122
|
+
|
|
123
|
+
USAGE:
|
|
124
|
+
stellamail inbox --account <name> [options]
|
|
125
|
+
|
|
126
|
+
OPTIONS:
|
|
127
|
+
--account <name> Account name (required)
|
|
128
|
+
--limit <n> Number of messages (default 20)
|
|
129
|
+
--folder <name> Folder name (default INBOX)
|
|
130
|
+
--unread Only show unread messages
|
|
131
|
+
--output json Output as JSON
|
|
132
|
+
--config <path> Path to config file
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
module.exports = { run, help };
|