mxroute-cli 0.3.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -11
- package/dist/commands/accounts-search.d.ts +1 -0
- package/dist/commands/accounts-search.js +66 -0
- package/dist/commands/accounts-search.js.map +1 -0
- package/dist/commands/accounts.js +39 -9
- package/dist/commands/accounts.js.map +1 -1
- package/dist/commands/aliases-sync.d.ts +1 -0
- package/dist/commands/aliases-sync.js +162 -0
- package/dist/commands/aliases-sync.js.map +1 -0
- package/dist/commands/autoresponder.js +7 -6
- package/dist/commands/autoresponder.js.map +1 -1
- package/dist/commands/backup.d.ts +1 -0
- package/dist/commands/backup.js +170 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/bulk.js +52 -9
- package/dist/commands/bulk.js.map +1 -1
- package/dist/commands/catchall.js +2 -2
- package/dist/commands/catchall.js.map +1 -1
- package/dist/commands/cleanup.d.ts +1 -0
- package/dist/commands/cleanup.js +234 -0
- package/dist/commands/cleanup.js.map +1 -0
- package/dist/commands/config.js +9 -2
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/credentials-export.d.ts +13 -0
- package/dist/commands/credentials-export.js +142 -0
- package/dist/commands/credentials-export.js.map +1 -0
- package/dist/commands/deprovision.d.ts +1 -0
- package/dist/commands/deprovision.js +125 -0
- package/dist/commands/deprovision.js.map +1 -0
- package/dist/commands/dns-setup.js +20 -3
- package/dist/commands/dns-setup.js.map +1 -1
- package/dist/commands/export-import.js +22 -1
- package/dist/commands/export-import.js.map +1 -1
- package/dist/commands/filters.js +2 -2
- package/dist/commands/filters.js.map +1 -1
- package/dist/commands/fix.js +1 -1
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/forwarders-validate.d.ts +1 -0
- package/dist/commands/forwarders-validate.js +190 -0
- package/dist/commands/forwarders-validate.js.map +1 -0
- package/dist/commands/forwarders.js +6 -5
- package/dist/commands/forwarders.js.map +1 -1
- package/dist/commands/lists.js +7 -6
- package/dist/commands/lists.js.map +1 -1
- package/dist/commands/mail.d.ts +15 -0
- package/dist/commands/mail.js +998 -0
- package/dist/commands/mail.js.map +1 -0
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/onboard.js +11 -4
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/password-audit.d.ts +1 -0
- package/dist/commands/password-audit.js +304 -0
- package/dist/commands/password-audit.js.map +1 -0
- package/dist/commands/password.d.ts +1 -0
- package/dist/commands/password.js +96 -0
- package/dist/commands/password.js.map +1 -0
- package/dist/commands/provision.d.ts +13 -0
- package/dist/commands/provision.js +306 -0
- package/dist/commands/provision.js.map +1 -0
- package/dist/commands/quota-policy.d.ts +3 -0
- package/dist/commands/quota-policy.js +192 -0
- package/dist/commands/quota-policy.js.map +1 -0
- package/dist/commands/quota.js +1 -1
- package/dist/commands/quota.js.map +1 -1
- package/dist/commands/rate-limit.d.ts +2 -0
- package/dist/commands/rate-limit.js +121 -0
- package/dist/commands/rate-limit.js.map +1 -0
- package/dist/commands/reputation.d.ts +1 -0
- package/dist/commands/reputation.js +265 -0
- package/dist/commands/reputation.js.map +1 -0
- package/dist/commands/schedule.d.ts +3 -0
- package/dist/commands/schedule.js +270 -0
- package/dist/commands/schedule.js.map +1 -0
- package/dist/commands/send.js +2 -1
- package/dist/commands/send.js.map +1 -1
- package/dist/commands/setup.js +22 -0
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/share.js +20 -10
- package/dist/commands/share.js.map +1 -1
- package/dist/commands/smtp-debug.d.ts +1 -0
- package/dist/commands/smtp-debug.js +188 -0
- package/dist/commands/smtp-debug.js.map +1 -0
- package/dist/commands/spam.js +3 -3
- package/dist/commands/spam.js.map +1 -1
- package/dist/commands/ssl-check.d.ts +1 -0
- package/dist/commands/ssl-check.js +145 -0
- package/dist/commands/ssl-check.js.map +1 -0
- package/dist/commands/status.js +53 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/templates.d.ts +4 -0
- package/dist/commands/templates.js +310 -0
- package/dist/commands/templates.js.map +1 -0
- package/dist/commands/test-delivery.d.ts +1 -0
- package/dist/commands/test-delivery.js +87 -0
- package/dist/commands/test-delivery.js.map +1 -0
- package/dist/commands/usage-history.d.ts +1 -0
- package/dist/commands/usage-history.js +173 -0
- package/dist/commands/usage-history.js.map +1 -0
- package/dist/commands/webhook.js +18 -3
- package/dist/commands/webhook.js.map +1 -1
- package/dist/commands/welcome-send.d.ts +11 -0
- package/dist/commands/welcome-send.js +163 -0
- package/dist/commands/welcome-send.js.map +1 -0
- package/dist/index.js +362 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1940 -10
- package/dist/mcp.js.map +1 -1
- package/dist/utils/config.js +1 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/directadmin.js +23 -2
- package/dist/utils/directadmin.js.map +1 -1
- package/dist/utils/imap.d.ts +61 -0
- package/dist/utils/imap.js +426 -0
- package/dist/utils/imap.js.map +1 -0
- package/dist/utils/mime.d.ts +50 -0
- package/dist/utils/mime.js +369 -0
- package/dist/utils/mime.js.map +1 -0
- package/dist/utils/shared.d.ts +10 -0
- package/dist/utils/shared.js +28 -1
- package/dist/utils/shared.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.mailInbox = mailInbox;
|
|
40
|
+
exports.mailRead = mailRead;
|
|
41
|
+
exports.mailCompose = mailCompose;
|
|
42
|
+
exports.mailReply = mailReply;
|
|
43
|
+
exports.mailForward = mailForward;
|
|
44
|
+
exports.mailDelete = mailDelete;
|
|
45
|
+
exports.mailSearch = mailSearch;
|
|
46
|
+
exports.mailFolders = mailFolders;
|
|
47
|
+
exports.mailFolderCreate = mailFolderCreate;
|
|
48
|
+
exports.mailFolderDelete = mailFolderDelete;
|
|
49
|
+
exports.mailMove = mailMove;
|
|
50
|
+
exports.mailSaveAttachment = mailSaveAttachment;
|
|
51
|
+
exports.mailUnread = mailUnread;
|
|
52
|
+
exports.mailMarkRead = mailMarkRead;
|
|
53
|
+
exports.mailMarkUnread = mailMarkUnread;
|
|
54
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
55
|
+
const ora_1 = __importDefault(require("ora"));
|
|
56
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const path = __importStar(require("path"));
|
|
59
|
+
const tls = __importStar(require("tls"));
|
|
60
|
+
const theme_1 = require("../utils/theme");
|
|
61
|
+
const config_1 = require("../utils/config");
|
|
62
|
+
const imap_1 = require("../utils/imap");
|
|
63
|
+
const mime_1 = require("../utils/mime");
|
|
64
|
+
function getImapConfig() {
|
|
65
|
+
const config = (0, config_1.getConfig)();
|
|
66
|
+
if (!config.server || !config.username || !config.password) {
|
|
67
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} SMTP/IMAP not configured. Run ${theme_1.theme.bold('mxroute config smtp')} first.\n`));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
host: `${config.server}.mxrouting.net`,
|
|
72
|
+
port: 993,
|
|
73
|
+
user: config.username,
|
|
74
|
+
password: config.password,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function withImap(fn) {
|
|
78
|
+
const imapConfig = getImapConfig();
|
|
79
|
+
const client = new imap_1.ImapClient(imapConfig);
|
|
80
|
+
try {
|
|
81
|
+
await client.connect();
|
|
82
|
+
await client.login();
|
|
83
|
+
return await fn(client);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await client.logout();
|
|
87
|
+
client.disconnect();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function formatDate(dateStr) {
|
|
91
|
+
try {
|
|
92
|
+
const d = new Date(dateStr);
|
|
93
|
+
const now = new Date();
|
|
94
|
+
const diff = now.getTime() - d.getTime();
|
|
95
|
+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
96
|
+
if (days === 0)
|
|
97
|
+
return d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
98
|
+
if (days === 1)
|
|
99
|
+
return 'Yesterday';
|
|
100
|
+
if (days < 7)
|
|
101
|
+
return d.toLocaleDateString(undefined, { weekday: 'short' });
|
|
102
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return dateStr.substring(0, 16);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function truncate(str, len) {
|
|
109
|
+
if (str.length <= len)
|
|
110
|
+
return str;
|
|
111
|
+
return str.substring(0, len - 1) + '\u2026';
|
|
112
|
+
}
|
|
113
|
+
function sanitizeSmtpValue(s) {
|
|
114
|
+
if (/[\r\n]/.test(s)) {
|
|
115
|
+
throw new Error('Invalid input: contains newline characters');
|
|
116
|
+
}
|
|
117
|
+
return s;
|
|
118
|
+
}
|
|
119
|
+
function extractEmail(addr) {
|
|
120
|
+
const match = addr.match(/<([^>]+)>/);
|
|
121
|
+
return sanitizeSmtpValue(match ? match[1] : addr);
|
|
122
|
+
}
|
|
123
|
+
function escapeHtml(s) {
|
|
124
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
125
|
+
}
|
|
126
|
+
function extractName(addr) {
|
|
127
|
+
const match = addr.match(/^"?([^"<]+)"?\s*</);
|
|
128
|
+
if (match)
|
|
129
|
+
return match[1].trim();
|
|
130
|
+
return addr.split('@')[0];
|
|
131
|
+
}
|
|
132
|
+
// ─── Inbox ──────────────────────────────────────────────
|
|
133
|
+
async function mailInbox(folder) {
|
|
134
|
+
const targetFolder = folder || 'INBOX';
|
|
135
|
+
console.log(theme_1.theme.heading(`Mail: ${targetFolder}`));
|
|
136
|
+
const spinner = (0, ora_1.default)({ text: 'Connecting to mailbox...', spinner: 'dots12', color: 'cyan' }).start();
|
|
137
|
+
try {
|
|
138
|
+
await withImap(async (client) => {
|
|
139
|
+
const info = await client.selectFolder(targetFolder);
|
|
140
|
+
spinner.text = `Fetching messages (${info.exists} total)...`;
|
|
141
|
+
if (info.exists === 0) {
|
|
142
|
+
spinner.stop();
|
|
143
|
+
console.log(theme_1.theme.muted(' No messages in this folder.\n'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const count = Math.min(info.exists, 25);
|
|
147
|
+
const envelopes = await client.fetchEnvelopes(info.exists, count);
|
|
148
|
+
spinner.stop();
|
|
149
|
+
// Sort newest first
|
|
150
|
+
envelopes.sort((a, b) => b.seq - a.seq);
|
|
151
|
+
console.log(theme_1.theme.muted(` Showing ${envelopes.length} of ${info.exists} messages (${info.recent} new)\n`));
|
|
152
|
+
for (const env of envelopes) {
|
|
153
|
+
const isUnread = !env.flags.includes('\\Seen');
|
|
154
|
+
const isFlagged = env.flags.includes('\\Flagged');
|
|
155
|
+
const marker = isUnread ? chalk_1.default.cyan('\u25cf') : ' ';
|
|
156
|
+
const flag = isFlagged ? chalk_1.default.yellow('\u2605') : ' ';
|
|
157
|
+
const uid = theme_1.theme.muted(`#${env.uid}`);
|
|
158
|
+
const from = truncate(extractName(env.from), 20).padEnd(20);
|
|
159
|
+
const subject = truncate(env.subject || '(no subject)', 45);
|
|
160
|
+
const date = formatDate(env.date);
|
|
161
|
+
const size = theme_1.theme.muted((0, mime_1.formatFileSize)(env.size));
|
|
162
|
+
const fromFormatted = isUnread ? chalk_1.default.bold.white(from) : theme_1.theme.muted(from);
|
|
163
|
+
const subjectFormatted = isUnread ? chalk_1.default.white(subject) : theme_1.theme.muted(subject);
|
|
164
|
+
console.log(` ${marker} ${flag} ${uid.padEnd(14)} ${fromFormatted} ${subjectFormatted} ${theme_1.theme.muted(date)} ${size}`);
|
|
165
|
+
}
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(theme_1.theme.muted(` Read: ${theme_1.theme.bold('mxroute mail read <uid>')}`));
|
|
168
|
+
console.log(theme_1.theme.muted(` Unread: ${envelopes.filter((e) => !e.flags.includes('\\Seen')).length} messages\n`));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
spinner.fail(chalk_1.default.red('Failed to fetch inbox'));
|
|
173
|
+
console.log(theme_1.theme.error(` ${err.message}\n`));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// ─── Read ───────────────────────────────────────────────
|
|
177
|
+
async function mailRead(uid) {
|
|
178
|
+
if (!uid) {
|
|
179
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Usage: mxroute mail read <uid>\n`));
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
const uidNum = parseInt(uid, 10);
|
|
183
|
+
if (isNaN(uidNum)) {
|
|
184
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Invalid message UID.\n`));
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const spinner = (0, ora_1.default)({ text: 'Fetching message...', spinner: 'dots12', color: 'cyan' }).start();
|
|
188
|
+
try {
|
|
189
|
+
await withImap(async (client) => {
|
|
190
|
+
await client.selectFolder('INBOX');
|
|
191
|
+
// Mark as read
|
|
192
|
+
await client.setFlags(uidNum, '\\Seen');
|
|
193
|
+
const rawBody = await client.fetchBody(uidNum);
|
|
194
|
+
spinner.stop();
|
|
195
|
+
const msg = (0, mime_1.parseMessage)(rawBody);
|
|
196
|
+
// Header
|
|
197
|
+
console.log(theme_1.theme.heading('Message'));
|
|
198
|
+
console.log(theme_1.theme.keyValue('From', msg.from));
|
|
199
|
+
console.log(theme_1.theme.keyValue('To', msg.to));
|
|
200
|
+
if (msg.cc)
|
|
201
|
+
console.log(theme_1.theme.keyValue('Cc', msg.cc));
|
|
202
|
+
console.log(theme_1.theme.keyValue('Date', msg.date));
|
|
203
|
+
console.log(theme_1.theme.keyValue('Subject', msg.subject));
|
|
204
|
+
if (msg.attachments.length > 0) {
|
|
205
|
+
const attList = msg.attachments.map((a) => `${a.filename} (${(0, mime_1.formatFileSize)(a.size)})`).join(', ');
|
|
206
|
+
console.log(theme_1.theme.keyValue('Attachments', attList));
|
|
207
|
+
}
|
|
208
|
+
console.log(theme_1.theme.separator());
|
|
209
|
+
console.log('');
|
|
210
|
+
// Body
|
|
211
|
+
const body = msg.textBody || (0, mime_1.htmlToText)(msg.htmlBody) || '(empty message)';
|
|
212
|
+
const lines = body.split('\n');
|
|
213
|
+
for (const line of lines) {
|
|
214
|
+
if (line.startsWith('>')) {
|
|
215
|
+
console.log(theme_1.theme.muted(` ${line}`));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
console.log(` ${line}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
console.log('');
|
|
222
|
+
if (msg.attachments.length > 0) {
|
|
223
|
+
console.log(theme_1.theme.muted(` ${msg.attachments.length} attachment${msg.attachments.length === 1 ? '' : 's'}:`));
|
|
224
|
+
for (const att of msg.attachments) {
|
|
225
|
+
console.log(theme_1.theme.muted(` \u2022 ${att.filename} (${(0, mime_1.formatFileSize)(att.size)})`));
|
|
226
|
+
}
|
|
227
|
+
console.log(theme_1.theme.muted(` Save with: ${theme_1.theme.bold(`mxroute mail save-attachment ${uidNum}`)}\n`));
|
|
228
|
+
}
|
|
229
|
+
// Actions
|
|
230
|
+
console.log(theme_1.theme.muted(` Reply: ${theme_1.theme.bold(`mxroute mail reply ${uidNum}`)}`));
|
|
231
|
+
console.log(theme_1.theme.muted(` Forward: ${theme_1.theme.bold(`mxroute mail forward ${uidNum}`)}`));
|
|
232
|
+
console.log(theme_1.theme.muted(` Delete: ${theme_1.theme.bold(`mxroute mail delete ${uidNum}`)}\n`));
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
spinner.fail(chalk_1.default.red('Failed to read message'));
|
|
237
|
+
console.log(theme_1.theme.error(` ${err.message}\n`));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ─── Compose ────────────────────────────────────────────
|
|
241
|
+
async function mailCompose() {
|
|
242
|
+
const config = (0, config_1.getConfig)();
|
|
243
|
+
console.log(theme_1.theme.heading('Compose Email'));
|
|
244
|
+
const answers = await inquirer_1.default.prompt([
|
|
245
|
+
{
|
|
246
|
+
type: 'input',
|
|
247
|
+
name: 'to',
|
|
248
|
+
message: theme_1.theme.secondary('To:'),
|
|
249
|
+
validate: (input) => (input.includes('@') ? true : 'Enter a valid email address'),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
type: 'input',
|
|
253
|
+
name: 'cc',
|
|
254
|
+
message: theme_1.theme.secondary('Cc (optional):'),
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
type: 'input',
|
|
258
|
+
name: 'bcc',
|
|
259
|
+
message: theme_1.theme.secondary('Bcc (optional):'),
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
type: 'input',
|
|
263
|
+
name: 'subject',
|
|
264
|
+
message: theme_1.theme.secondary('Subject:'),
|
|
265
|
+
validate: (input) => (input.trim() ? true : 'Subject is required'),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
type: 'confirm',
|
|
269
|
+
name: 'useHtml',
|
|
270
|
+
message: 'Compose as HTML?',
|
|
271
|
+
default: false,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
type: 'editor',
|
|
275
|
+
name: 'body',
|
|
276
|
+
message: theme_1.theme.secondary('Message body (opens editor):'),
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
type: 'confirm',
|
|
280
|
+
name: 'hasAttachments',
|
|
281
|
+
message: 'Attach files?',
|
|
282
|
+
default: false,
|
|
283
|
+
},
|
|
284
|
+
]);
|
|
285
|
+
const attachments = [];
|
|
286
|
+
if (answers.hasAttachments) {
|
|
287
|
+
let addMore = true;
|
|
288
|
+
while (addMore) {
|
|
289
|
+
const { filePath } = await inquirer_1.default.prompt([
|
|
290
|
+
{
|
|
291
|
+
type: 'input',
|
|
292
|
+
name: 'filePath',
|
|
293
|
+
message: theme_1.theme.secondary('File path:'),
|
|
294
|
+
validate: (input) => {
|
|
295
|
+
const resolved = path.resolve(input);
|
|
296
|
+
if (!fs.existsSync(resolved))
|
|
297
|
+
return `File not found: ${resolved}`;
|
|
298
|
+
if (fs.statSync(resolved).isDirectory())
|
|
299
|
+
return 'Cannot attach a directory';
|
|
300
|
+
return true;
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
const resolved = path.resolve(filePath);
|
|
305
|
+
attachments.push({ filename: path.basename(resolved), path: resolved });
|
|
306
|
+
console.log(theme_1.theme.success(` ${theme_1.theme.statusIcon('pass')} ${path.basename(resolved)} attached`));
|
|
307
|
+
const { more } = await inquirer_1.default.prompt([
|
|
308
|
+
{ type: 'confirm', name: 'more', message: 'Attach another file?', default: false },
|
|
309
|
+
]);
|
|
310
|
+
addMore = more;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Summary
|
|
314
|
+
console.log('');
|
|
315
|
+
console.log(theme_1.theme.keyValue('From', config.username));
|
|
316
|
+
console.log(theme_1.theme.keyValue('To', answers.to));
|
|
317
|
+
if (answers.cc)
|
|
318
|
+
console.log(theme_1.theme.keyValue('Cc', answers.cc));
|
|
319
|
+
if (answers.bcc)
|
|
320
|
+
console.log(theme_1.theme.keyValue('Bcc', answers.bcc));
|
|
321
|
+
console.log(theme_1.theme.keyValue('Subject', answers.subject));
|
|
322
|
+
if (attachments.length > 0) {
|
|
323
|
+
console.log(theme_1.theme.keyValue('Attachments', attachments.map((a) => a.filename).join(', ')));
|
|
324
|
+
}
|
|
325
|
+
console.log('');
|
|
326
|
+
const { confirm } = await inquirer_1.default.prompt([
|
|
327
|
+
{ type: 'confirm', name: 'confirm', message: 'Send this email?', default: true },
|
|
328
|
+
]);
|
|
329
|
+
if (!confirm) {
|
|
330
|
+
console.log(theme_1.theme.muted('\n Cancelled.\n'));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const spinner = (0, ora_1.default)({ text: 'Sending...', spinner: 'dots12', color: 'cyan' }).start();
|
|
334
|
+
try {
|
|
335
|
+
if (attachments.length > 0 || answers.cc || answers.bcc) {
|
|
336
|
+
// Use SMTP directly for attachments/CC/BCC
|
|
337
|
+
const mime = (0, mime_1.buildMimeMessage)({
|
|
338
|
+
from: config.username,
|
|
339
|
+
to: answers.to,
|
|
340
|
+
cc: answers.cc || undefined,
|
|
341
|
+
bcc: answers.bcc || undefined,
|
|
342
|
+
subject: answers.subject,
|
|
343
|
+
textBody: answers.useHtml ? undefined : answers.body,
|
|
344
|
+
htmlBody: answers.useHtml ? answers.body : undefined,
|
|
345
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
346
|
+
});
|
|
347
|
+
await sendViaSMTP(config, answers.to, answers.cc, answers.bcc, mime);
|
|
348
|
+
spinner.succeed(chalk_1.default.green(`Email sent to ${answers.to}`));
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Use SMTP API for simple messages
|
|
352
|
+
const { sendEmail } = await Promise.resolve().then(() => __importStar(require('../utils/api')));
|
|
353
|
+
const result = await sendEmail({
|
|
354
|
+
server: `${config.server}.mxrouting.net`,
|
|
355
|
+
username: config.username,
|
|
356
|
+
password: config.password,
|
|
357
|
+
from: config.username,
|
|
358
|
+
to: answers.to,
|
|
359
|
+
subject: answers.subject,
|
|
360
|
+
body: answers.useHtml
|
|
361
|
+
? answers.body
|
|
362
|
+
: `<pre style="font-family: system-ui, sans-serif; white-space: pre-wrap;">${escapeHtml(answers.body)}</pre>`,
|
|
363
|
+
});
|
|
364
|
+
if (result.success) {
|
|
365
|
+
spinner.succeed(chalk_1.default.green(`Email sent to ${answers.to}`));
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
spinner.fail(chalk_1.default.red(result.message));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
spinner.fail(chalk_1.default.red(`Send failed: ${err.message}`));
|
|
374
|
+
}
|
|
375
|
+
console.log('');
|
|
376
|
+
}
|
|
377
|
+
// ─── Reply ──────────────────────────────────────────────
|
|
378
|
+
async function mailReply(uid) {
|
|
379
|
+
if (!uid) {
|
|
380
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Usage: mxroute mail reply <uid>\n`));
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
const uidNum = parseInt(uid, 10);
|
|
384
|
+
const config = (0, config_1.getConfig)();
|
|
385
|
+
const spinner = (0, ora_1.default)({ text: 'Fetching original message...', spinner: 'dots12', color: 'cyan' }).start();
|
|
386
|
+
try {
|
|
387
|
+
let originalMsg = null;
|
|
388
|
+
await withImap(async (client) => {
|
|
389
|
+
await client.selectFolder('INBOX');
|
|
390
|
+
const rawBody = await client.fetchBody(uidNum);
|
|
391
|
+
originalMsg = (0, mime_1.parseMessage)(rawBody);
|
|
392
|
+
});
|
|
393
|
+
spinner.stop();
|
|
394
|
+
if (!originalMsg) {
|
|
395
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Message not found.\n`));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const msg = originalMsg;
|
|
399
|
+
const replyTo = extractEmail(msg.from);
|
|
400
|
+
const replySubject = msg.subject.startsWith('Re:') ? msg.subject : `Re: ${msg.subject}`;
|
|
401
|
+
console.log(theme_1.theme.heading('Reply'));
|
|
402
|
+
console.log(theme_1.theme.keyValue('To', replyTo));
|
|
403
|
+
console.log(theme_1.theme.keyValue('Subject', replySubject));
|
|
404
|
+
console.log('');
|
|
405
|
+
const { body } = await inquirer_1.default.prompt([
|
|
406
|
+
{
|
|
407
|
+
type: 'editor',
|
|
408
|
+
name: 'body',
|
|
409
|
+
message: theme_1.theme.secondary('Your reply (opens editor):'),
|
|
410
|
+
},
|
|
411
|
+
]);
|
|
412
|
+
const { confirm } = await inquirer_1.default.prompt([
|
|
413
|
+
{ type: 'confirm', name: 'confirm', message: 'Send reply?', default: true },
|
|
414
|
+
]);
|
|
415
|
+
if (!confirm) {
|
|
416
|
+
console.log(theme_1.theme.muted('\n Cancelled.\n'));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const originalBody = msg.textBody || (0, mime_1.htmlToText)(msg.htmlBody);
|
|
420
|
+
const quoted = originalBody
|
|
421
|
+
.split('\n')
|
|
422
|
+
.map((l) => `> ${l}`)
|
|
423
|
+
.join('\n');
|
|
424
|
+
const fullBody = `${body.trim()}\n\nOn ${msg.date}, ${msg.from} wrote:\n${quoted}`;
|
|
425
|
+
const sendSpinner = (0, ora_1.default)({ text: 'Sending reply...', spinner: 'dots12', color: 'cyan' }).start();
|
|
426
|
+
try {
|
|
427
|
+
const { sendEmail } = await Promise.resolve().then(() => __importStar(require('../utils/api')));
|
|
428
|
+
const result = await sendEmail({
|
|
429
|
+
server: `${config.server}.mxrouting.net`,
|
|
430
|
+
username: config.username,
|
|
431
|
+
password: config.password,
|
|
432
|
+
from: config.username,
|
|
433
|
+
to: replyTo,
|
|
434
|
+
subject: replySubject,
|
|
435
|
+
body: `<pre style="font-family: system-ui, sans-serif; white-space: pre-wrap;">${escapeHtml(fullBody)}</pre>`,
|
|
436
|
+
});
|
|
437
|
+
if (result.success) {
|
|
438
|
+
sendSpinner.succeed(chalk_1.default.green(`Reply sent to ${replyTo}`));
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
sendSpinner.fail(chalk_1.default.red(result.message));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
sendSpinner.fail(chalk_1.default.red(err.message));
|
|
446
|
+
}
|
|
447
|
+
console.log('');
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
spinner.fail(chalk_1.default.red(`Failed: ${err.message}`));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ─── Forward ────────────────────────────────────────────
|
|
454
|
+
async function mailForward(uid) {
|
|
455
|
+
if (!uid) {
|
|
456
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Usage: mxroute mail forward <uid>\n`));
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
const uidNum = parseInt(uid, 10);
|
|
460
|
+
const config = (0, config_1.getConfig)();
|
|
461
|
+
const spinner = (0, ora_1.default)({ text: 'Fetching original message...', spinner: 'dots12', color: 'cyan' }).start();
|
|
462
|
+
try {
|
|
463
|
+
let originalMsg = null;
|
|
464
|
+
await withImap(async (client) => {
|
|
465
|
+
await client.selectFolder('INBOX');
|
|
466
|
+
const rawBody = await client.fetchBody(uidNum);
|
|
467
|
+
originalMsg = (0, mime_1.parseMessage)(rawBody);
|
|
468
|
+
});
|
|
469
|
+
spinner.stop();
|
|
470
|
+
if (!originalMsg) {
|
|
471
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Message not found.\n`));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const msg = originalMsg;
|
|
475
|
+
console.log(theme_1.theme.heading('Forward'));
|
|
476
|
+
console.log(theme_1.theme.keyValue('Subject', `Fwd: ${msg.subject}`));
|
|
477
|
+
console.log('');
|
|
478
|
+
const answers = await inquirer_1.default.prompt([
|
|
479
|
+
{
|
|
480
|
+
type: 'input',
|
|
481
|
+
name: 'to',
|
|
482
|
+
message: theme_1.theme.secondary('Forward to:'),
|
|
483
|
+
validate: (input) => (input.includes('@') ? true : 'Enter a valid email address'),
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
type: 'editor',
|
|
487
|
+
name: 'note',
|
|
488
|
+
message: theme_1.theme.secondary('Add a note (optional, opens editor):'),
|
|
489
|
+
},
|
|
490
|
+
]);
|
|
491
|
+
const { confirm } = await inquirer_1.default.prompt([
|
|
492
|
+
{ type: 'confirm', name: 'confirm', message: `Forward to ${answers.to}?`, default: true },
|
|
493
|
+
]);
|
|
494
|
+
if (!confirm) {
|
|
495
|
+
console.log(theme_1.theme.muted('\n Cancelled.\n'));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const originalBody = msg.textBody || (0, mime_1.htmlToText)(msg.htmlBody);
|
|
499
|
+
const forwarded = [
|
|
500
|
+
answers.note.trim() ? `${answers.note.trim()}\n\n` : '',
|
|
501
|
+
'---------- Forwarded message ----------',
|
|
502
|
+
`From: ${msg.from}`,
|
|
503
|
+
`Date: ${msg.date}`,
|
|
504
|
+
`Subject: ${msg.subject}`,
|
|
505
|
+
`To: ${msg.to}`,
|
|
506
|
+
'',
|
|
507
|
+
originalBody,
|
|
508
|
+
].join('\n');
|
|
509
|
+
const sendSpinner = (0, ora_1.default)({ text: 'Forwarding...', spinner: 'dots12', color: 'cyan' }).start();
|
|
510
|
+
try {
|
|
511
|
+
const { sendEmail } = await Promise.resolve().then(() => __importStar(require('../utils/api')));
|
|
512
|
+
const result = await sendEmail({
|
|
513
|
+
server: `${config.server}.mxrouting.net`,
|
|
514
|
+
username: config.username,
|
|
515
|
+
password: config.password,
|
|
516
|
+
from: config.username,
|
|
517
|
+
to: answers.to,
|
|
518
|
+
subject: `Fwd: ${msg.subject}`,
|
|
519
|
+
body: `<pre style="font-family: system-ui, sans-serif; white-space: pre-wrap;">${escapeHtml(forwarded)}</pre>`,
|
|
520
|
+
});
|
|
521
|
+
if (result.success) {
|
|
522
|
+
sendSpinner.succeed(chalk_1.default.green(`Forwarded to ${answers.to}`));
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
sendSpinner.fail(chalk_1.default.red(result.message));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
sendSpinner.fail(chalk_1.default.red(err.message));
|
|
530
|
+
}
|
|
531
|
+
console.log('');
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
spinner.fail(chalk_1.default.red(`Failed: ${err.message}`));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// ─── Delete ─────────────────────────────────────────────
|
|
538
|
+
async function mailDelete(uid) {
|
|
539
|
+
if (!uid) {
|
|
540
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Usage: mxroute mail delete <uid>\n`));
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
const uidNum = parseInt(uid, 10);
|
|
544
|
+
// Fetch subject first for confirmation
|
|
545
|
+
const spinner = (0, ora_1.default)({ text: 'Fetching message...', spinner: 'dots12', color: 'cyan' }).start();
|
|
546
|
+
try {
|
|
547
|
+
await withImap(async (client) => {
|
|
548
|
+
await client.selectFolder('INBOX');
|
|
549
|
+
const envelopes = await client.fetchEnvelopesByUid([uidNum]);
|
|
550
|
+
spinner.stop();
|
|
551
|
+
if (envelopes.length === 0) {
|
|
552
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Message #${uidNum} not found.\n`));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const env = envelopes[0];
|
|
556
|
+
console.log('');
|
|
557
|
+
console.log(theme_1.theme.keyValue('From', env.from));
|
|
558
|
+
console.log(theme_1.theme.keyValue('Subject', env.subject));
|
|
559
|
+
console.log(theme_1.theme.keyValue('Date', env.date));
|
|
560
|
+
console.log('');
|
|
561
|
+
const { confirm } = await inquirer_1.default.prompt([
|
|
562
|
+
{ type: 'confirm', name: 'confirm', message: 'Delete this message?', default: false },
|
|
563
|
+
]);
|
|
564
|
+
if (!confirm) {
|
|
565
|
+
console.log(theme_1.theme.muted('\n Cancelled.\n'));
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const delSpinner = (0, ora_1.default)({ text: 'Deleting...', spinner: 'dots12', color: 'cyan' }).start();
|
|
569
|
+
await client.deleteMessage(uidNum);
|
|
570
|
+
delSpinner.succeed(chalk_1.default.green('Message deleted'));
|
|
571
|
+
console.log('');
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
spinner.fail(chalk_1.default.red(`Failed: ${err.message}`));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// ─── Search ─────────────────────────────────────────────
|
|
579
|
+
async function mailSearch(query) {
|
|
580
|
+
if (!query) {
|
|
581
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Usage: mxroute mail search <query>\n`));
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
console.log(theme_1.theme.heading(`Search: "${query}"`));
|
|
585
|
+
const spinner = (0, ora_1.default)({ text: 'Searching...', spinner: 'dots12', color: 'cyan' }).start();
|
|
586
|
+
try {
|
|
587
|
+
await withImap(async (client) => {
|
|
588
|
+
await client.selectFolder('INBOX');
|
|
589
|
+
// Sanitize search query: escape quotes and reject CRLF
|
|
590
|
+
const sanitized = query.replace(/[\r\n]/g, '').replace(/"/g, '\\"');
|
|
591
|
+
const criteria = `OR OR SUBJECT "${sanitized}" FROM "${sanitized}" BODY "${sanitized}"`;
|
|
592
|
+
const uids = await client.search(criteria);
|
|
593
|
+
if (uids.length === 0) {
|
|
594
|
+
spinner.stop();
|
|
595
|
+
console.log(theme_1.theme.muted(` No messages matching "${query}".\n`));
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
// Fetch envelopes for results (limit to 50)
|
|
599
|
+
const limitedUids = uids.slice(-50);
|
|
600
|
+
const envelopes = await client.fetchEnvelopesByUid(limitedUids);
|
|
601
|
+
spinner.stop();
|
|
602
|
+
console.log(theme_1.theme.muted(` Found ${uids.length} match${uids.length === 1 ? '' : 'es'}:\n`));
|
|
603
|
+
// Sort newest first
|
|
604
|
+
envelopes.sort((a, b) => b.uid - a.uid);
|
|
605
|
+
for (const env of envelopes) {
|
|
606
|
+
const isUnread = !env.flags.includes('\\Seen');
|
|
607
|
+
const marker = isUnread ? chalk_1.default.cyan('\u25cf') : ' ';
|
|
608
|
+
const uid = theme_1.theme.muted(`#${env.uid}`);
|
|
609
|
+
const from = truncate(extractName(env.from), 20).padEnd(20);
|
|
610
|
+
const subject = truncate(env.subject || '(no subject)', 45);
|
|
611
|
+
const date = formatDate(env.date);
|
|
612
|
+
console.log(` ${marker} ${uid.padEnd(14)} ${from} ${subject} ${theme_1.theme.muted(date)}`);
|
|
613
|
+
}
|
|
614
|
+
console.log('');
|
|
615
|
+
console.log(theme_1.theme.muted(` Read: ${theme_1.theme.bold('mxroute mail read <uid>')}\n`));
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
catch (err) {
|
|
619
|
+
spinner.fail(chalk_1.default.red('Search failed'));
|
|
620
|
+
console.log(theme_1.theme.error(` ${err.message}\n`));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// ─── Folders ────────────────────────────────────────────
|
|
624
|
+
async function mailFolders() {
|
|
625
|
+
console.log(theme_1.theme.heading('Mailbox Folders'));
|
|
626
|
+
const spinner = (0, ora_1.default)({ text: 'Fetching folders...', spinner: 'dots12', color: 'cyan' }).start();
|
|
627
|
+
try {
|
|
628
|
+
await withImap(async (client) => {
|
|
629
|
+
const folders = await client.listFolders();
|
|
630
|
+
spinner.stop();
|
|
631
|
+
if (folders.length === 0) {
|
|
632
|
+
console.log(theme_1.theme.muted(' No folders found.\n'));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
for (const folder of folders) {
|
|
636
|
+
const isSpecial = folder.flags.includes('\\Noselect') || folder.flags.includes('\\NonExistent');
|
|
637
|
+
const icon = isSpecial ? theme_1.theme.muted('\u25cb') : theme_1.theme.statusIcon('info');
|
|
638
|
+
const flags = folder.flags.length > 0 ? theme_1.theme.muted(` (${folder.flags.join(', ')})`) : '';
|
|
639
|
+
console.log(` ${icon} ${theme_1.theme.bold(folder.name)}${flags}`);
|
|
640
|
+
}
|
|
641
|
+
console.log('');
|
|
642
|
+
console.log(theme_1.theme.muted(` View folder: ${theme_1.theme.bold('mxroute mail inbox <folder>')}`));
|
|
643
|
+
console.log(theme_1.theme.muted(` Create: ${theme_1.theme.bold('mxroute mail folder-create <name>')}`));
|
|
644
|
+
console.log(theme_1.theme.muted(` Delete: ${theme_1.theme.bold('mxroute mail folder-delete <name>')}\n`));
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
catch (err) {
|
|
648
|
+
spinner.fail(chalk_1.default.red('Failed to list folders'));
|
|
649
|
+
console.log(theme_1.theme.error(` ${err.message}\n`));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
async function mailFolderCreate(name) {
|
|
653
|
+
if (!name) {
|
|
654
|
+
const { folderName } = await inquirer_1.default.prompt([
|
|
655
|
+
{
|
|
656
|
+
type: 'input',
|
|
657
|
+
name: 'folderName',
|
|
658
|
+
message: theme_1.theme.secondary('Folder name:'),
|
|
659
|
+
validate: (input) => (input.trim() ? true : 'Folder name is required'),
|
|
660
|
+
},
|
|
661
|
+
]);
|
|
662
|
+
name = folderName;
|
|
663
|
+
}
|
|
664
|
+
const spinner = (0, ora_1.default)({ text: `Creating folder "${name}"...`, spinner: 'dots12', color: 'cyan' }).start();
|
|
665
|
+
try {
|
|
666
|
+
await withImap(async (client) => {
|
|
667
|
+
await client.createFolder(name);
|
|
668
|
+
});
|
|
669
|
+
spinner.succeed(chalk_1.default.green(`Folder "${name}" created`));
|
|
670
|
+
}
|
|
671
|
+
catch (err) {
|
|
672
|
+
spinner.fail(chalk_1.default.red(`Failed: ${err.message}`));
|
|
673
|
+
}
|
|
674
|
+
console.log('');
|
|
675
|
+
}
|
|
676
|
+
async function mailFolderDelete(name) {
|
|
677
|
+
if (!name) {
|
|
678
|
+
// List folders first for selection
|
|
679
|
+
try {
|
|
680
|
+
const folders = await withImap(async (client) => client.listFolders());
|
|
681
|
+
const selectable = folders.filter((f) => !f.flags.includes('\\Noselect') && f.name !== 'INBOX');
|
|
682
|
+
if (selectable.length === 0) {
|
|
683
|
+
console.log(theme_1.theme.muted('\n No deletable folders.\n'));
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const { selected } = await inquirer_1.default.prompt([
|
|
687
|
+
{
|
|
688
|
+
type: 'list',
|
|
689
|
+
name: 'selected',
|
|
690
|
+
message: 'Select folder to delete:',
|
|
691
|
+
choices: selectable.map((f) => f.name),
|
|
692
|
+
},
|
|
693
|
+
]);
|
|
694
|
+
name = selected;
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
console.log(theme_1.theme.error(` ${err.message}\n`));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const { confirm } = await inquirer_1.default.prompt([
|
|
702
|
+
{
|
|
703
|
+
type: 'confirm',
|
|
704
|
+
name: 'confirm',
|
|
705
|
+
message: `Delete folder "${name}" and all its contents?`,
|
|
706
|
+
default: false,
|
|
707
|
+
},
|
|
708
|
+
]);
|
|
709
|
+
if (!confirm) {
|
|
710
|
+
console.log(theme_1.theme.muted('\n Cancelled.\n'));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const spinner = (0, ora_1.default)({ text: `Deleting folder "${name}"...`, spinner: 'dots12', color: 'cyan' }).start();
|
|
714
|
+
try {
|
|
715
|
+
await withImap(async (client) => {
|
|
716
|
+
await client.deleteFolder(name);
|
|
717
|
+
});
|
|
718
|
+
spinner.succeed(chalk_1.default.green(`Folder "${name}" deleted`));
|
|
719
|
+
}
|
|
720
|
+
catch (err) {
|
|
721
|
+
spinner.fail(chalk_1.default.red(`Failed: ${err.message}`));
|
|
722
|
+
}
|
|
723
|
+
console.log('');
|
|
724
|
+
}
|
|
725
|
+
// ─── Move ───────────────────────────────────────────────
|
|
726
|
+
async function mailMove(uid, folder) {
|
|
727
|
+
if (!uid) {
|
|
728
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Usage: mxroute mail move <uid> <folder>\n`));
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
const uidNum = parseInt(uid, 10);
|
|
732
|
+
if (!folder) {
|
|
733
|
+
try {
|
|
734
|
+
const folders = await withImap(async (client) => client.listFolders());
|
|
735
|
+
const selectable = folders.filter((f) => !f.flags.includes('\\Noselect'));
|
|
736
|
+
const { selected } = await inquirer_1.default.prompt([
|
|
737
|
+
{
|
|
738
|
+
type: 'list',
|
|
739
|
+
name: 'selected',
|
|
740
|
+
message: 'Move to folder:',
|
|
741
|
+
choices: selectable.map((f) => f.name),
|
|
742
|
+
},
|
|
743
|
+
]);
|
|
744
|
+
folder = selected;
|
|
745
|
+
}
|
|
746
|
+
catch (err) {
|
|
747
|
+
console.log(theme_1.theme.error(` ${err.message}\n`));
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const spinner = (0, ora_1.default)({
|
|
752
|
+
text: `Moving message #${uidNum} to ${folder}...`,
|
|
753
|
+
spinner: 'dots12',
|
|
754
|
+
color: 'cyan',
|
|
755
|
+
}).start();
|
|
756
|
+
try {
|
|
757
|
+
await withImap(async (client) => {
|
|
758
|
+
await client.selectFolder('INBOX');
|
|
759
|
+
await client.moveMessage(uidNum, folder);
|
|
760
|
+
});
|
|
761
|
+
spinner.succeed(chalk_1.default.green(`Message moved to ${folder}`));
|
|
762
|
+
}
|
|
763
|
+
catch (err) {
|
|
764
|
+
spinner.fail(chalk_1.default.red(`Failed: ${err.message}`));
|
|
765
|
+
}
|
|
766
|
+
console.log('');
|
|
767
|
+
}
|
|
768
|
+
// ─── Save Attachment ────────────────────────────────────
|
|
769
|
+
async function mailSaveAttachment(uid) {
|
|
770
|
+
if (!uid) {
|
|
771
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Usage: mxroute mail save-attachment <uid>\n`));
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
const uidNum = parseInt(uid, 10);
|
|
775
|
+
const spinner = (0, ora_1.default)({ text: 'Fetching message...', spinner: 'dots12', color: 'cyan' }).start();
|
|
776
|
+
try {
|
|
777
|
+
await withImap(async (client) => {
|
|
778
|
+
await client.selectFolder('INBOX');
|
|
779
|
+
const rawBody = await client.fetchBody(uidNum);
|
|
780
|
+
spinner.stop();
|
|
781
|
+
const msg = (0, mime_1.parseMessage)(rawBody);
|
|
782
|
+
if (msg.attachments.length === 0) {
|
|
783
|
+
console.log(theme_1.theme.muted('\n No attachments in this message.\n'));
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
console.log(theme_1.theme.heading(`Attachments (${msg.attachments.length})`));
|
|
787
|
+
const { selected } = await inquirer_1.default.prompt([
|
|
788
|
+
{
|
|
789
|
+
type: 'checkbox',
|
|
790
|
+
name: 'selected',
|
|
791
|
+
message: 'Select attachments to save:',
|
|
792
|
+
choices: [
|
|
793
|
+
{ name: 'All attachments', value: '__ALL__' },
|
|
794
|
+
...msg.attachments.map((a, i) => ({
|
|
795
|
+
name: `${a.filename} (${(0, mime_1.formatFileSize)(a.size)})`,
|
|
796
|
+
value: i,
|
|
797
|
+
})),
|
|
798
|
+
],
|
|
799
|
+
},
|
|
800
|
+
]);
|
|
801
|
+
let indices = selected;
|
|
802
|
+
if (selected.includes('__ALL__')) {
|
|
803
|
+
indices = msg.attachments.map((_, i) => i);
|
|
804
|
+
}
|
|
805
|
+
if (indices.length === 0) {
|
|
806
|
+
console.log(theme_1.theme.muted('\n No attachments selected.\n'));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const { outputDir } = await inquirer_1.default.prompt([
|
|
810
|
+
{
|
|
811
|
+
type: 'input',
|
|
812
|
+
name: 'outputDir',
|
|
813
|
+
message: theme_1.theme.secondary('Save to directory:'),
|
|
814
|
+
default: '.',
|
|
815
|
+
},
|
|
816
|
+
]);
|
|
817
|
+
const resolved = path.resolve(outputDir);
|
|
818
|
+
if (!fs.existsSync(resolved)) {
|
|
819
|
+
fs.mkdirSync(resolved, { recursive: true });
|
|
820
|
+
}
|
|
821
|
+
for (const idx of indices) {
|
|
822
|
+
if (typeof idx !== 'number')
|
|
823
|
+
continue;
|
|
824
|
+
const att = msg.attachments[idx];
|
|
825
|
+
// Sanitize filename to prevent path traversal
|
|
826
|
+
const safeFilename = path.basename(att.filename) || 'attachment';
|
|
827
|
+
const filePath = path.join(resolved, safeFilename);
|
|
828
|
+
fs.writeFileSync(filePath, att.content);
|
|
829
|
+
console.log(theme_1.theme.success(` ${theme_1.theme.statusIcon('pass')} ${att.filename} (${(0, mime_1.formatFileSize)(att.size)})`));
|
|
830
|
+
}
|
|
831
|
+
console.log('');
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
spinner.fail(chalk_1.default.red(`Failed: ${err.message}`));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// ─── Unread Count ───────────────────────────────────────
|
|
839
|
+
async function mailUnread() {
|
|
840
|
+
const spinner = (0, ora_1.default)({ text: 'Checking unread...', spinner: 'dots12', color: 'cyan' }).start();
|
|
841
|
+
try {
|
|
842
|
+
await withImap(async (client) => {
|
|
843
|
+
const info = await client.selectFolder('INBOX');
|
|
844
|
+
const unreadUids = await client.search('UNSEEN');
|
|
845
|
+
spinner.stop();
|
|
846
|
+
const lines = [
|
|
847
|
+
theme_1.theme.keyValue('Total Messages', info.exists.toString(), 0),
|
|
848
|
+
theme_1.theme.keyValue('Unread', unreadUids.length > 0 ? chalk_1.default.cyan.bold(unreadUids.length.toString()) : '0', 0),
|
|
849
|
+
theme_1.theme.keyValue('Recent', info.recent.toString(), 0),
|
|
850
|
+
];
|
|
851
|
+
console.log(theme_1.theme.box(lines.join('\n'), 'Inbox Status'));
|
|
852
|
+
console.log('');
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
catch (err) {
|
|
856
|
+
spinner.fail(chalk_1.default.red(`Failed: ${err.message}`));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// ─── SMTP Send Helper ──────────────────────────────────
|
|
860
|
+
async function sendViaSMTP(config, to, cc, bcc, mimeMessage) {
|
|
861
|
+
const host = `${config.server}.mxrouting.net`;
|
|
862
|
+
const port = 465;
|
|
863
|
+
return new Promise((resolve, reject) => {
|
|
864
|
+
const socket = tls.connect({ host, port, servername: host }, () => {
|
|
865
|
+
let step = 0;
|
|
866
|
+
let buffer = '';
|
|
867
|
+
const allRecipients = [to];
|
|
868
|
+
if (cc)
|
|
869
|
+
allRecipients.push(...cc.split(',').map((s) => s.trim()));
|
|
870
|
+
if (bcc)
|
|
871
|
+
allRecipients.push(...bcc.split(',').map((s) => s.trim()));
|
|
872
|
+
// Reject CRLF in email addresses to prevent SMTP command injection
|
|
873
|
+
for (const addr of allRecipients) {
|
|
874
|
+
if (/[\r\n]/.test(addr)) {
|
|
875
|
+
reject(new Error('Invalid email address: contains newline characters'));
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
let recipientIdx = 0;
|
|
880
|
+
function processLine(line) {
|
|
881
|
+
const code = parseInt(line.substring(0, 3), 10);
|
|
882
|
+
// Handle multi-line responses
|
|
883
|
+
if (line[3] === '-')
|
|
884
|
+
return;
|
|
885
|
+
if (step === 0 && code === 220) {
|
|
886
|
+
step = 1;
|
|
887
|
+
socket.write(`EHLO mxroute-cli.local\r\n`);
|
|
888
|
+
}
|
|
889
|
+
else if (step === 1 && code === 250) {
|
|
890
|
+
step = 2;
|
|
891
|
+
socket.write('AUTH LOGIN\r\n');
|
|
892
|
+
}
|
|
893
|
+
else if (step === 2 && code === 334) {
|
|
894
|
+
step = 3;
|
|
895
|
+
socket.write(Buffer.from(config.username).toString('base64') + '\r\n');
|
|
896
|
+
}
|
|
897
|
+
else if (step === 3 && code === 334) {
|
|
898
|
+
step = 4;
|
|
899
|
+
socket.write(Buffer.from(config.password).toString('base64') + '\r\n');
|
|
900
|
+
}
|
|
901
|
+
else if (step === 4 && code === 235) {
|
|
902
|
+
step = 5;
|
|
903
|
+
socket.write(`MAIL FROM:<${config.username}>\r\n`);
|
|
904
|
+
}
|
|
905
|
+
else if (step === 4 && code >= 400) {
|
|
906
|
+
socket.destroy();
|
|
907
|
+
reject(new Error('SMTP authentication failed'));
|
|
908
|
+
}
|
|
909
|
+
else if (step === 5 && code === 250) {
|
|
910
|
+
step = 6;
|
|
911
|
+
recipientIdx = 0;
|
|
912
|
+
socket.write(`RCPT TO:<${extractEmail(allRecipients[recipientIdx])}>\r\n`);
|
|
913
|
+
}
|
|
914
|
+
else if (step === 6 && code === 250) {
|
|
915
|
+
recipientIdx++;
|
|
916
|
+
if (recipientIdx < allRecipients.length) {
|
|
917
|
+
socket.write(`RCPT TO:<${extractEmail(allRecipients[recipientIdx])}>\r\n`);
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
step = 7;
|
|
921
|
+
socket.write('DATA\r\n');
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
else if (step === 7 && code === 354) {
|
|
925
|
+
step = 8;
|
|
926
|
+
socket.write(mimeMessage + '\r\n.\r\n');
|
|
927
|
+
}
|
|
928
|
+
else if (step === 8 && code === 250) {
|
|
929
|
+
step = 9;
|
|
930
|
+
socket.write('QUIT\r\n');
|
|
931
|
+
}
|
|
932
|
+
else if (step === 9) {
|
|
933
|
+
socket.destroy();
|
|
934
|
+
resolve();
|
|
935
|
+
}
|
|
936
|
+
else if (code >= 400) {
|
|
937
|
+
socket.destroy();
|
|
938
|
+
reject(new Error(`SMTP error ${code}: ${line}`));
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
socket.on('data', (data) => {
|
|
942
|
+
buffer += data.toString();
|
|
943
|
+
const lines = buffer.split('\r\n');
|
|
944
|
+
buffer = lines.pop() || '';
|
|
945
|
+
for (const line of lines) {
|
|
946
|
+
if (line)
|
|
947
|
+
processLine(line);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
socket.setTimeout(30000);
|
|
952
|
+
socket.on('timeout', () => {
|
|
953
|
+
socket.destroy();
|
|
954
|
+
reject(new Error('SMTP connection timed out'));
|
|
955
|
+
});
|
|
956
|
+
socket.on('error', (err) => reject(err));
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
// ─── Mark Read/Unread ───────────────────────────────────
|
|
960
|
+
async function mailMarkRead(uid) {
|
|
961
|
+
if (!uid) {
|
|
962
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Usage: mxroute mail mark-read <uid>\n`));
|
|
963
|
+
process.exit(1);
|
|
964
|
+
}
|
|
965
|
+
const uidNum = parseInt(uid, 10);
|
|
966
|
+
const spinner = (0, ora_1.default)({ text: 'Marking as read...', spinner: 'dots12', color: 'cyan' }).start();
|
|
967
|
+
try {
|
|
968
|
+
await withImap(async (client) => {
|
|
969
|
+
await client.selectFolder('INBOX');
|
|
970
|
+
await client.setFlags(uidNum, '\\Seen');
|
|
971
|
+
});
|
|
972
|
+
spinner.succeed(chalk_1.default.green(`Message #${uidNum} marked as read`));
|
|
973
|
+
}
|
|
974
|
+
catch (err) {
|
|
975
|
+
spinner.fail(chalk_1.default.red(`Failed: ${err.message}`));
|
|
976
|
+
}
|
|
977
|
+
console.log('');
|
|
978
|
+
}
|
|
979
|
+
async function mailMarkUnread(uid) {
|
|
980
|
+
if (!uid) {
|
|
981
|
+
console.log(theme_1.theme.error(`\n ${theme_1.theme.statusIcon('fail')} Usage: mxroute mail mark-unread <uid>\n`));
|
|
982
|
+
process.exit(1);
|
|
983
|
+
}
|
|
984
|
+
const uidNum = parseInt(uid, 10);
|
|
985
|
+
const spinner = (0, ora_1.default)({ text: 'Marking as unread...', spinner: 'dots12', color: 'cyan' }).start();
|
|
986
|
+
try {
|
|
987
|
+
await withImap(async (client) => {
|
|
988
|
+
await client.selectFolder('INBOX');
|
|
989
|
+
await client.setFlags(uidNum, '\\Seen', '-');
|
|
990
|
+
});
|
|
991
|
+
spinner.succeed(chalk_1.default.green(`Message #${uidNum} marked as unread`));
|
|
992
|
+
}
|
|
993
|
+
catch (err) {
|
|
994
|
+
spinner.fail(chalk_1.default.red(`Failed: ${err.message}`));
|
|
995
|
+
}
|
|
996
|
+
console.log('');
|
|
997
|
+
}
|
|
998
|
+
//# sourceMappingURL=mail.js.map
|