@jsayubi/ccgram 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/.env.example +19 -0
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/ccgram.service +24 -0
- package/config/channels.json +58 -0
- package/config/default.json +27 -0
- package/config/defaults/config.json +16 -0
- package/config/defaults/i18n.json +32 -0
- package/config/email-template.json +31 -0
- package/config/test-with-subagent.json +16 -0
- package/config/user.json +27 -0
- package/dist/claude-hook-notify.d.ts +7 -0
- package/dist/claude-hook-notify.d.ts.map +1 -0
- package/dist/claude-hook-notify.js +154 -0
- package/dist/claude-hook-notify.js.map +1 -0
- package/dist/claude-remote.d.ts +50 -0
- package/dist/claude-remote.d.ts.map +1 -0
- package/dist/claude-remote.js +927 -0
- package/dist/claude-remote.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +110 -0
- package/dist/cli.js.map +1 -0
- package/dist/enhanced-hook-notify.d.ts +16 -0
- package/dist/enhanced-hook-notify.d.ts.map +1 -0
- package/dist/enhanced-hook-notify.js +288 -0
- package/dist/enhanced-hook-notify.js.map +1 -0
- package/dist/permission-hook.d.ts +15 -0
- package/dist/permission-hook.d.ts.map +1 -0
- package/dist/permission-hook.js +357 -0
- package/dist/permission-hook.js.map +1 -0
- package/dist/prompt-bridge.d.ts +50 -0
- package/dist/prompt-bridge.d.ts.map +1 -0
- package/dist/prompt-bridge.js +173 -0
- package/dist/prompt-bridge.js.map +1 -0
- package/dist/question-notify.d.ts +16 -0
- package/dist/question-notify.d.ts.map +1 -0
- package/dist/question-notify.js +272 -0
- package/dist/question-notify.js.map +1 -0
- package/dist/setup.d.ts +10 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +649 -0
- package/dist/setup.js.map +1 -0
- package/dist/smart-monitor.d.ts +7 -0
- package/dist/smart-monitor.d.ts.map +1 -0
- package/dist/smart-monitor.js +256 -0
- package/dist/smart-monitor.js.map +1 -0
- package/dist/src/automation/claude-automation.d.ts +45 -0
- package/dist/src/automation/claude-automation.d.ts.map +1 -0
- package/dist/src/automation/claude-automation.js +367 -0
- package/dist/src/automation/claude-automation.js.map +1 -0
- package/dist/src/automation/clipboard-automation.d.ts +35 -0
- package/dist/src/automation/clipboard-automation.d.ts.map +1 -0
- package/dist/src/automation/clipboard-automation.js +242 -0
- package/dist/src/automation/clipboard-automation.js.map +1 -0
- package/dist/src/automation/simple-automation.d.ts +56 -0
- package/dist/src/automation/simple-automation.d.ts.map +1 -0
- package/dist/src/automation/simple-automation.js +283 -0
- package/dist/src/automation/simple-automation.js.map +1 -0
- package/dist/src/channels/base/channel.d.ts +60 -0
- package/dist/src/channels/base/channel.d.ts.map +1 -0
- package/dist/src/channels/base/channel.js +96 -0
- package/dist/src/channels/base/channel.js.map +1 -0
- package/dist/src/channels/email/smtp.d.ts +74 -0
- package/dist/src/channels/email/smtp.d.ts.map +1 -0
- package/dist/src/channels/email/smtp.js +605 -0
- package/dist/src/channels/email/smtp.js.map +1 -0
- package/dist/src/channels/line/line.d.ts +36 -0
- package/dist/src/channels/line/line.d.ts.map +1 -0
- package/dist/src/channels/line/line.js +180 -0
- package/dist/src/channels/line/line.js.map +1 -0
- package/dist/src/channels/line/webhook.d.ts +55 -0
- package/dist/src/channels/line/webhook.d.ts.map +1 -0
- package/dist/src/channels/line/webhook.js +191 -0
- package/dist/src/channels/line/webhook.js.map +1 -0
- package/dist/src/channels/local/desktop.d.ts +30 -0
- package/dist/src/channels/local/desktop.d.ts.map +1 -0
- package/dist/src/channels/local/desktop.js +161 -0
- package/dist/src/channels/local/desktop.js.map +1 -0
- package/dist/src/channels/telegram/telegram.d.ts +43 -0
- package/dist/src/channels/telegram/telegram.d.ts.map +1 -0
- package/dist/src/channels/telegram/telegram.js +223 -0
- package/dist/src/channels/telegram/telegram.js.map +1 -0
- package/dist/src/channels/telegram/webhook.d.ts +75 -0
- package/dist/src/channels/telegram/webhook.d.ts.map +1 -0
- package/dist/src/channels/telegram/webhook.js +278 -0
- package/dist/src/channels/telegram/webhook.js.map +1 -0
- package/dist/src/config-manager.d.ts +16 -0
- package/dist/src/config-manager.d.ts.map +1 -0
- package/dist/src/config-manager.js +152 -0
- package/dist/src/config-manager.js.map +1 -0
- package/dist/src/core/config.d.ts +28 -0
- package/dist/src/core/config.d.ts.map +1 -0
- package/dist/src/core/config.js +248 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/logger.d.ts +19 -0
- package/dist/src/core/logger.d.ts.map +1 -0
- package/dist/src/core/logger.js +47 -0
- package/dist/src/core/logger.js.map +1 -0
- package/dist/src/core/notifier.d.ts +45 -0
- package/dist/src/core/notifier.d.ts.map +1 -0
- package/dist/src/core/notifier.js +189 -0
- package/dist/src/core/notifier.js.map +1 -0
- package/dist/src/daemon/taskping-daemon.d.ts +38 -0
- package/dist/src/daemon/taskping-daemon.d.ts.map +1 -0
- package/dist/src/daemon/taskping-daemon.js +306 -0
- package/dist/src/daemon/taskping-daemon.js.map +1 -0
- package/dist/src/relay/claude-command-bridge.d.ts +57 -0
- package/dist/src/relay/claude-command-bridge.d.ts.map +1 -0
- package/dist/src/relay/claude-command-bridge.js +188 -0
- package/dist/src/relay/claude-command-bridge.js.map +1 -0
- package/dist/src/relay/command-relay.d.ts +94 -0
- package/dist/src/relay/command-relay.d.ts.map +1 -0
- package/dist/src/relay/command-relay.js +463 -0
- package/dist/src/relay/command-relay.js.map +1 -0
- package/dist/src/relay/email-listener.d.ts +65 -0
- package/dist/src/relay/email-listener.d.ts.map +1 -0
- package/dist/src/relay/email-listener.js +460 -0
- package/dist/src/relay/email-listener.js.map +1 -0
- package/dist/src/relay/relay-pty.d.ts +21 -0
- package/dist/src/relay/relay-pty.d.ts.map +1 -0
- package/dist/src/relay/relay-pty.js +696 -0
- package/dist/src/relay/relay-pty.js.map +1 -0
- package/dist/src/relay/smart-injector.d.ts +30 -0
- package/dist/src/relay/smart-injector.d.ts.map +1 -0
- package/dist/src/relay/smart-injector.js +233 -0
- package/dist/src/relay/smart-injector.js.map +1 -0
- package/dist/src/relay/tmux-injector.d.ts +46 -0
- package/dist/src/relay/tmux-injector.d.ts.map +1 -0
- package/dist/src/relay/tmux-injector.js +413 -0
- package/dist/src/relay/tmux-injector.js.map +1 -0
- package/dist/src/tools/config-manager.d.ts +33 -0
- package/dist/src/tools/config-manager.d.ts.map +1 -0
- package/dist/src/tools/config-manager.js +448 -0
- package/dist/src/tools/config-manager.js.map +1 -0
- package/dist/src/tools/installer.d.ts +38 -0
- package/dist/src/tools/installer.d.ts.map +1 -0
- package/dist/src/tools/installer.js +222 -0
- package/dist/src/tools/installer.js.map +1 -0
- package/dist/src/types/callbacks.d.ts +29 -0
- package/dist/src/types/callbacks.d.ts.map +1 -0
- package/dist/src/types/callbacks.js +7 -0
- package/dist/src/types/callbacks.js.map +1 -0
- package/dist/src/types/config.d.ts +56 -0
- package/dist/src/types/config.d.ts.map +1 -0
- package/dist/src/types/config.js +6 -0
- package/dist/src/types/config.js.map +1 -0
- package/dist/src/types/hooks.d.ts +47 -0
- package/dist/src/types/hooks.d.ts.map +1 -0
- package/dist/src/types/hooks.js +6 -0
- package/dist/src/types/hooks.js.map +1 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +23 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/ipc.d.ts +43 -0
- package/dist/src/types/ipc.d.ts.map +1 -0
- package/dist/src/types/ipc.js +7 -0
- package/dist/src/types/ipc.js.map +1 -0
- package/dist/src/types/session.d.ts +70 -0
- package/dist/src/types/session.d.ts.map +1 -0
- package/dist/src/types/session.js +9 -0
- package/dist/src/types/session.js.map +1 -0
- package/dist/src/types/telegram.d.ts +58 -0
- package/dist/src/types/telegram.d.ts.map +1 -0
- package/dist/src/types/telegram.js +6 -0
- package/dist/src/types/telegram.js.map +1 -0
- package/dist/src/utils/active-check.d.ts +19 -0
- package/dist/src/utils/active-check.d.ts.map +1 -0
- package/dist/src/utils/active-check.js +41 -0
- package/dist/src/utils/active-check.js.map +1 -0
- package/dist/src/utils/callback-parser.d.ts +21 -0
- package/dist/src/utils/callback-parser.d.ts.map +1 -0
- package/dist/src/utils/callback-parser.js +58 -0
- package/dist/src/utils/callback-parser.js.map +1 -0
- package/dist/src/utils/controller-injector.d.ts +21 -0
- package/dist/src/utils/controller-injector.d.ts.map +1 -0
- package/dist/src/utils/controller-injector.js +108 -0
- package/dist/src/utils/controller-injector.js.map +1 -0
- package/dist/src/utils/conversation-tracker.d.ts +32 -0
- package/dist/src/utils/conversation-tracker.d.ts.map +1 -0
- package/dist/src/utils/conversation-tracker.js +119 -0
- package/dist/src/utils/conversation-tracker.js.map +1 -0
- package/dist/src/utils/http-request.d.ts +25 -0
- package/dist/src/utils/http-request.d.ts.map +1 -0
- package/dist/src/utils/http-request.js +66 -0
- package/dist/src/utils/http-request.js.map +1 -0
- package/dist/src/utils/optional-require.d.ts +13 -0
- package/dist/src/utils/optional-require.d.ts.map +1 -0
- package/dist/src/utils/optional-require.js +37 -0
- package/dist/src/utils/optional-require.js.map +1 -0
- package/dist/src/utils/paths.d.ts +11 -0
- package/dist/src/utils/paths.d.ts.map +1 -0
- package/dist/src/utils/paths.js +28 -0
- package/dist/src/utils/paths.js.map +1 -0
- package/dist/src/utils/pty-session-manager.d.ts +42 -0
- package/dist/src/utils/pty-session-manager.d.ts.map +1 -0
- package/dist/src/utils/pty-session-manager.js +182 -0
- package/dist/src/utils/pty-session-manager.js.map +1 -0
- package/dist/src/utils/subagent-tracker.d.ts +64 -0
- package/dist/src/utils/subagent-tracker.d.ts.map +1 -0
- package/dist/src/utils/subagent-tracker.js +191 -0
- package/dist/src/utils/subagent-tracker.js.map +1 -0
- package/dist/src/utils/tmux-monitor.d.ts +102 -0
- package/dist/src/utils/tmux-monitor.d.ts.map +1 -0
- package/dist/src/utils/tmux-monitor.js +642 -0
- package/dist/src/utils/tmux-monitor.js.map +1 -0
- package/dist/src/utils/trace-capture.d.ts +42 -0
- package/dist/src/utils/trace-capture.d.ts.map +1 -0
- package/dist/src/utils/trace-capture.js +102 -0
- package/dist/src/utils/trace-capture.js.map +1 -0
- package/dist/start-all-webhooks.d.ts +7 -0
- package/dist/start-all-webhooks.d.ts.map +1 -0
- package/dist/start-all-webhooks.js +98 -0
- package/dist/start-all-webhooks.js.map +1 -0
- package/dist/start-line-webhook.d.ts +7 -0
- package/dist/start-line-webhook.d.ts.map +1 -0
- package/dist/start-line-webhook.js +59 -0
- package/dist/start-line-webhook.js.map +1 -0
- package/dist/start-relay-pty.d.ts +7 -0
- package/dist/start-relay-pty.d.ts.map +1 -0
- package/dist/start-relay-pty.js +173 -0
- package/dist/start-relay-pty.js.map +1 -0
- package/dist/start-telegram-webhook.d.ts +7 -0
- package/dist/start-telegram-webhook.d.ts.map +1 -0
- package/dist/start-telegram-webhook.js +80 -0
- package/dist/start-telegram-webhook.js.map +1 -0
- package/dist/user-prompt-hook.d.ts +13 -0
- package/dist/user-prompt-hook.d.ts.map +1 -0
- package/dist/user-prompt-hook.js +45 -0
- package/dist/user-prompt-hook.js.map +1 -0
- package/dist/workspace-router.d.ts +78 -0
- package/dist/workspace-router.d.ts.map +1 -0
- package/dist/workspace-router.js +408 -0
- package/dist/workspace-router.js.map +1 -0
- package/dist/workspace-telegram-bot.d.ts +3 -0
- package/dist/workspace-telegram-bot.d.ts.map +1 -0
- package/dist/workspace-telegram-bot.js +1172 -0
- package/dist/workspace-telegram-bot.js.map +1 -0
- package/package.json +80 -0
- package/src/types/callbacks.ts +39 -0
- package/src/types/config.ts +63 -0
- package/src/types/hooks.ts +50 -0
- package/src/types/index.ts +6 -0
- package/src/types/ipc.ts +55 -0
- package/src/types/session.ts +72 -0
- package/src/types/telegram.ts +66 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* relay-pty.ts - Fixed version
|
|
5
|
+
* Uses node-imap instead of ImapFlow to resolve Feishu email compatibility issues
|
|
6
|
+
*/
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.startImap = startImap;
|
|
12
|
+
exports.handleMailMessage = handleMailMessage;
|
|
13
|
+
exports.extractTokenFromSubject = extractTokenFromSubject;
|
|
14
|
+
exports.cleanEmailText = cleanEmailText;
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
18
|
+
const paths_1 = require("../utils/paths");
|
|
19
|
+
const optional_require_1 = require("../utils/optional-require");
|
|
20
|
+
const envPath = path_1.default.join(paths_1.PROJECT_ROOT, '.env');
|
|
21
|
+
dotenv_1.default.config({ path: envPath });
|
|
22
|
+
const Imap = (0, optional_require_1.optionalRequire)('node-imap', 'IMAP email relay');
|
|
23
|
+
const mailparserModule = (0, optional_require_1.optionalRequire)('mailparser', 'email parsing');
|
|
24
|
+
const simpleParser = mailparserModule ? mailparserModule.simpleParser : null;
|
|
25
|
+
const nodePty = (0, optional_require_1.optionalRequire)('node-pty', 'PTY terminal emulation');
|
|
26
|
+
const _spawn = nodePty ? nodePty.spawn : null;
|
|
27
|
+
const pinoModule = (0, optional_require_1.optionalRequire)('pino', 'structured logging');
|
|
28
|
+
// Configure logging with fallback to console
|
|
29
|
+
let log;
|
|
30
|
+
if (pinoModule) {
|
|
31
|
+
const pinoPretty = (0, optional_require_1.optionalRequire)('pino-pretty', 'log formatting');
|
|
32
|
+
if (pinoPretty) {
|
|
33
|
+
log = pinoModule({
|
|
34
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
35
|
+
transport: {
|
|
36
|
+
target: 'pino-pretty',
|
|
37
|
+
options: { colorize: true, translateTime: 'HH:MM:ss' }
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
log = pinoModule({ level: process.env.LOG_LEVEL || 'info' });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const logLevel = process.env.LOG_LEVEL || 'info';
|
|
47
|
+
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
48
|
+
const minLevel = levels[logLevel] ?? 1;
|
|
49
|
+
log = {
|
|
50
|
+
debug: (...args) => { if (minLevel <= 0)
|
|
51
|
+
console.log('[DEBUG]', ...args); },
|
|
52
|
+
info: (...args) => { if (minLevel <= 1)
|
|
53
|
+
console.log('[INFO]', ...args); },
|
|
54
|
+
warn: (...args) => { if (minLevel <= 2)
|
|
55
|
+
console.warn('[WARN]', ...args); },
|
|
56
|
+
error: (...args) => { if (minLevel <= 3)
|
|
57
|
+
console.error('[ERROR]', ...args); },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Global configuration
|
|
61
|
+
const SESS_PATH = process.env.SESSION_MAP_PATH || path_1.default.join(paths_1.PROJECT_ROOT, 'src/data/session-map.json');
|
|
62
|
+
const PROCESSED_PATH = path_1.default.join(paths_1.PROJECT_ROOT, 'src/data/processed-messages.json');
|
|
63
|
+
const SENT_MESSAGES_PATH = path_1.default.join(paths_1.PROJECT_ROOT, 'src/data/sent-messages.json');
|
|
64
|
+
const ALLOWED_SENDERS = (process.env.ALLOWED_SENDERS || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
65
|
+
const PTY_POOL = new Map();
|
|
66
|
+
let PROCESSED_MESSAGES = new Set();
|
|
67
|
+
// Load processed messages
|
|
68
|
+
function loadProcessedMessages() {
|
|
69
|
+
if ((0, fs_1.existsSync)(PROCESSED_PATH)) {
|
|
70
|
+
try {
|
|
71
|
+
const data = JSON.parse((0, fs_1.readFileSync)(PROCESSED_PATH, 'utf8'));
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
// Keep only records from the last 7 days
|
|
74
|
+
const validMessages = data.filter(item => (now - item.timestamp) < 7 * 24 * 60 * 60 * 1000);
|
|
75
|
+
PROCESSED_MESSAGES = new Set(validMessages.map(item => item.id));
|
|
76
|
+
// Update file, remove expired records
|
|
77
|
+
saveProcessedMessages();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
log.error({ error }, 'Failed to load processed messages');
|
|
81
|
+
PROCESSED_MESSAGES = new Set();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Save processed messages
|
|
86
|
+
function saveProcessedMessages() {
|
|
87
|
+
try {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const data = Array.from(PROCESSED_MESSAGES).map(id => ({
|
|
90
|
+
id,
|
|
91
|
+
timestamp: now
|
|
92
|
+
}));
|
|
93
|
+
// Ensure directory exists
|
|
94
|
+
const dir = path_1.default.dirname(PROCESSED_PATH);
|
|
95
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
96
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
(0, fs_1.writeFileSync)(PROCESSED_PATH, JSON.stringify(data, null, 2));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
log.error({ error }, 'Failed to save processed messages');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Load session mapping
|
|
105
|
+
function loadSessions() {
|
|
106
|
+
if (!(0, fs_1.existsSync)(SESS_PATH))
|
|
107
|
+
return {};
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse((0, fs_1.readFileSync)(SESS_PATH, 'utf8'));
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
log.error({ error }, 'Failed to load session map');
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Check if sender is in whitelist
|
|
117
|
+
function isAllowed(fromAddress) {
|
|
118
|
+
if (!fromAddress)
|
|
119
|
+
return false;
|
|
120
|
+
const addr = fromAddress.toLowerCase();
|
|
121
|
+
return ALLOWED_SENDERS.some(allowed => addr.includes(allowed));
|
|
122
|
+
}
|
|
123
|
+
// Extract CCGram token from subject
|
|
124
|
+
function extractTokenFromSubject(subject = '') {
|
|
125
|
+
const patterns = [
|
|
126
|
+
/\[CCGram\s+#([A-Z0-9]+)\]/,
|
|
127
|
+
/Re:\s*\[CCGram\s+#([A-Z0-9]+)\]/
|
|
128
|
+
];
|
|
129
|
+
for (const pattern of patterns) {
|
|
130
|
+
const match = subject.match(pattern);
|
|
131
|
+
if (match)
|
|
132
|
+
return match[1];
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
// Clean email text
|
|
137
|
+
function cleanEmailText(text = '') {
|
|
138
|
+
const lines = text.split(/\r?\n/);
|
|
139
|
+
const cleanLines = [];
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
// Detect quoted content (more comprehensive detection)
|
|
142
|
+
if (line.includes('-----Original Message-----') ||
|
|
143
|
+
line.includes('--- Original Message ---') ||
|
|
144
|
+
line.includes('at') && line.includes('wrote:') ||
|
|
145
|
+
line.includes('On') && line.includes('wrote:') ||
|
|
146
|
+
line.includes('Session ID:') ||
|
|
147
|
+
line.includes(`<${process.env.SMTP_USER}>`) ||
|
|
148
|
+
line.includes('CCGram Notification System') ||
|
|
149
|
+
line.includes('on 2025') && line.includes('wrote:') ||
|
|
150
|
+
line.match(/^>.*/) || // Quote lines start with >
|
|
151
|
+
line.includes('From:') && line.includes('@') ||
|
|
152
|
+
line.includes('To:') && line.includes('@') ||
|
|
153
|
+
line.includes('Subject:') ||
|
|
154
|
+
line.includes('Sent:') ||
|
|
155
|
+
line.includes('Date:')) {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
// Detect email signature
|
|
159
|
+
if (line.match(/^--\s*$/) ||
|
|
160
|
+
line.includes('Sent from') ||
|
|
161
|
+
line.includes('Sent from my') ||
|
|
162
|
+
line.includes('Best regards') ||
|
|
163
|
+
line.includes('Sincerely')) {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
cleanLines.push(line);
|
|
167
|
+
}
|
|
168
|
+
// Get valid content
|
|
169
|
+
const cleanText = cleanLines.join('\n').trim();
|
|
170
|
+
// Find actual command content (skip greetings, etc.)
|
|
171
|
+
const contentLines = cleanText.split(/\r?\n/).filter(l => l.trim().length > 0);
|
|
172
|
+
// Collect all valid command lines (support multi-line commands)
|
|
173
|
+
const validCommandLines = [];
|
|
174
|
+
for (const line of contentLines) {
|
|
175
|
+
const trimmedLine = line.trim();
|
|
176
|
+
// Skip common greetings (but only if they're standalone)
|
|
177
|
+
if (trimmedLine.match(/^(hi|hello|thank you|thanks|ok|yes)$/i)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Skip remaining email quotes
|
|
181
|
+
if (trimmedLine.includes('CCGram Notification System') ||
|
|
182
|
+
trimmedLine.includes(`<${process.env.SMTP_USER}>`) ||
|
|
183
|
+
trimmedLine.includes('on 2025')) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// Collect valid command lines
|
|
187
|
+
if (trimmedLine.length > 0) {
|
|
188
|
+
validCommandLines.push(trimmedLine);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Join all valid lines to form the complete command
|
|
192
|
+
if (validCommandLines.length > 0) {
|
|
193
|
+
const fullCommand = validCommandLines.join('\n').slice(0, 8192);
|
|
194
|
+
const deduplicatedCommand = deduplicateCommand(fullCommand);
|
|
195
|
+
return deduplicatedCommand;
|
|
196
|
+
}
|
|
197
|
+
// If no obvious command is found, return first non-empty line (and deduplicate)
|
|
198
|
+
const firstLine = contentLines[0] || '';
|
|
199
|
+
const command = firstLine.slice(0, 8192).trim();
|
|
200
|
+
return deduplicateCommand(command);
|
|
201
|
+
}
|
|
202
|
+
// Deduplicate command text (handle cases like: "drink cola okay drink cola okay" -> "drink cola okay")
|
|
203
|
+
function deduplicateCommand(command) {
|
|
204
|
+
if (!command || command.length === 0) {
|
|
205
|
+
return command;
|
|
206
|
+
}
|
|
207
|
+
// Check if command is self-repeating
|
|
208
|
+
const length = command.length;
|
|
209
|
+
for (let i = 1; i <= Math.floor(length / 2); i++) {
|
|
210
|
+
const firstPart = command.substring(0, i);
|
|
211
|
+
const remaining = command.substring(i);
|
|
212
|
+
// Check if remaining part completely repeats the first part
|
|
213
|
+
if (remaining === firstPart.repeat(Math.floor(remaining.length / firstPart.length))) {
|
|
214
|
+
// Found repetition pattern, return first part
|
|
215
|
+
log.debug({
|
|
216
|
+
originalCommand: command,
|
|
217
|
+
deduplicatedCommand: firstPart,
|
|
218
|
+
pattern: firstPart
|
|
219
|
+
}, 'Detected and removed command duplication');
|
|
220
|
+
return firstPart;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// No repetition detected, return original command
|
|
224
|
+
return command;
|
|
225
|
+
}
|
|
226
|
+
// Unattended remote command injection - tmux priority, smart fallback
|
|
227
|
+
async function injectCommandRemote(token, command) {
|
|
228
|
+
const sessions = loadSessions();
|
|
229
|
+
const session = sessions[token];
|
|
230
|
+
if (!session) {
|
|
231
|
+
log.warn({ token }, 'Session not found');
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
// Check if session has expired
|
|
235
|
+
const now = Math.floor(Date.now() / 1000);
|
|
236
|
+
if (session.expiresAt && session.expiresAt < now) {
|
|
237
|
+
log.warn({ token }, 'Session expired');
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
log.info({ token, command }, 'Starting remote command injection');
|
|
242
|
+
// Method 1: Prefer tmux unattended injection
|
|
243
|
+
const TmuxInjector = require('./tmux-injector');
|
|
244
|
+
const tmuxSessionName = session.tmuxSession || 'claude-taskping';
|
|
245
|
+
const tmuxInjector = new TmuxInjector(log, tmuxSessionName);
|
|
246
|
+
const tmuxResult = await tmuxInjector.injectCommandFull(token, command);
|
|
247
|
+
if (tmuxResult.success) {
|
|
248
|
+
log.info({ token, session: tmuxResult.session }, 'Tmux remote injection successful');
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
log.warn({ token, error: tmuxResult.error }, 'Tmux injection failed, trying smart fallback');
|
|
253
|
+
// Method 2: Fall back to smart injector
|
|
254
|
+
const SmartInjector = require('./smart-injector');
|
|
255
|
+
const smartInjector = new SmartInjector(log);
|
|
256
|
+
const smartResult = await smartInjector.injectCommand(token, command);
|
|
257
|
+
if (smartResult) {
|
|
258
|
+
log.info({ token }, 'Smart injection fallback successful');
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
log.error({ token }, 'All remote injection methods failed');
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
log.error({ error, token }, 'Failed to inject command remotely');
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Try automatic paste to active window
|
|
273
|
+
async function tryAutoPaste(command) {
|
|
274
|
+
return new Promise((resolve) => {
|
|
275
|
+
// First copy command to clipboard
|
|
276
|
+
const { spawn } = require('child_process');
|
|
277
|
+
const pbcopy = spawn('pbcopy');
|
|
278
|
+
pbcopy.stdin.write(command);
|
|
279
|
+
pbcopy.stdin.end();
|
|
280
|
+
pbcopy.on('close', (code) => {
|
|
281
|
+
if (code !== 0) {
|
|
282
|
+
resolve({ success: false, error: 'clipboard_copy_failed' });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Execute AppleScript auto-paste
|
|
286
|
+
const autoScript = `
|
|
287
|
+
tell application "System Events"
|
|
288
|
+
set claudeApps to {"Claude", "Claude Code", "Terminal", "iTerm2", "iTerm"}
|
|
289
|
+
set targetApp to null
|
|
290
|
+
set targetName to ""
|
|
291
|
+
|
|
292
|
+
repeat with appName in claudeApps
|
|
293
|
+
try
|
|
294
|
+
if application process appName exists then
|
|
295
|
+
set targetApp to application process appName
|
|
296
|
+
set targetName to appName
|
|
297
|
+
exit repeat
|
|
298
|
+
end if
|
|
299
|
+
end try
|
|
300
|
+
end repeat
|
|
301
|
+
|
|
302
|
+
if targetApp is not null then
|
|
303
|
+
set frontmost of targetApp to true
|
|
304
|
+
delay 0.8
|
|
305
|
+
|
|
306
|
+
repeat 10 times
|
|
307
|
+
if frontmost of targetApp then exit repeat
|
|
308
|
+
delay 0.1
|
|
309
|
+
end repeat
|
|
310
|
+
|
|
311
|
+
if targetName is in {"Terminal", "iTerm2", "iTerm"} then
|
|
312
|
+
keystroke "${command.replace(/"/g, '\\"')}"
|
|
313
|
+
delay 0.3
|
|
314
|
+
keystroke return
|
|
315
|
+
return "terminal_typed"
|
|
316
|
+
else
|
|
317
|
+
keystroke "a" using command down
|
|
318
|
+
delay 0.2
|
|
319
|
+
keystroke "v" using command down
|
|
320
|
+
delay 0.5
|
|
321
|
+
keystroke return
|
|
322
|
+
return "claude_pasted"
|
|
323
|
+
end if
|
|
324
|
+
else
|
|
325
|
+
return "no_target_found"
|
|
326
|
+
end if
|
|
327
|
+
end tell
|
|
328
|
+
`;
|
|
329
|
+
const { exec } = require('child_process');
|
|
330
|
+
exec(`osascript -e '${autoScript}'`, (error, stdout, _stderr) => {
|
|
331
|
+
if (error) {
|
|
332
|
+
resolve({ success: false, error: error.message });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const result = stdout.trim();
|
|
336
|
+
switch (result) {
|
|
337
|
+
case 'terminal_typed':
|
|
338
|
+
resolve({ success: true, method: 'Terminal direct input' });
|
|
339
|
+
break;
|
|
340
|
+
case 'claude_pasted':
|
|
341
|
+
resolve({ success: true, method: 'Claude app paste' });
|
|
342
|
+
break;
|
|
343
|
+
case 'no_target_found':
|
|
344
|
+
resolve({ success: false, error: 'no_target_application' });
|
|
345
|
+
break;
|
|
346
|
+
default:
|
|
347
|
+
resolve({ success: false, error: `unknown_result: ${result}` });
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
// Fallback to clipboard + strong reminder
|
|
354
|
+
async function fallbackToClipboard(command) {
|
|
355
|
+
return new Promise((resolve) => {
|
|
356
|
+
// Copy to clipboard
|
|
357
|
+
const { spawn } = require('child_process');
|
|
358
|
+
const pbcopy = spawn('pbcopy');
|
|
359
|
+
pbcopy.stdin.write(command);
|
|
360
|
+
pbcopy.stdin.end();
|
|
361
|
+
pbcopy.on('close', (code) => {
|
|
362
|
+
if (code !== 0) {
|
|
363
|
+
resolve(false);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// Send strong reminder notification
|
|
367
|
+
const shortCommand = command.length > 30 ? command.substring(0, 30) + '...' : command;
|
|
368
|
+
const notificationScript = `
|
|
369
|
+
display notification "\u{1f6a8} Email command auto-copied! Please paste and execute in Claude Code immediately (Cmd+V)" with title "TaskPing Auto-Injection" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "Basso"
|
|
370
|
+
`;
|
|
371
|
+
const { exec } = require('child_process');
|
|
372
|
+
exec(`osascript -e '${notificationScript}'`, (error) => {
|
|
373
|
+
if (error) {
|
|
374
|
+
log.warn({ error: error.message }, 'Failed to send notification');
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
log.info('Strong reminder notification sent');
|
|
378
|
+
}
|
|
379
|
+
resolve(true);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
// Handle email message
|
|
385
|
+
async function handleMailMessage(parsed) {
|
|
386
|
+
try {
|
|
387
|
+
log.debug({ uid: parsed.uid, messageId: parsed.messageId }, 'handleMailMessage called');
|
|
388
|
+
// Check if this is a system-sent email
|
|
389
|
+
const messageId = parsed.messageId;
|
|
390
|
+
if (await isSystemSentEmail(messageId)) {
|
|
391
|
+
log.info({ messageId }, 'Skipping system-sent email');
|
|
392
|
+
await removeFromSentMessages(messageId);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// Simplified duplicate detection (UID already checked earlier)
|
|
396
|
+
const uid = parsed.uid;
|
|
397
|
+
// Only perform additional checks for emails without UID
|
|
398
|
+
if (!uid) {
|
|
399
|
+
const identifier = messageId;
|
|
400
|
+
if (identifier && PROCESSED_MESSAGES.has(identifier)) {
|
|
401
|
+
log.debug({ messageId, identifier }, 'Message already processed by messageId, skipping');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
// Content hash deduplication (as last resort)
|
|
405
|
+
const emailSubject = parsed.subject || '';
|
|
406
|
+
const emailDate = parsed.date || new Date();
|
|
407
|
+
const contentHash = `${emailSubject}_${emailDate.getTime()}`;
|
|
408
|
+
if (PROCESSED_MESSAGES.has(contentHash)) {
|
|
409
|
+
log.debug({ subject: emailSubject, date: emailDate, contentHash }, 'Message already processed by content hash, skipping');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Verify sender
|
|
414
|
+
if (!isAllowed(parsed.from?.text || '')) {
|
|
415
|
+
log.warn({ from: parsed.from?.text }, 'Sender not allowed');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// Extract token
|
|
419
|
+
const subject = parsed.subject || '';
|
|
420
|
+
const token = extractTokenFromSubject(subject);
|
|
421
|
+
if (!token) {
|
|
422
|
+
log.warn({ subject }, 'No token found in email');
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Extract command - add detailed debugging
|
|
426
|
+
log.debug({
|
|
427
|
+
token,
|
|
428
|
+
rawEmailText: parsed.text?.substring(0, 500),
|
|
429
|
+
emailSubject: parsed.subject
|
|
430
|
+
}, 'Raw email content before cleaning');
|
|
431
|
+
const command = cleanEmailText(parsed.text);
|
|
432
|
+
log.debug({
|
|
433
|
+
token,
|
|
434
|
+
cleanedCommand: command,
|
|
435
|
+
commandLength: command?.length
|
|
436
|
+
}, 'Email content after cleaning');
|
|
437
|
+
if (!command) {
|
|
438
|
+
log.warn({ token }, 'No command found in email');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
log.info({ token, command }, 'Processing email command');
|
|
442
|
+
// Unattended remote command injection (tmux priority, smart fallback)
|
|
443
|
+
const success = await injectCommandRemote(token, command);
|
|
444
|
+
if (!success) {
|
|
445
|
+
log.warn({ token }, 'Could not inject command');
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// Mark as processed (only mark after successful processing)
|
|
449
|
+
if (uid) {
|
|
450
|
+
// Mark UID as processed
|
|
451
|
+
PROCESSED_MESSAGES.add(uid);
|
|
452
|
+
log.debug({ uid }, 'Marked message UID as processed');
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
// For emails without UID, use messageId and content hash
|
|
456
|
+
if (messageId) {
|
|
457
|
+
PROCESSED_MESSAGES.add(messageId);
|
|
458
|
+
log.debug({ messageId }, 'Marked message as processed by messageId');
|
|
459
|
+
}
|
|
460
|
+
// Content hash marking
|
|
461
|
+
const emailSubject = parsed.subject || '';
|
|
462
|
+
const emailDate = parsed.date || new Date();
|
|
463
|
+
const contentHash = `${emailSubject}_${emailDate.getTime()}`;
|
|
464
|
+
PROCESSED_MESSAGES.add(contentHash);
|
|
465
|
+
log.debug({ contentHash }, 'Marked message as processed by content hash');
|
|
466
|
+
}
|
|
467
|
+
// Persist processed messages
|
|
468
|
+
saveProcessedMessages();
|
|
469
|
+
log.info({ token }, 'Command injected successfully via remote method');
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
log.error({ error }, 'Failed to handle email message');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Start IMAP listening
|
|
476
|
+
function startImap() {
|
|
477
|
+
if (!Imap) {
|
|
478
|
+
console.error('Error: node-imap is required for the email relay. Install with: npm install node-imap');
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
if (!simpleParser) {
|
|
482
|
+
console.error('Error: mailparser is required for the email relay. Install with: npm install mailparser');
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
// First load processed messages
|
|
486
|
+
loadProcessedMessages();
|
|
487
|
+
log.info('Starting relay-pty service', {
|
|
488
|
+
mode: 'pty',
|
|
489
|
+
imapHost: process.env.IMAP_HOST,
|
|
490
|
+
imapUser: process.env.IMAP_USER,
|
|
491
|
+
allowedSenders: ALLOWED_SENDERS,
|
|
492
|
+
sessionMapPath: SESS_PATH,
|
|
493
|
+
processedCount: PROCESSED_MESSAGES.size
|
|
494
|
+
});
|
|
495
|
+
const imap = new Imap({
|
|
496
|
+
user: process.env.IMAP_USER,
|
|
497
|
+
password: process.env.IMAP_PASS,
|
|
498
|
+
host: process.env.IMAP_HOST,
|
|
499
|
+
port: parseInt(process.env.IMAP_PORT || '993') || 993,
|
|
500
|
+
tls: process.env.IMAP_SECURE === 'true',
|
|
501
|
+
connTimeout: 60000,
|
|
502
|
+
authTimeout: 30000,
|
|
503
|
+
keepalive: true
|
|
504
|
+
});
|
|
505
|
+
imap.once('ready', function () {
|
|
506
|
+
log.info('Connected to IMAP server');
|
|
507
|
+
imap.openBox('INBOX', false, function (err, box) {
|
|
508
|
+
if (err) {
|
|
509
|
+
log.error({ error: err.message }, 'Failed to open INBOX');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
log.info(`Mailbox opened: ${box.messages.total} total messages, ${box.messages.new} new`);
|
|
513
|
+
// Only process existing unread emails at startup
|
|
514
|
+
processExistingEmails(imap);
|
|
515
|
+
// Listen for new emails (main mechanism)
|
|
516
|
+
imap.on('mail', function (numNewMsgs) {
|
|
517
|
+
log.info({ newMessages: numNewMsgs }, 'New mail arrived');
|
|
518
|
+
// Add delay to avoid conflicts with existing email processing
|
|
519
|
+
setTimeout(() => {
|
|
520
|
+
processNewEmails(imap);
|
|
521
|
+
}, 1000);
|
|
522
|
+
});
|
|
523
|
+
// Periodic check for new emails (backup only, extended interval)
|
|
524
|
+
setInterval(() => {
|
|
525
|
+
log.debug('Periodic email check...');
|
|
526
|
+
processNewEmails(imap);
|
|
527
|
+
}, 120000); // Check every 2 minutes, reduced frequency
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
imap.once('error', function (err) {
|
|
531
|
+
log.error({ error: err.message }, 'IMAP error');
|
|
532
|
+
// Reconnection mechanism
|
|
533
|
+
setTimeout(() => {
|
|
534
|
+
log.info('Attempting to reconnect...');
|
|
535
|
+
startImap();
|
|
536
|
+
}, 10000);
|
|
537
|
+
});
|
|
538
|
+
imap.once('end', function () {
|
|
539
|
+
log.info('IMAP connection ended');
|
|
540
|
+
});
|
|
541
|
+
imap.connect();
|
|
542
|
+
// Graceful shutdown
|
|
543
|
+
process.on('SIGINT', () => {
|
|
544
|
+
log.info('Shutting down gracefully...');
|
|
545
|
+
imap.end();
|
|
546
|
+
process.exit(0);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
// Process existing emails
|
|
550
|
+
function processExistingEmails(imap) {
|
|
551
|
+
// Search unread emails
|
|
552
|
+
imap.search(['UNSEEN'], function (err, results) {
|
|
553
|
+
if (err) {
|
|
554
|
+
log.error({ error: err.message }, 'Failed to search emails');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (results.length > 0) {
|
|
558
|
+
log.info(`Found ${results.length} unread messages`);
|
|
559
|
+
log.debug({ uids: results }, 'Unread message UIDs');
|
|
560
|
+
fetchAndProcessEmails(imap, results);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
log.debug('No unread messages found');
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
// Process new emails
|
|
568
|
+
function processNewEmails(imap) {
|
|
569
|
+
// Search emails from the last 5 minutes
|
|
570
|
+
const since = new Date();
|
|
571
|
+
since.setMinutes(since.getMinutes() - 5);
|
|
572
|
+
const sinceStr = since.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
573
|
+
imap.search([['SINCE', sinceStr], 'UNSEEN'], function (err, results) {
|
|
574
|
+
if (err) {
|
|
575
|
+
log.error({ error: err.message }, 'Failed to search new emails');
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (results.length > 0) {
|
|
579
|
+
log.info(`Found ${results.length} new messages`);
|
|
580
|
+
fetchAndProcessEmails(imap, results);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
// Fetch and process emails
|
|
585
|
+
function fetchAndProcessEmails(imap, uids) {
|
|
586
|
+
log.debug({ uids }, 'Starting to fetch emails');
|
|
587
|
+
const fetch = imap.fetch(uids, {
|
|
588
|
+
bodies: '', // Get complete email
|
|
589
|
+
markSeen: true // Mark as read
|
|
590
|
+
});
|
|
591
|
+
fetch.on('message', function (msg, seqno) {
|
|
592
|
+
let buffer = '';
|
|
593
|
+
let messageUid = null;
|
|
594
|
+
let skipProcessing = false;
|
|
595
|
+
let bodyProcessed = false;
|
|
596
|
+
let attributesReceived = false;
|
|
597
|
+
// Get UID to prevent duplicate processing
|
|
598
|
+
msg.once('attributes', function (attrs) {
|
|
599
|
+
messageUid = attrs.uid;
|
|
600
|
+
attributesReceived = true;
|
|
601
|
+
log.debug({ uid: messageUid, seqno }, 'Received attributes');
|
|
602
|
+
// Only check if already processed, don't mark immediately
|
|
603
|
+
if (messageUid && PROCESSED_MESSAGES.has(messageUid)) {
|
|
604
|
+
log.debug({ uid: messageUid, seqno }, 'Message UID already processed, skipping entire message');
|
|
605
|
+
skipProcessing = true;
|
|
606
|
+
return; // Return directly, do not continue processing
|
|
607
|
+
}
|
|
608
|
+
log.debug({ uid: messageUid, seqno }, 'Message UID ready for processing');
|
|
609
|
+
// If body is processed, can now parse email
|
|
610
|
+
if (bodyProcessed && !skipProcessing) {
|
|
611
|
+
processEmailBuffer(buffer, messageUid, seqno);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
msg.on('body', function (stream, _info) {
|
|
615
|
+
stream.on('data', function (chunk) {
|
|
616
|
+
buffer += chunk.toString('utf8');
|
|
617
|
+
});
|
|
618
|
+
stream.once('end', function () {
|
|
619
|
+
bodyProcessed = true;
|
|
620
|
+
log.debug({ uid: messageUid, seqno, bufferLength: buffer.length, attributesReceived }, 'Body stream ended');
|
|
621
|
+
// If attributes received and not marked to skip, can now parse email
|
|
622
|
+
if (attributesReceived && !skipProcessing) {
|
|
623
|
+
processEmailBuffer(buffer, messageUid, seqno);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
// Separated email processing function
|
|
628
|
+
function processEmailBuffer(buffer, uid, seqno) {
|
|
629
|
+
if (buffer.length > 0 && uid) {
|
|
630
|
+
log.debug({ uid, seqno }, 'Starting email parsing');
|
|
631
|
+
simpleParser(buffer, function (err, parsed) {
|
|
632
|
+
if (err) {
|
|
633
|
+
log.error({ error: err.message, seqno, uid }, 'Failed to parse email');
|
|
634
|
+
PROCESSED_MESSAGES.delete(uid);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
log.debug({ uid, seqno }, 'Email parsed successfully, calling handleMailMessage');
|
|
638
|
+
parsed.uid = uid;
|
|
639
|
+
handleMailMessage(parsed);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
log.debug({ uid, seqno, bufferLength: buffer.length }, 'Skipping email - no buffer or uid');
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
msg.once('error', function (err) {
|
|
648
|
+
log.error({ error: err.message, seqno, uid: messageUid }, 'Error fetching message');
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
fetch.once('error', function (err) {
|
|
652
|
+
log.error({ error: err.message }, 'Error fetching emails');
|
|
653
|
+
});
|
|
654
|
+
fetch.once('end', function () {
|
|
655
|
+
log.debug('Email fetch completed');
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
// Check if email is system-sent
|
|
659
|
+
async function isSystemSentEmail(messageId) {
|
|
660
|
+
if (!messageId || !(0, fs_1.existsSync)(SENT_MESSAGES_PATH)) {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
const sentMessages = JSON.parse((0, fs_1.readFileSync)(SENT_MESSAGES_PATH, 'utf8'));
|
|
665
|
+
return sentMessages.messages.some(msg => msg.messageId === messageId);
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
log.error({ error }, 'Error reading sent messages');
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// Remove email from sent messages tracking
|
|
673
|
+
async function removeFromSentMessages(messageId) {
|
|
674
|
+
if (!(0, fs_1.existsSync)(SENT_MESSAGES_PATH)) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
const sentMessages = JSON.parse((0, fs_1.readFileSync)(SENT_MESSAGES_PATH, 'utf8'));
|
|
679
|
+
sentMessages.messages = sentMessages.messages.filter(msg => msg.messageId !== messageId);
|
|
680
|
+
// Also clean up old messages (older than 24 hours)
|
|
681
|
+
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
682
|
+
sentMessages.messages = sentMessages.messages.filter(msg => {
|
|
683
|
+
return new Date(msg.sentAt) > oneDayAgo;
|
|
684
|
+
});
|
|
685
|
+
(0, fs_1.writeFileSync)(SENT_MESSAGES_PATH, JSON.stringify(sentMessages, null, 2));
|
|
686
|
+
log.debug({ messageId }, 'Removed message from sent tracking');
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
log.error({ error }, 'Error removing from sent messages');
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
// Start service
|
|
693
|
+
if (require.main === module) {
|
|
694
|
+
startImap();
|
|
695
|
+
}
|
|
696
|
+
//# sourceMappingURL=relay-pty.js.map
|