640 lines
30 KiB
Plaintext
640 lines
30 KiB
Plaintext
/**
|
|
* Email Notification Channel
|
|
* Sends notifications via email with reply support
|
|
*/
|
|
|
|
const NotificationChannel = require('../base/channel');
|
|
const nodemailer = require('nodemailer');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const TmuxMonitor = require('../../utils/tmux-monitor');
|
|
const { execSync } = require('child_process');
|
|
|
|
class EmailChannel extends NotificationChannel {
|
|
constructor(config = {}) {
|
|
super('email', config);
|
|
this.transporter = null;
|
|
this.sessionsDir = path.join(__dirname, '../../data/sessions');
|
|
this.templatesDir = path.join(__dirname, '../../assets/email-templates');
|
|
this.sentMessagesPath = config.sentMessagesPath || path.join(__dirname, '../../data/sent-messages.json');
|
|
this.tmuxMonitor = new TmuxMonitor();
|
|
|
|
this._ensureDirectories();
|
|
this._initializeTransporter();
|
|
}
|
|
|
|
_escapeHtml(text) {
|
|
if (!text) return '';
|
|
const htmlEntities = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
return text.replace(/[&<>"']/g, char => htmlEntities[char]);
|
|
}
|
|
|
|
_ensureDirectories() {
|
|
if (!fs.existsSync(this.sessionsDir)) {
|
|
fs.mkdirSync(this.sessionsDir, { recursive: true });
|
|
}
|
|
if (!fs.existsSync(this.templatesDir)) {
|
|
fs.mkdirSync(this.templatesDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
_generateToken() {
|
|
// Generate short Token (uppercase letters + numbers, 8 digits)
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
let token = '';
|
|
for (let i = 0; i < 8; i++) {
|
|
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
return token;
|
|
}
|
|
|
|
_initializeTransporter() {
|
|
if (!this.config.smtp) {
|
|
this.logger.warn('SMTP configuration not found');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.transporter = nodemailer.createTransport({
|
|
host: this.config.smtp.host,
|
|
port: this.config.smtp.port,
|
|
secure: this.config.smtp.secure || false,
|
|
auth: {
|
|
user: this.config.smtp.auth.user,
|
|
pass: this.config.smtp.auth.pass
|
|
},
|
|
// Add timeout settings
|
|
connectionTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000,
|
|
greetingTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000,
|
|
socketTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000
|
|
});
|
|
|
|
this.logger.debug('Email transporter initialized');
|
|
} catch (error) {
|
|
this.logger.error('Failed to initialize email transporter:', error.message);
|
|
}
|
|
}
|
|
|
|
_getCurrentTmuxSession() {
|
|
try {
|
|
// Try to get current tmux session
|
|
const tmuxSession = execSync('tmux display-message -p "#S"', {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
}).trim();
|
|
|
|
return tmuxSession || null;
|
|
} catch (error) {
|
|
// Not in a tmux session or tmux not available
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async _sendImpl(notification) {
|
|
if (!this.transporter) {
|
|
throw new Error('Email transporter not initialized');
|
|
}
|
|
|
|
if (!this.config.to) {
|
|
throw new Error('Email recipient not configured');
|
|
}
|
|
|
|
// Generate session ID and Token
|
|
const sessionId = uuidv4();
|
|
const token = this._generateToken();
|
|
|
|
// Get current tmux session and conversation content
|
|
const tmuxSession = this._getCurrentTmuxSession();
|
|
if (tmuxSession && !notification.metadata) {
|
|
const conversation = this.tmuxMonitor.getRecentConversation(tmuxSession);
|
|
const fullTrace = this.tmuxMonitor.getFullExecutionTrace(tmuxSession);
|
|
notification.metadata = {
|
|
userQuestion: conversation.userQuestion || notification.message,
|
|
claudeResponse: conversation.claudeResponse || notification.message,
|
|
tmuxSession: tmuxSession,
|
|
fullExecutionTrace: fullTrace
|
|
};
|
|
}
|
|
|
|
// Create session record
|
|
await this._createSession(sessionId, notification, token);
|
|
|
|
// Generate email content
|
|
const emailContent = this._generateEmailContent(notification, sessionId, token);
|
|
|
|
// Generate unique Message-ID
|
|
const messageId = `<${sessionId}-${Date.now()}@claude-code-remote>`;
|
|
|
|
const mailOptions = {
|
|
from: this.config.from || this.config.smtp.auth.user,
|
|
to: this.config.to,
|
|
subject: emailContent.subject,
|
|
html: emailContent.html,
|
|
text: emailContent.text,
|
|
messageId: messageId,
|
|
// Add custom headers for reply recognition
|
|
headers: {
|
|
'X-Claude-Code-Remote-Session-ID': sessionId,
|
|
'X-Claude-Code-Remote-Type': notification.type
|
|
}
|
|
};
|
|
|
|
try {
|
|
const result = await this.transporter.sendMail(mailOptions);
|
|
this.logger.info(`Email sent successfully to ${this.config.to}, Session: ${sessionId}`);
|
|
|
|
// Track sent message
|
|
await this._trackSentMessage(messageId, sessionId, token);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
this.logger.error('Failed to send email:', error.message);
|
|
// Clean up failed session
|
|
await this._removeSession(sessionId);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async _createSession(sessionId, notification, token) {
|
|
const session = {
|
|
id: sessionId,
|
|
token: token,
|
|
type: 'pty',
|
|
created: new Date().toISOString(),
|
|
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // Expires after 24 hours
|
|
createdAt: Math.floor(Date.now() / 1000),
|
|
expiresAt: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000),
|
|
cwd: process.cwd(),
|
|
notification: {
|
|
type: notification.type,
|
|
project: notification.project,
|
|
message: notification.message
|
|
},
|
|
status: 'waiting',
|
|
commandCount: 0,
|
|
maxCommands: 10
|
|
};
|
|
|
|
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2));
|
|
|
|
// Also save in PTY mapping format
|
|
const sessionMapPath = process.env.SESSION_MAP_PATH || path.join(__dirname, '../../data/session-map.json');
|
|
let sessionMap = {};
|
|
if (fs.existsSync(sessionMapPath)) {
|
|
try {
|
|
sessionMap = JSON.parse(fs.readFileSync(sessionMapPath, 'utf8'));
|
|
} catch (e) {
|
|
sessionMap = {};
|
|
}
|
|
}
|
|
|
|
// Use passed tmux session name or detect current session
|
|
let tmuxSession = notification.metadata?.tmuxSession || this._getCurrentTmuxSession() || 'claude-code-remote';
|
|
|
|
sessionMap[token] = {
|
|
type: 'pty',
|
|
createdAt: Math.floor(Date.now() / 1000),
|
|
expiresAt: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000),
|
|
cwd: process.cwd(),
|
|
sessionId: sessionId,
|
|
tmuxSession: tmuxSession,
|
|
description: `${notification.type} - ${notification.project}`
|
|
};
|
|
|
|
// Ensure directory exists
|
|
const mapDir = path.dirname(sessionMapPath);
|
|
if (!fs.existsSync(mapDir)) {
|
|
fs.mkdirSync(mapDir, { recursive: true });
|
|
}
|
|
|
|
fs.writeFileSync(sessionMapPath, JSON.stringify(sessionMap, null, 2));
|
|
|
|
this.logger.debug(`Session created: ${sessionId}, Token: ${token}`);
|
|
}
|
|
|
|
async _removeSession(sessionId) {
|
|
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
if (fs.existsSync(sessionFile)) {
|
|
fs.unlinkSync(sessionFile);
|
|
this.logger.debug(`Session removed: ${sessionId}`);
|
|
}
|
|
}
|
|
|
|
async _trackSentMessage(messageId, sessionId, token) {
|
|
let sentMessages = { messages: [] };
|
|
|
|
// Read existing data if file exists
|
|
if (fs.existsSync(this.sentMessagesPath)) {
|
|
try {
|
|
sentMessages = JSON.parse(fs.readFileSync(this.sentMessagesPath, 'utf8'));
|
|
} catch (e) {
|
|
this.logger.warn('Failed to read sent-messages.json, creating new one');
|
|
}
|
|
}
|
|
|
|
// Add new message
|
|
sentMessages.messages.push({
|
|
messageId: messageId,
|
|
sessionId: sessionId,
|
|
token: token,
|
|
type: 'notification',
|
|
sentAt: new Date().toISOString()
|
|
});
|
|
|
|
// Ensure directory exists
|
|
const dir = path.dirname(this.sentMessagesPath);
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
// Write updated data
|
|
fs.writeFileSync(this.sentMessagesPath, JSON.stringify(sentMessages, null, 2));
|
|
this.logger.debug(`Tracked sent message: ${messageId}`);
|
|
}
|
|
|
|
_generateEmailContent(notification, sessionId, token) {
|
|
const template = this._getTemplate(notification.type);
|
|
const timestamp = new Date().toLocaleString('zh-CN');
|
|
|
|
// Get project directory name (last level directory)
|
|
const projectDir = path.basename(process.cwd());
|
|
|
|
// Extract user question (from notification.metadata if available)
|
|
let userQuestion = '';
|
|
let claudeResponse = '';
|
|
|
|
if (notification.metadata) {
|
|
userQuestion = notification.metadata.userQuestion || '';
|
|
claudeResponse = notification.metadata.claudeResponse || '';
|
|
}
|
|
|
|
// Limit user question length for title
|
|
const maxQuestionLength = 30;
|
|
const shortQuestion = userQuestion.length > maxQuestionLength ?
|
|
userQuestion.substring(0, maxQuestionLength) + '...' : userQuestion;
|
|
|
|
// Generate more distinctive title
|
|
let enhancedSubject = template.subject;
|
|
if (shortQuestion) {
|
|
enhancedSubject = enhancedSubject.replace('{{project}}', `${projectDir} | ${shortQuestion}`);
|
|
} else {
|
|
enhancedSubject = enhancedSubject.replace('{{project}}', projectDir);
|
|
}
|
|
|
|
// Template variable replacement
|
|
const variables = {
|
|
project: projectDir,
|
|
message: notification.message,
|
|
timestamp: timestamp,
|
|
sessionId: sessionId,
|
|
token: token,
|
|
type: notification.type === 'completed' ? 'Task completed' : 'Waiting for input',
|
|
userQuestion: userQuestion || 'No specified task',
|
|
claudeResponse: claudeResponse || notification.message,
|
|
projectDir: projectDir,
|
|
shortQuestion: shortQuestion || 'No specific question',
|
|
subagentActivities: notification.metadata?.subagentActivities || '',
|
|
fullExecutionTrace: notification.metadata?.fullExecutionTrace ||
|
|
'No execution trace available. This may occur if the task completed very quickly or if tmux session logging is not enabled.'
|
|
};
|
|
|
|
let subject = enhancedSubject;
|
|
let html = template.html;
|
|
let text = template.text;
|
|
|
|
// Replace template variables
|
|
Object.keys(variables).forEach(key => {
|
|
const placeholder = new RegExp(`{{${key}}}`, 'g');
|
|
subject = subject.replace(placeholder, variables[key]);
|
|
// Escape HTML entities for HTML content
|
|
html = html.replace(placeholder, this._escapeHtml(variables[key]));
|
|
// No escaping needed for plain text
|
|
text = text.replace(placeholder, variables[key]);
|
|
});
|
|
|
|
return { subject, html, text };
|
|
}
|
|
|
|
_getTemplate(type) {
|
|
// Default templates
|
|
const templates = {
|
|
completed: {
|
|
subject: '[Claude-Code-Remote #{{token}}] Claude Code Task Completed - {{project}}',
|
|
html: `
|
|
<div style="font-family: 'Consolas', 'Monaco', 'Courier New', monospace; background-color: #f5f5f5; padding: 0; margin: 0;">
|
|
<div style="max-width: 900px; margin: 0 auto; background-color: #1e1e1e; border: 1px solid #333; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);">
|
|
<!-- Terminal Header -->
|
|
<div style="background-color: #2d2d2d; padding: 10px 15px; border-bottom: 1px solid #444;">
|
|
<table style="display: inline-table; vertical-align: middle;" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td style="padding: 0;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #ff5f56;"></div></td>
|
|
<td style="padding: 0 0 0 5px;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #ffbd2e;"></div></td>
|
|
<td style="padding: 0 0 0 5px;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #27c93f;"></div></td>
|
|
<td style="padding: 0 0 0 12px; color: #999; font-size: 14px; white-space: nowrap;">claude-code-remote@{{project}} - Task Completed</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Terminal Content -->
|
|
<div style="padding: 20px; background-color: #1a1a1a; min-height: 400px;">
|
|
<!-- User Input (Terminal Style) -->
|
|
<div style="margin-bottom: 30px;">
|
|
<div style="color: #00ff00; margin-bottom: 10px;">
|
|
<span style="color: #999;">$</span> <span style="color: #00ff00;">cat user_request.txt</span>
|
|
</div>
|
|
<div style="background-color: #262626; border-left: 4px solid #ff9800; padding: 15px 20px; margin-left: 20px; color: #f0f0f0; font-size: 15px; line-height: 1.6; font-weight: 500;">{{userQuestion}}</div>
|
|
</div>
|
|
|
|
<!-- Claude Response (Terminal Style) -->
|
|
<div style="margin-bottom: 30px;">
|
|
<div style="color: #00ff00; margin-bottom: 10px;">
|
|
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code execute</span>
|
|
</div>
|
|
<<<<<<< HEAD
|
|
<div style="margin-left: 20px;">
|
|
<div style="color: #999; margin-bottom: 10px; font-size: 13px;">
|
|
<span style="color: #00bcd4;">[INFO]</span> Processing request...<br>
|
|
<span style="color: #00bcd4;">[INFO]</span> Task execution started at {{timestamp}}
|
|
</div>
|
|
<div style="background-color: #1f1f1f; border-left: 4px solid #00ff00; padding: 15px 20px; color: #f0f0f0; font-size: 14px; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">{{claudeResponse}}</div>
|
|
<div style="color: #00ff00; margin-top: 10px; font-size: 13px;">
|
|
<span style="color: #00bcd4;">[SUCCESS]</span> Task completed successfully ✓
|
|
=======
|
|
<div style="background-color: #262626; border-left: 3px solid #00ff00; padding: 15px; margin: 10px 0; color: #f0f0f0; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%; font-size: 14px; line-height: 1.6;">{{claudeResponse}}</div>
|
|
<div style="color: #00ff00; margin-top: 10px;">
|
|
<span style="color: #00bcd4;">[SUCCESS]</span> Task completed successfully ✓
|
|
</div>
|
|
</div>
|
|
|
|
{{subagentActivities}}
|
|
|
|
<!-- Continue Instructions -->
|
|
<div style="margin: 30px 0 20px 0; border-top: 1px solid #333; padding-top: 20px;">
|
|
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code help --continue</span><br>
|
|
<div style="color: #f0f0f0; margin: 10px 0;">
|
|
<div style="color: #ff9800; margin-bottom: 10px;">→ TO CONTINUE THIS SESSION:</div>
|
|
<div style="background-color: #262626; padding: 15px; border: 1px solid #333; margin: 10px 0;">
|
|
Reply to this email directly with your next instruction.<br><br>
|
|
<span style="color: #999;">Examples:</span><br>
|
|
<span style="color: #00ff00;"> • "Add error handling to the function"</span><br>
|
|
<span style="color: #00ff00;"> • "Write unit tests for this code"</span><br>
|
|
<span style="color: #00ff00;"> • "Optimize the performance"</span>
|
|
>>>>>>> upstream/master
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{subagentActivities}}
|
|
|
|
<!-- Continue Instructions (Terminal Style) -->
|
|
<div style="margin: 40px 0; padding-top: 30px; border-top: 1px solid #333;">
|
|
<div style="color: #ff9800; margin-bottom: 15px;">
|
|
<span style="color: #999;">$</span> <span style="color: #ff9800;">claude-code --help continue</span>
|
|
</div>
|
|
<div style="margin-left: 20px; background-color: #0d0d0d; padding: 15px; border: 1px solid #333;">
|
|
<div style="color: #00ff00; margin-bottom: 10px; font-weight: bold;">TO CONTINUE THIS SESSION:</div>
|
|
<div style="color: #ccc; font-size: 13px; line-height: 1.8;">
|
|
Reply to this email directly with your next instruction.<br><br>
|
|
<span style="color: #666;">Examples:</span><br>
|
|
<span style="color: #999;">•</span> <span style="color: #00ff00;">"Add error handling to the function"</span><br>
|
|
<span style="color: #999;">•</span> <span style="color: #00ff00;">"Write unit tests for this code"</span><br>
|
|
<span style="color: #999;">•</span> <span style="color: #00ff00;">"Optimize the performance"</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Full Execution Trace (Terminal Style) -->
|
|
<div style="margin-top: 40px; border-top: 1px solid #333; padding-top: 30px;">
|
|
<div style="color: #666; margin-bottom: 15px;">
|
|
<span style="color: #666;">$</span> <span style="color: #666;">tail -n 1000 execution.log</span>
|
|
</div>
|
|
<details style="margin-left: 20px;">
|
|
<summary style="color: #666; font-size: 12px; cursor: pointer; user-select: none; margin-bottom: 10px;">
|
|
<span style="color: #999;">[</span><span style="color: #666;">Click to view full execution trace</span><span style="color: #999;">]</span>
|
|
</summary>
|
|
<div style="background-color: #0d0d0d; border: 1px solid #222; padding: 15px; max-height: 300px; overflow-y: auto; overflow-x: auto; scrollbar-width: thin; scrollbar-color: #444 #0d0d0d;">
|
|
<pre style="margin: 0; color: #888; font-size: 11px; line-height: 1.4; white-space: pre-wrap; word-wrap: break-word; font-family: 'Consolas', 'Monaco', 'Courier New', monospace;">{{fullExecutionTrace}}</pre>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
text: `
|
|
[Claude-Code-Remote #{{token}}] Claude Code Task Completed - {{projectDir}} | {{shortQuestion}}
|
|
|
|
Project: {{projectDir}}
|
|
Time: {{timestamp}}
|
|
Status: {{type}}
|
|
|
|
📝 Your Question:
|
|
{{userQuestion}}
|
|
|
|
🤖 Claude's Response:
|
|
{{claudeResponse}}
|
|
|
|
{{subagentActivities}}
|
|
|
|
<<<<<<< HEAD
|
|
====== FULL EXECUTION TRACE ======
|
|
{{fullExecutionTrace}}
|
|
==================================
|
|
|
|
=======
|
|
>>>>>>> upstream/master
|
|
How to Continue Conversation:
|
|
To continue conversation with Claude Code, please reply to this email directly and enter your instructions in the email body.
|
|
|
|
Example Replies:
|
|
• "Please continue optimizing the code"
|
|
• "Generate unit tests"
|
|
• "Explain the purpose of this function"
|
|
|
|
Session ID: {{sessionId}}
|
|
Security Note: Please do not forward this email, session will automatically expire after 24 hours
|
|
`
|
|
},
|
|
waiting: {
|
|
subject: '[Claude-Code-Remote #{{token}}] Claude Code Waiting for Input - {{project}}',
|
|
html: `
|
|
<div style="font-family: 'Consolas', 'Monaco', 'Courier New', monospace; background-color: #f5f5f5; padding: 0; margin: 0;">
|
|
<div style="max-width: 900px; margin: 0 auto; background-color: #1e1e1e; border: 1px solid #333; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);">
|
|
<!-- Terminal Header -->
|
|
<div style="background-color: #2d2d2d; padding: 10px 15px; border-bottom: 1px solid #444;">
|
|
<table style="display: inline-table; vertical-align: middle;" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td style="padding: 0;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #ff5f56;"></div></td>
|
|
<td style="padding: 0 0 0 5px;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #ffbd2e;"></div></td>
|
|
<td style="padding: 0 0 0 5px;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #27c93f;"></div></td>
|
|
<td style="padding: 0 0 0 12px; color: #999; font-size: 14px; white-space: nowrap;">claude-code-remote@{{project}} - Waiting for Input</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Terminal Content -->
|
|
<div style="padding: 20px; background-color: #1a1a1a; min-height: 400px;">
|
|
<!-- Session Info -->
|
|
<div style="color: #00ff00; margin-bottom: 20px;">
|
|
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code status</span><br>
|
|
<div style="margin-left: 20px; margin-top: 5px; color: #ccc;">
|
|
<span style="color: #ff9800;">PROJECT:</span> {{projectDir}}<br>
|
|
<span style="color: #ff9800;">SESSION:</span> #{{token}}<br>
|
|
<span style="color: #ff9800;">STATUS:</span> <span style="color: #ffeb3b;">⏳ Waiting for input</span><br>
|
|
<span style="color: #ff9800;">TIME:</span> {{timestamp}}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Waiting Message -->
|
|
<div style="margin: 20px 0;">
|
|
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code wait</span><br>
|
|
<div style="color: #ffeb3b; margin: 10px 0;">
|
|
<span style="color: #ff9800;">[WAITING]</span> Claude needs your input to continue...<br>
|
|
</div>
|
|
<div style="background-color: #262626; border-left: 3px solid #ffeb3b; padding: 15px; margin: 10px 0; color: #f0f0f0;">
|
|
{{message}}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Continue Instructions -->
|
|
<div style="margin: 30px 0 20px 0; border-top: 1px solid #333; padding-top: 20px;">
|
|
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code help --respond</span><br>
|
|
<div style="color: #f0f0f0; margin: 10px 0;">
|
|
<div style="color: #ff9800; margin-bottom: 10px;">→ ACTION REQUIRED:</div>
|
|
<div style="background-color: #262626; padding: 15px; border: 1px solid #333; margin: 10px 0;">
|
|
<span style="color: #ffeb3b;">Claude is waiting for your guidance.</span><br><br>
|
|
Reply to this email with your instructions to continue.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Session Footer -->
|
|
<div style="color: #666; font-size: 12px; margin-top: 30px; padding-top: 20px; border-top: 1px solid #333;">
|
|
<span style="color: #999;">$</span> <span style="color: #666;">echo $SESSION_INFO</span><br>
|
|
<div style="margin-left: 20px; margin-top: 5px;">
|
|
SESSION_ID={{sessionId}}<br>
|
|
EXPIRES_IN=24h<br>
|
|
SECURITY=Do not forward this email<br>
|
|
POWERED_BY=Claude-Code-Remote
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
text: `
|
|
[Claude-Code-Remote #{{token}}] Claude Code Waiting for Input - {{projectDir}}
|
|
|
|
Project: {{projectDir}}
|
|
Time: {{timestamp}}
|
|
Status: {{type}}
|
|
|
|
⏳ Waiting for Processing: {{message}}
|
|
|
|
Claude needs your further guidance. Please reply to this email to tell Claude what to do next.
|
|
|
|
Session ID: {{sessionId}}
|
|
Security Note: Please do not forward this email, session will automatically expire after 24 hours
|
|
`
|
|
}
|
|
};
|
|
|
|
return templates[type] || templates.completed;
|
|
}
|
|
|
|
validateConfig() {
|
|
if (!this.config.smtp) {
|
|
return { valid: false, error: 'SMTP configuration required' };
|
|
}
|
|
|
|
if (!this.config.smtp.host) {
|
|
return { valid: false, error: 'SMTP host required' };
|
|
}
|
|
|
|
if (!this.config.smtp.auth || !this.config.smtp.auth.user || !this.config.smtp.auth.pass) {
|
|
return { valid: false, error: 'SMTP authentication required' };
|
|
}
|
|
|
|
if (!this.config.to) {
|
|
return { valid: false, error: 'Recipient email required' };
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
async test() {
|
|
try {
|
|
if (!this.transporter) {
|
|
throw new Error('Email transporter not initialized');
|
|
}
|
|
|
|
// Verify SMTP connection
|
|
await this.transporter.verify();
|
|
|
|
// Send test email
|
|
const testNotification = {
|
|
type: 'completed',
|
|
title: 'Claude-Code-Remote Test',
|
|
message: 'This is a test email to verify that the email notification function is working properly.',
|
|
project: 'Claude-Code-Remote-Test',
|
|
metadata: {
|
|
test: true,
|
|
timestamp: new Date().toISOString(),
|
|
userQuestion: 'This is a test notification',
|
|
claudeResponse: 'Email notification system is working correctly.',
|
|
fullExecutionTrace: `> claude-remote test
|
|
|
|
🧪 Testing email notification system...
|
|
|
|
[2025-08-01T06:29:28.893Z] [Config] [INFO] Configuration loaded successfully
|
|
[2025-08-01T06:29:28.918Z] [Notifier] [INFO] Initialized 2 channels
|
|
[2025-08-01T06:29:29.015Z] [Channel:desktop] [INFO] Notification sent successfully
|
|
[2025-08-01T06:29:32.880Z] [Channel:email] [INFO] Email sent successfully
|
|
|
|
✅ Test completed successfully!
|
|
|
|
This is a test trace to demonstrate how the full execution trace will appear in actual usage.
|
|
When Claude Code completes a task, this section will contain the complete terminal output including:
|
|
- User commands
|
|
- Claude's responses
|
|
- Subagent activities
|
|
- Error messages
|
|
- Debug information
|
|
|
|
The trace provides complete transparency about what happened during task execution.`
|
|
}
|
|
};
|
|
|
|
const result = await this._sendImpl(testNotification);
|
|
return result;
|
|
} catch (error) {
|
|
this.logger.error('Email test failed:', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
getStatus() {
|
|
const baseStatus = super.getStatus();
|
|
return {
|
|
...baseStatus,
|
|
configured: this.validateConfig().valid,
|
|
supportsRelay: true,
|
|
smtp: {
|
|
host: this.config.smtp?.host || 'not configured',
|
|
port: this.config.smtp?.port || 'not configured',
|
|
secure: this.config.smtp?.secure || false
|
|
},
|
|
recipient: this.config.to || 'not configured'
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = EmailChannel; |