dooray-mail-cli 0.1.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 ADDED
@@ -0,0 +1,19 @@
1
+ # Dooray OpenClaw Skill
2
+
3
+ Dooray mail CLI for OpenClaw - IMAP/SMTP integration
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g .
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ dooray-cli config # Setup
15
+ dooray-cli recent # List recent mails
16
+ dooray-cli test # Test connection
17
+ ```
18
+
19
+ See `SKILL.md` for OpenClaw integration.
package/SKILL.md ADDED
@@ -0,0 +1,195 @@
1
+ ---
2
+ name: dooray
3
+ description: "Manage Dooray emails via IMAP/SMTP using the dooray-cli command. List, read, send, and search emails from the terminal. Use when working with Dooray email, checking Dooray inbox, or when the user mentions Dooray mail operations."
4
+ homepage: https://dooray.com
5
+ metadata:
6
+ {
7
+ "openclaw":
8
+ {
9
+ "emoji": "šŸ“§",
10
+ "requires": { "bins": ["dooray-cli"] },
11
+ "install":
12
+ [
13
+ {
14
+ "id": "npm-global",
15
+ "kind": "npm",
16
+ "package": "dooray-mail-cli",
17
+ "bins": ["dooray-cli"],
18
+ "label": "Install Dooray CLI (npm)",
19
+ },
20
+ ],
21
+ },
22
+ }
23
+ ---
24
+
25
+ # Dooray Email CLI
26
+
27
+ Dooray CLI is a command-line tool for managing Dooray emails using IMAP/SMTP protocols.
28
+
29
+ ## Prerequisites
30
+
31
+ 1. Dooray CLI installed (`dooray-cli --help` to verify)
32
+ 2. Dooray email account with IMAP/SMTP access enabled
33
+ 3. Configuration file setup (created via `dooray-cli config`)
34
+
35
+ ## Configuration Setup
36
+
37
+ Run the interactive configuration wizard:
38
+
39
+ ```bash
40
+ dooray-cli config
41
+ ```
42
+
43
+ This will:
44
+ - Prompt for your Dooray email address
45
+ - Securely encrypt and store your password
46
+ - Save configuration to `~/.dooray-config.json`
47
+
48
+ **Default Dooray IMAP/SMTP settings:**
49
+ - IMAP: `imap.dooray.com:993` (SSL)
50
+ - SMTP: `smtp.dooray.com:465` (SSL)
51
+
52
+ ## Common Operations
53
+
54
+ ### List Recent Emails (All)
55
+
56
+ List 10 most recent emails (read and unread):
57
+
58
+ ```bash
59
+ dooray-cli recent
60
+ ```
61
+
62
+ Output format:
63
+ ```
64
+ šŸ“¬ Recent Mails (10)
65
+
66
+ 1. āœ“ [UID: 279] sender@example.com <sender@example.com>
67
+ Subject: Project Update
68
+ Date: 2026-02-09T08:29:51.000Z
69
+
70
+ 2. āœ“ [UID: 278] John Doe <john.doe@example.com>
71
+ Subject: Meeting Schedule - 02.12(Thu) 03:30 PM
72
+ Date: 2026-02-09T07:00:47.000Z
73
+ ```
74
+
75
+ **āœ“** = Read, **ā—** = Unread
76
+
77
+ ### List Unread Emails Only
78
+
79
+ List up to 10 most recent unread emails:
80
+
81
+ ```bash
82
+ dooray-cli list
83
+ ```
84
+
85
+ Output format:
86
+ ```
87
+ šŸ“¬ Unread Mails (3)
88
+
89
+ 1. [UID: 123] sender@example.com <sender@example.com>
90
+ Subject: Project Update
91
+ Date: 2026-02-09T10:30:00.000Z
92
+
93
+ 2. [UID: 124] colleague@example.com <colleague@example.com>
94
+ Subject: Meeting Notes
95
+ Date: 2026-02-09T11:15:00.000Z
96
+ ```
97
+
98
+ ### Read a Specific Email
99
+
100
+ Read email by UID (from list output):
101
+
102
+ ```bash
103
+ dooray-cli read 123
104
+ ```
105
+
106
+ ### Send a New Email
107
+
108
+ **Non-interactive (recommended for automation):**
109
+
110
+ ```bash
111
+ dooray-cli send --to "recipient@example.com" --subject "Subject here" --body "Message body here"
112
+ ```
113
+
114
+ **Interactive mode:**
115
+
116
+ ```bash
117
+ dooray-cli send
118
+ ```
119
+
120
+ You'll be prompted for:
121
+ - To: recipient email address
122
+ - Subject: email subject
123
+ - Body: email content (press Ctrl+D when done)
124
+
125
+ ### Search Emails
126
+
127
+ Search for emails by keywords in subject:
128
+
129
+ ```bash
130
+ dooray-cli search "meeting"
131
+ dooray-cli search "project" "update"
132
+ ```
133
+
134
+ ### Check Unread Count
135
+
136
+ Get total unread email count:
137
+
138
+ ```bash
139
+ dooray-cli unread
140
+ ```
141
+
142
+ Output: `šŸ“¬ Unread mails: 5`
143
+
144
+ ### Test Connection
145
+
146
+ Verify IMAP/SMTP connection:
147
+
148
+ ```bash
149
+ dooray-cli test
150
+ ```
151
+
152
+ ## Output Formats
153
+
154
+ All commands output to stdout with emoji indicators:
155
+
156
+ - `āœ…` Success
157
+ - `āŒ` Error
158
+ - `šŸ“¬` Unread mail info
159
+ - `šŸ“§` Mail details
160
+ - `šŸ”` Search results
161
+
162
+ ## Error Handling
163
+
164
+ Common errors:
165
+
166
+ **Configuration not found:**
167
+ ```
168
+ āŒ Configuration not found. Run: dooray-cli config
169
+ ```
170
+ Solution: Run `dooray-cli config` to set up credentials.
171
+
172
+ **Connection failed:**
173
+ ```
174
+ āŒ IMAP connection failed
175
+ ```
176
+ Solution: Check network, verify credentials, or run `dooray-cli test`.
177
+
178
+ **Invalid UID:**
179
+ ```
180
+ āŒ Usage: dooray-cli read <uid>
181
+ ```
182
+ Solution: Use `dooray-cli list` to get valid UIDs.
183
+
184
+ ## Tips
185
+
186
+ - UIDs are relative to the inbox folder
187
+ - Password is encrypted using AES-256 and stored locally
188
+ - Configuration file: `~/.dooray-config.json` (Windows: `%USERPROFILE%\.dooray-config.json`)
189
+ - Run `dooray-cli help` for quick reference
190
+
191
+ ## Security Notes
192
+
193
+ - Never share your `.dooray-config.json` file
194
+ - Password is encrypted but stored on local filesystem
195
+ - Use app-specific passwords if 2FA is enabled on Dooray account
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+ /**
3
+ * Dooray Mail Client (IMAP/SMTP)
4
+ *
5
+ * IMAP/SMTP ķ”„ė”œķ† ģ½œģ„ ģ‚¬ģš©ķ•˜ģ—¬ Dooray ė©”ģ¼ź³¼ ķ†µģ‹ ķ•©ė‹ˆė‹¤.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.DoorayMailClient = void 0;
42
+ const nodemailer = __importStar(require("nodemailer"));
43
+ const imaps = __importStar(require("imap-simple"));
44
+ const mailparser_1 = require("mailparser");
45
+ class DoorayMailClient {
46
+ constructor(config) {
47
+ this.config = config;
48
+ // SMTP 설정
49
+ this.smtpTransporter = nodemailer.createTransport({
50
+ host: config.smtp.host,
51
+ port: config.smtp.port,
52
+ secure: config.smtp.secure,
53
+ auth: {
54
+ user: config.email,
55
+ pass: config.password
56
+ },
57
+ tls: {
58
+ rejectUnauthorized: false // Dooray SSL ģøģ¦ģ„œ 문제 ķ•“ź²°
59
+ }
60
+ });
61
+ }
62
+ /**
63
+ * IMAP ģ—°ź²° ģƒģ„±
64
+ */
65
+ async getImapConnection() {
66
+ const imapConfig = {
67
+ imap: {
68
+ user: this.config.email,
69
+ password: this.config.password,
70
+ host: this.config.imap.host,
71
+ port: this.config.imap.port,
72
+ tls: this.config.imap.secure,
73
+ tlsOptions: {
74
+ rejectUnauthorized: false
75
+ },
76
+ authTimeout: 30000
77
+ }
78
+ };
79
+ try {
80
+ const connection = await imaps.connect(imapConfig);
81
+ return connection;
82
+ }
83
+ catch (error) {
84
+ const err = error;
85
+ console.error('[Dooray IMAP] Connection failed:', err.message);
86
+ throw new Error(`IMAP ģ—°ź²° ģ‹¤ķŒØ: ${err.message}`);
87
+ }
88
+ }
89
+ /**
90
+ * ģ½ģ§€ ģ•Šģ€ ė©”ģ¼ 개수
91
+ */
92
+ async getUnreadCount() {
93
+ let connection;
94
+ try {
95
+ connection = await this.getImapConnection();
96
+ await connection.openBox('INBOX');
97
+ const searchCriteria = ['UNSEEN'];
98
+ const fetchOptions = {
99
+ bodies: [''],
100
+ struct: true
101
+ };
102
+ const messages = await connection.search(searchCriteria, fetchOptions);
103
+ connection.end();
104
+ return messages.length;
105
+ }
106
+ catch (error) {
107
+ const err = error;
108
+ console.error('[Dooray] Failed to get unread count:', err.message);
109
+ if (connection)
110
+ connection.end();
111
+ return 0;
112
+ }
113
+ }
114
+ /**
115
+ * ģ½ģ§€ ģ•Šģ€ ė©”ģ¼ ėŖ©ė”
116
+ */
117
+ async getUnreadMail(limit = 10) {
118
+ let connection;
119
+ try {
120
+ connection = await this.getImapConnection();
121
+ await connection.openBox('INBOX');
122
+ const searchCriteria = ['UNSEEN'];
123
+ const fetchOptions = {
124
+ bodies: ['HEADER', 'TEXT', ''],
125
+ struct: true,
126
+ markSeen: false
127
+ };
128
+ const messages = await connection.search(searchCriteria, fetchOptions);
129
+ // ģµœģ‹  ė©”ģ¼ė¶€ķ„° limit개만
130
+ const limitedMessages = messages.slice(0, limit);
131
+ const parsedMails = [];
132
+ for (const item of limitedMessages) {
133
+ const mail = await this.parseImapMessage(item);
134
+ if (mail)
135
+ parsedMails.push(mail);
136
+ }
137
+ connection.end();
138
+ return parsedMails;
139
+ }
140
+ catch (error) {
141
+ const err = error;
142
+ console.error('[Dooray] Failed to get unread mail:', err.message);
143
+ if (connection)
144
+ connection.end();
145
+ return [];
146
+ }
147
+ }
148
+ /**
149
+ * 최근 ė©”ģ¼ ź°€ģ øģ˜¤źø° (ģ½ģŒ/ģ•ˆģ½ģŒ 모두)
150
+ */
151
+ async getRecentMail(limit = 10) {
152
+ let connection;
153
+ try {
154
+ connection = await this.getImapConnection();
155
+ const box = await connection.openBox('INBOX');
156
+ // ė©”ģ¼ķ•Øģ˜ 전첓 ė©”ģ‹œģ§€ 수
157
+ const totalMessages = box.messages.total;
158
+ if (totalMessages === 0) {
159
+ connection.end();
160
+ return [];
161
+ }
162
+ // 최근 Nź°œģ˜ UID ė²”ģœ„ 계산
163
+ const startSeq = Math.max(1, totalMessages - limit + 1);
164
+ const endSeq = totalMessages;
165
+ const searchCriteria = [[`${startSeq}:${endSeq}`]];
166
+ const fetchOptions = {
167
+ bodies: ['HEADER', 'TEXT', ''],
168
+ struct: true,
169
+ markSeen: false
170
+ };
171
+ const messages = await connection.search(searchCriteria, fetchOptions);
172
+ // ģµœģ‹  ė©”ģ¼ė¶€ķ„° ģ •ė ¬
173
+ const recentMessages = messages.reverse();
174
+ const parsedMails = [];
175
+ for (const item of recentMessages) {
176
+ const mail = await this.parseImapMessage(item);
177
+ if (mail)
178
+ parsedMails.push(mail);
179
+ }
180
+ connection.end();
181
+ return parsedMails;
182
+ }
183
+ catch (error) {
184
+ const err = error;
185
+ console.error('[Dooray] Failed to get recent mail:', err.message);
186
+ if (connection)
187
+ connection.end();
188
+ return [];
189
+ }
190
+ }
191
+ /**
192
+ * ė©”ģ¼ ID딜 씰회
193
+ */
194
+ async getMailById(mailId) {
195
+ let connection;
196
+ try {
197
+ connection = await this.getImapConnection();
198
+ await connection.openBox('INBOX');
199
+ const uid = parseInt(mailId);
200
+ const fetchOptions = {
201
+ bodies: ['HEADER', 'TEXT', ''],
202
+ struct: true
203
+ };
204
+ const messages = await connection.search([['UID', uid]], fetchOptions);
205
+ if (messages.length === 0) {
206
+ connection.end();
207
+ return null;
208
+ }
209
+ const mail = await this.parseImapMessage(messages[0]);
210
+ connection.end();
211
+ return mail;
212
+ }
213
+ catch (error) {
214
+ const err = error;
215
+ console.error('[Dooray] Failed to get mail by ID:', err.message);
216
+ if (connection)
217
+ connection.end();
218
+ return null;
219
+ }
220
+ }
221
+ /**
222
+ * ė©”ģ¼ ė°œģ†”
223
+ */
224
+ async sendMail(params) {
225
+ try {
226
+ const recipients = Array.isArray(params.to) ? params.to : [params.to];
227
+ const mailOptions = {
228
+ from: this.config.email,
229
+ to: recipients.join(', '),
230
+ subject: params.subject,
231
+ text: params.html ? undefined : params.body,
232
+ html: params.html ? params.body : undefined,
233
+ cc: params.cc?.join(', '),
234
+ bcc: params.bcc?.join(', '),
235
+ attachments: params.attachments
236
+ };
237
+ const info = await this.smtpTransporter.sendMail(mailOptions);
238
+ console.log('[Dooray] Mail sent:', info.messageId);
239
+ return {
240
+ success: true,
241
+ mailId: info.messageId
242
+ };
243
+ }
244
+ catch (error) {
245
+ const err = error;
246
+ console.error('[Dooray] Failed to send mail:', err.message);
247
+ return { success: false };
248
+ }
249
+ }
250
+ /**
251
+ * ė©”ģ¼ ģ½ģŒ 처리
252
+ */
253
+ async markAsRead(mailId) {
254
+ let connection;
255
+ try {
256
+ connection = await this.getImapConnection();
257
+ await connection.openBox('INBOX');
258
+ const uid = parseInt(mailId);
259
+ await connection.addFlags(uid, '\\Seen');
260
+ connection.end();
261
+ return true;
262
+ }
263
+ catch (error) {
264
+ const err = error;
265
+ console.error('[Dooray] Failed to mark as read:', err.message);
266
+ if (connection)
267
+ connection.end();
268
+ return false;
269
+ }
270
+ }
271
+ /**
272
+ * ė©”ģ¼ ź²€ģƒ‰
273
+ */
274
+ async searchMail(query, limit = 10) {
275
+ let connection;
276
+ try {
277
+ connection = await this.getImapConnection();
278
+ await connection.openBox('INBOX');
279
+ // IMAP SEARCH: ģ œėŖ©ģ“ė‚˜ ė³øė¬øģ—ģ„œ ź²€ģƒ‰
280
+ const searchCriteria = [
281
+ 'OR',
282
+ ['SUBJECT', query],
283
+ ['BODY', query]
284
+ ];
285
+ const fetchOptions = {
286
+ bodies: ['HEADER', 'TEXT', ''],
287
+ struct: true
288
+ };
289
+ const messages = await connection.search(searchCriteria, fetchOptions);
290
+ // ģµœģ‹  ė©”ģ¼ė¶€ķ„° limit개만
291
+ const limitedMessages = messages.slice(0, limit);
292
+ const parsedMails = [];
293
+ for (const item of limitedMessages) {
294
+ const mail = await this.parseImapMessage(item);
295
+ if (mail)
296
+ parsedMails.push(mail);
297
+ }
298
+ connection.end();
299
+ return parsedMails;
300
+ }
301
+ catch (error) {
302
+ const err = error;
303
+ console.error('[Dooray] Failed to search mail:', err.message);
304
+ if (connection)
305
+ connection.end();
306
+ return [];
307
+ }
308
+ }
309
+ /**
310
+ * IMAP ė©”ģ‹œģ§€ ķŒŒģ‹±
311
+ */
312
+ async parseImapMessage(item) {
313
+ try {
314
+ const all = item.parts.find((part) => part.which === '');
315
+ const uid = item.attributes.uid;
316
+ const flags = item.attributes.flags || [];
317
+ if (!all || !all.body) {
318
+ return null;
319
+ }
320
+ const parsed = await (0, mailparser_1.simpleParser)(all.body);
321
+ return {
322
+ id: uid.toString(),
323
+ subject: parsed.subject || '(제목 ģ—†ģŒ)',
324
+ from: {
325
+ name: parsed.from?.value[0]?.name || parsed.from?.text || 'ģ•Œ 수 ģ—†ģŒ',
326
+ email: parsed.from?.value[0]?.address || ''
327
+ },
328
+ to: (parsed.to?.value || []).map((addr) => ({
329
+ name: addr.name || addr.address,
330
+ email: addr.address
331
+ })),
332
+ body: parsed.text || '',
333
+ bodyHtml: parsed.html || undefined,
334
+ receivedAt: parsed.date?.toISOString() || new Date().toISOString(),
335
+ isRead: flags.includes('\\Seen'),
336
+ hasAttachments: (parsed.attachments?.length || 0) > 0
337
+ };
338
+ }
339
+ catch (error) {
340
+ const err = error;
341
+ console.error('[Dooray] Failed to parse message:', err.message);
342
+ return null;
343
+ }
344
+ }
345
+ /**
346
+ * ģ—°ź²° ķ…ŒģŠ¤ķŠø
347
+ */
348
+ async testConnection() {
349
+ try {
350
+ // SMTP ķ…ŒģŠ¤ķŠø
351
+ await this.smtpTransporter.verify();
352
+ console.log('[Dooray] SMTP connection verified');
353
+ // IMAP ķ…ŒģŠ¤ķŠø
354
+ const connection = await this.getImapConnection();
355
+ await connection.openBox('INBOX');
356
+ connection.end();
357
+ console.log('[Dooray] IMAP connection verified');
358
+ return true;
359
+ }
360
+ catch (error) {
361
+ const err = error;
362
+ console.error('[Dooray] Connection test failed:', err.message);
363
+ return false;
364
+ }
365
+ }
366
+ /**
367
+ * ė¦¬ģ†ŒģŠ¤ 정리
368
+ */
369
+ async close() {
370
+ if (this.imapConnection) {
371
+ try {
372
+ this.imapConnection.end();
373
+ }
374
+ catch (error) {
375
+ // Ignore
376
+ }
377
+ }
378
+ }
379
+ }
380
+ exports.DoorayMailClient = DoorayMailClient;
381
+ //# sourceMappingURL=mail-client.js.map
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * Security Manager
4
+ *
5
+ * ė¹„ė°€ė²ˆķ˜ø ė° 민감정볓 ģ•”ķ˜øķ™”/ė³µķ˜øķ™”
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ var __importDefault = (this && this.__importDefault) || function (mod) {
41
+ return (mod && mod.__esModule) ? mod : { "default": mod };
42
+ };
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.SecurityManager = void 0;
45
+ const crypto = __importStar(require("crypto"));
46
+ const crypto_js_1 = __importDefault(require("crypto-js"));
47
+ class SecurityManager {
48
+ constructor(encryptionKey) {
49
+ // ģ‚¬ģš©ģž 제공 키 ė˜ėŠ” 머신별 ģžė™ ģƒģ„±
50
+ this.encryptionKey = encryptionKey || this.generateMachineKey();
51
+ }
52
+ /**
53
+ * 머신별 고유 ģ•”ķ˜øķ™” 키 ģƒģ„±
54
+ */
55
+ generateMachineKey() {
56
+ const machineId = require('os').hostname() + require('os').platform();
57
+ return crypto.createHash('sha256').update(machineId).digest('hex');
58
+ }
59
+ /**
60
+ * ė¹„ė°€ė²ˆķ˜ø/토큰 ģ•”ķ˜øķ™”
61
+ */
62
+ async encryptToken(token) {
63
+ try {
64
+ const encrypted = crypto_js_1.default.AES.encrypt(token, this.encryptionKey).toString();
65
+ return encrypted;
66
+ }
67
+ catch (error) {
68
+ const err = error;
69
+ console.error('[Dooray Security] Encryption failed:', err.message);
70
+ throw new Error('Failed to encrypt token');
71
+ }
72
+ }
73
+ /**
74
+ * ė¹„ė°€ė²ˆķ˜ø/토큰 ė³µķ˜øķ™”
75
+ */
76
+ async decryptToken(encryptedToken) {
77
+ try {
78
+ // ķƒ€ģž… 첓크
79
+ if (typeof encryptedToken !== 'string') {
80
+ console.warn('[Dooray Security] Token is not a string, returning as-is');
81
+ return String(encryptedToken);
82
+ }
83
+
84
+ // CryptoJS ģ•”ķ˜øķ™” ķ˜•ģ‹ ķ™•ģø
85
+ if (!encryptedToken.includes('U2FsdGVkX1')) {
86
+ // ķ‰ė¬øģœ¼ė”œ ė³“ģž„
87
+ console.warn('[Dooray Security] Token appears unencrypted');
88
+ return encryptedToken;
89
+ }
90
+
91
+ const decrypted = crypto_js_1.default.AES.decrypt(encryptedToken, this.encryptionKey);
92
+ const token = decrypted.toString(crypto_js_1.default.enc.Utf8);
93
+
94
+ if (!token) {
95
+ throw new Error('Decryption resulted in empty token');
96
+ }
97
+
98
+ return token;
99
+ }
100
+ catch (error) {
101
+ const err = error;
102
+ console.error('[Dooray Security] Decryption failed:', err.message);
103
+ // ė³µķ˜øķ™” ģ‹¤ķŒØ ģ‹œ 원본 ė°˜ķ™˜ (ķ‰ė¬øģ¼ ź°€ėŠ„ģ„±)
104
+ return String(encryptedToken);
105
+ }
106
+ }
107
+ /**
108
+ * ģ“ė©”ģ¼ ģ£¼ģ†Œ ź²€ģ¦
109
+ */
110
+ validateEmail(email) {
111
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
112
+ return emailPattern.test(email);
113
+ }
114
+ /**
115
+ * 민감정볓 딜그 ģ •ģ œ
116
+ */
117
+ sanitizeForLog(data) {
118
+ const sensitive = ['password', 'token', 'apiToken', 'authorization', 'pass'];
119
+ const sanitized = { ...data };
120
+ for (const key of sensitive) {
121
+ if (sanitized[key]) {
122
+ sanitized[key] = '***REDACTED***';
123
+ }
124
+ }
125
+ return sanitized;
126
+ }
127
+ /**
128
+ * ė¹„ė°€ė²ˆķ˜ø ź°•ė„ ķ™•ģø
129
+ */
130
+ checkPasswordStrength(password) {
131
+ const feedback = [];
132
+ let score = 0;
133
+ if (password.length >= 8)
134
+ score++;
135
+ else
136
+ feedback.push('ė¹„ė°€ė²ˆķ˜øėŠ” ģµœģ†Œ 8ģž ģ“ģƒģ“ģ–“ģ•¼ ķ•©ė‹ˆė‹¤');
137
+ if (password.length >= 12)
138
+ score++;
139
+ if (/[a-z]/.test(password))
140
+ score++;
141
+ else
142
+ feedback.push('ģ†Œė¬øģžė„¼ ķ¬ķ•Øķ•“ģ•¼ ķ•©ė‹ˆė‹¤');
143
+ if (/[A-Z]/.test(password))
144
+ score++;
145
+ else
146
+ feedback.push('ėŒ€ė¬øģžė„¼ ķ¬ķ•Øķ•“ģ•¼ ķ•©ė‹ˆė‹¤');
147
+ if (/[0-9]/.test(password))
148
+ score++;
149
+ else
150
+ feedback.push('ģˆ«ģžė„¼ ķ¬ķ•Øķ•“ģ•¼ ķ•©ė‹ˆė‹¤');
151
+ if (/[^a-zA-Z0-9]/.test(password))
152
+ score++;
153
+ else
154
+ feedback.push('ķŠ¹ģˆ˜ė¬øģžė„¼ ķ¬ķ•Øķ•“ģ•¼ ķ•©ė‹ˆė‹¤');
155
+ return {
156
+ strong: score >= 4,
157
+ score,
158
+ feedback
159
+ };
160
+ }
161
+ }
162
+ exports.SecurityManager = SecurityManager;
163
+ //# sourceMappingURL=security.js.map
package/dooray-cli.js ADDED
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Dooray IMAP/SMTP CLI
5
+ * Command-line interface for Dooray mail operations
6
+ */
7
+
8
+ const { DoorayMailClient } = require('./dist/mail-client');
9
+ const { SecurityManager } = require('./dist/security');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ const CONFIG_PATH = path.join(os.homedir(), '.dooray-config.json');
15
+
16
+ // CLI 명령얓 ķŒŒģ„œ
17
+ const args = process.argv.slice(2);
18
+ const command = args[0];
19
+
20
+ // 설정 ė”œė“œ
21
+ function loadConfig() {
22
+ if (!fs.existsSync(CONFIG_PATH)) {
23
+ console.error('āŒ Configuration not found. Run: dooray-cli config');
24
+ process.exit(1);
25
+ }
26
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
27
+ }
28
+
29
+ // 설정 ģ €ģž„
30
+ function saveConfig(config) {
31
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
32
+ }
33
+
34
+ // ė©”ģ¼ ķ“ė¼ģ“ģ–øķŠø ģƒģ„±
35
+ async function createClient(config) {
36
+ const security = new SecurityManager();
37
+ const decryptedPassword = await security.decryptToken(config.password);
38
+
39
+ return new DoorayMailClient({
40
+ email: config.email,
41
+ password: decryptedPassword,
42
+ imap: config.imap || { host: 'imap.dooray.com', port: 993, secure: true },
43
+ smtp: config.smtp || { host: 'smtp.dooray.com', port: 465, secure: true }
44
+ });
45
+ }
46
+
47
+ // 명령얓 ķ•øė“¤ėŸ¬
48
+ async function handleCommand() {
49
+ switch (command) {
50
+ case 'config':
51
+ await configCommand();
52
+ break;
53
+ case 'list':
54
+ await listCommand();
55
+ break;
56
+ case 'recent':
57
+ await recentCommand();
58
+ break;
59
+ case 'read':
60
+ await readCommand(args[1]);
61
+ break;
62
+ case 'send':
63
+ await sendCommand(args.slice(1));
64
+ break;
65
+ case 'search':
66
+ await searchCommand(args.slice(1));
67
+ break;
68
+ case 'unread':
69
+ await unreadCountCommand();
70
+ break;
71
+ case 'test':
72
+ await testCommand();
73
+ break;
74
+ case 'help':
75
+ case '--help':
76
+ case '-h':
77
+ showHelp();
78
+ break;
79
+ default:
80
+ console.error(`āŒ Unknown command: ${command}`);
81
+ showHelp();
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ // config: 설정 ģ €ģž„
87
+ async function configCommand() {
88
+ const readline = require('readline').createInterface({
89
+ input: process.stdin,
90
+ output: process.stdout
91
+ });
92
+
93
+ const question = (q) => new Promise(resolve => readline.question(q, resolve));
94
+
95
+ try {
96
+ console.log('\nšŸ“§ Dooray Configuration Setup\n');
97
+
98
+ const email = await question('Email: ');
99
+ const password = await question('Password: ');
100
+
101
+ const security = new SecurityManager();
102
+ const encryptedPassword = await security.encryptToken(password);
103
+
104
+ const config = {
105
+ email,
106
+ password: encryptedPassword,
107
+ imap: { host: 'imap.dooray.com', port: 993, secure: true },
108
+ smtp: { host: 'smtp.dooray.com', port: 465, secure: true }
109
+ };
110
+
111
+ saveConfig(config);
112
+ console.log(`\nāœ… Configuration saved to ${CONFIG_PATH}`);
113
+ } finally {
114
+ readline.close();
115
+ }
116
+ }
117
+
118
+ // list: ģ½ģ§€ ģ•Šģ€ ė©”ģ¼ ėŖ©ė”
119
+ async function listCommand() {
120
+ const config = loadConfig();
121
+ const client = await createClient(config);
122
+
123
+ try {
124
+ const mails = await client.getUnreadMail(10);
125
+
126
+ if (mails.length === 0) {
127
+ console.log('šŸ“­ No unread mails');
128
+ return;
129
+ }
130
+
131
+ console.log(`\nšŸ“¬ Unread Mails (${mails.length})\n`);
132
+ mails.forEach((mail, idx) => {
133
+ const fromText = typeof mail.from === 'object' ? `${mail.from.name} <${mail.from.email}>` : mail.from;
134
+ console.log(`${idx + 1}. [UID: ${mail.id}] ${fromText}`);
135
+ console.log(` Subject: ${mail.subject}`);
136
+ console.log(` Date: ${mail.receivedAt}\n`);
137
+ });
138
+ } finally {
139
+ await client.close();
140
+ }
141
+ }
142
+
143
+ // recent: 최근 ė©”ģ¼ ėŖ©ė” (ģ½ģŒ/ģ•ˆģ½ģŒ 모두)
144
+ async function recentCommand() {
145
+ const config = loadConfig();
146
+ const client = await createClient(config);
147
+
148
+ try {
149
+ const mails = await client.getRecentMail(10);
150
+
151
+ if (mails.length === 0) {
152
+ console.log('šŸ“­ No mails found');
153
+ return;
154
+ }
155
+
156
+ console.log(`\nšŸ“¬ Recent Mails (${mails.length})\n`);
157
+ mails.forEach((mail, idx) => {
158
+ const readStatus = mail.isRead ? 'āœ“' : 'ā—';
159
+ const fromText = typeof mail.from === 'object' ? `${mail.from.name} <${mail.from.email}>` : mail.from;
160
+ console.log(`${idx + 1}. ${readStatus} [UID: ${mail.id}] ${fromText}`);
161
+ console.log(` Subject: ${mail.subject}`);
162
+ console.log(` Date: ${mail.receivedAt}\n`);
163
+ });
164
+ } finally {
165
+ await client.close();
166
+ }
167
+ }
168
+
169
+ // read: ķŠ¹ģ • ė©”ģ¼ ģ½źø°
170
+ async function readCommand(uid) {
171
+ if (!uid) {
172
+ console.error('āŒ Usage: dooray-cli read <uid>');
173
+ process.exit(1);
174
+ }
175
+
176
+ const config = loadConfig();
177
+ const client = await createClient(config);
178
+
179
+ try {
180
+ const mail = await client.getMailById(uid);
181
+
182
+ if (!mail) {
183
+ console.error('āŒ Mail not found');
184
+ process.exit(1);
185
+ }
186
+
187
+ const fromText = typeof mail.from === 'object' ? `${mail.from.name} <${mail.from.email}>` : mail.from;
188
+ const toText = Array.isArray(mail.to)
189
+ ? mail.to.map(t => typeof t === 'object' ? `${t.name} <${t.email}>` : t).join(', ')
190
+ : mail.to;
191
+
192
+ console.log('\nšŸ“§ Mail Details\n');
193
+ console.log(`From: ${fromText}`);
194
+ console.log(`To: ${toText}`);
195
+ console.log(`Subject: ${mail.subject}`);
196
+ console.log(`Date: ${mail.receivedAt}`);
197
+ console.log(`Read: ${mail.isRead ? 'Yes' : 'No'}`);
198
+ console.log(`Attachments: ${mail.hasAttachments ? 'Yes' : 'No'}\n`);
199
+ console.log('─'.repeat(50));
200
+ console.log(mail.body || mail.bodyHtml || '(No content)');
201
+ console.log('─'.repeat(50));
202
+ } finally {
203
+ await client.close();
204
+ }
205
+ }
206
+
207
+ // send: ė©”ģ¼ ė°œģ†”
208
+ async function sendCommand(args) {
209
+ // ė¹„ģøķ„°ėž™ķ‹°ėøŒ ėŖØė“œ: --to, --subject, --body ģ˜µģ…˜ ģ‚¬ģš©
210
+ let to, subject, body;
211
+
212
+ for (let i = 0; i < args.length; i++) {
213
+ if (args[i] === '--to' && args[i + 1]) {
214
+ to = args[++i];
215
+ } else if (args[i] === '--subject' && args[i + 1]) {
216
+ subject = args[++i];
217
+ } else if (args[i] === '--body' && args[i + 1]) {
218
+ body = args[++i];
219
+ }
220
+ }
221
+
222
+ // ė¹„ģøķ„°ėž™ķ‹°ėøŒ ėŖØė“œė”œ ģ „ė‹¬ėœ 경우
223
+ if (to && subject && body) {
224
+ const config = loadConfig();
225
+ const client = await createClient(config);
226
+
227
+ try {
228
+ await client.sendMail({ to, subject, text: body });
229
+ console.log('āœ… Mail sent successfully');
230
+ console.log(` To: ${to}`);
231
+ console.log(` Subject: ${subject}`);
232
+ return;
233
+ } catch (error) {
234
+ console.error('āŒ Error:', error.message);
235
+ process.exit(1);
236
+ } finally {
237
+ await client.close();
238
+ }
239
+ }
240
+
241
+ // ģøķ„°ėž™ķ‹°ėøŒ ėŖØė“œ
242
+ const readline = require('readline').createInterface({
243
+ input: process.stdin,
244
+ output: process.stdout
245
+ });
246
+
247
+ const question = (q) => new Promise(resolve => readline.question(q, resolve));
248
+
249
+ try {
250
+ to = to || await question('To: ');
251
+ subject = subject || await question('Subject: ');
252
+
253
+ if (!body) {
254
+ console.log('Body (type message, press Ctrl+D when done):');
255
+
256
+ body = '';
257
+ readline.on('line', (line) => {
258
+ body += line + '\n';
259
+ });
260
+
261
+ await new Promise(resolve => readline.on('close', resolve));
262
+ }
263
+
264
+ const config = loadConfig();
265
+ const client = await createClient(config);
266
+
267
+ try {
268
+ await client.sendMail({ to, subject, text: body.trim() });
269
+ console.log('\nāœ… Mail sent successfully');
270
+ } finally {
271
+ await client.close();
272
+ }
273
+ } catch (error) {
274
+ console.error('āŒ Error:', error.message);
275
+ process.exit(1);
276
+ }
277
+ }
278
+
279
+ // search: ė©”ģ¼ ź²€ģƒ‰
280
+ async function searchCommand(keywords) {
281
+ if (keywords.length === 0) {
282
+ console.error('āŒ Usage: dooray-cli search <keyword1> [keyword2] ...');
283
+ process.exit(1);
284
+ }
285
+
286
+ const config = loadConfig();
287
+ const client = await createClient(config);
288
+
289
+ try {
290
+ const criteria = keywords.map(k => ['SUBJECT', k]);
291
+ const mails = await client.searchMail(criteria);
292
+
293
+ if (mails.length === 0) {
294
+ console.log('šŸ“­ No matching mails found');
295
+ return;
296
+ }
297
+
298
+ console.log(`\nšŸ” Search Results (${mails.length})\n`);
299
+ mails.slice(0, 10).forEach((mail, idx) => {
300
+ console.log(`${idx + 1}. [UID: ${mail.uid}] ${mail.from}`);
301
+ console.log(` Subject: ${mail.subject}`);
302
+ console.log(` Date: ${mail.date}\n`);
303
+ });
304
+ } finally {
305
+ await client.close();
306
+ }
307
+ }
308
+
309
+ // unread: ģ½ģ§€ ģ•Šģ€ ė©”ģ¼ 개수
310
+ async function unreadCountCommand() {
311
+ const config = loadConfig();
312
+ const client = await createClient(config);
313
+
314
+ try {
315
+ const count = await client.getUnreadCount();
316
+ console.log(`šŸ“¬ Unread mails: ${count}`);
317
+ } finally {
318
+ await client.close();
319
+ }
320
+ }
321
+
322
+ // test: ģ—°ź²° ķ…ŒģŠ¤ķŠø
323
+ async function testCommand() {
324
+ const config = loadConfig();
325
+ const client = await createClient(config);
326
+
327
+ try {
328
+ const result = await client.testConnection();
329
+
330
+ if (result) {
331
+ console.log('\nšŸŽ‰ All connections successful!');
332
+ } else {
333
+ console.error('āŒ Connection test failed');
334
+ process.exit(1);
335
+ }
336
+ } finally {
337
+ await client.close();
338
+ }
339
+ }
340
+
341
+ // help: ė„ģ›€ė§
342
+ function showHelp() {
343
+ console.log(`
344
+ šŸ“§ Dooray CLI - IMAP/SMTP mail client for Dooray
345
+
346
+ Usage: dooray-cli <command> [options]
347
+
348
+ Commands:
349
+ config Set up email configuration
350
+ list List unread mails only
351
+ recent List recent mails (read + unread)
352
+ read <uid> Read a specific mail by UID
353
+ send Send a new mail (interactive)
354
+ send --to <email> --subject <subject> --body <body>
355
+ Send a mail (non-interactive)
356
+ search <keywords> Search mails by keywords
357
+ unread Show unread mail count
358
+ test Test IMAP/SMTP connection
359
+ help Show this help message
360
+
361
+ Examples:
362
+ dooray-cli config
363
+ dooray-cli list
364
+ dooray-cli recent
365
+ dooray-cli read 123
366
+ dooray-cli send --to "user@example.com" --subject "Hello" --body "Test message"
367
+ dooray-cli search "meeting" "report"
368
+ dooray-cli unread
369
+
370
+ Configuration file: ${CONFIG_PATH}
371
+ `);
372
+ }
373
+
374
+ // ė©”ģø 실행
375
+ handleCommand().catch(error => {
376
+ console.error('āŒ Error:', error.message);
377
+ process.exit(1);
378
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "dooray-mail-cli",
3
+ "version": "0.1.0",
4
+ "description": "Dooray mail CLI for OpenClaw Skill - IMAP/SMTP integration",
5
+ "main": "./dist/mail-client.js",
6
+ "bin": {
7
+ "dooray-cli": "dooray-cli.js"
8
+ },
9
+ "files": [
10
+ "dist/",
11
+ "dooray-cli.js",
12
+ "SKILL.md",
13
+ "README.md"
14
+ ],
15
+ "keywords": [
16
+ "openclaw",
17
+ "dooray",
18
+ "mail",
19
+ "cli",
20
+ "skill",
21
+ "imap",
22
+ "smtp"
23
+ ],
24
+ "author": "Anonymous",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "crypto-js": "^4.2.0",
28
+ "imap-simple": "^5.1.0",
29
+ "mailparser": "^3.6.0",
30
+ "nodemailer": "^6.9.0"
31
+ }
32
+ }