claude-code-remote-remake/src/channels/email/smtp.js.backup

640 lines
30 KiB
Plaintext
Raw Normal View History

Add full execution trace in email notifications (#11) (#14) * Add configuration option to disable subagent notifications - Added 'enableSubagentNotifications' config option (default: false) - Modified notification handler to check config before sending subagent notifications - Created documentation explaining the feature - Updated README with note about subagent notifications This addresses the issue where frequent subagent notifications can be distracting. Users can now control whether they receive notifications when subagents stop/start. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add subagent activity tracking in completion emails - Track subagent activities instead of sending individual notifications - Include subagent activity summary in completion emails - Update email templates to display subagent activities - Add SubagentTracker utility to manage activity tracking - Update documentation to explain the new behavior This provides a better user experience by: 1. Reducing notification noise from frequent subagent activities 2. Still providing full visibility into what subagents did 3. Consolidating all information in the completion email 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Improve subagent activity details and documentation - Enhanced activity capture to include up to 1000 characters (was 200) - Improved email display format with better visual separation - Added detection for initialization-only captures with helpful message - Added configuration option for activity detail level - Created comprehensive documentation explaining the timing limitation - Added visual indicators for processing status This addresses the issue where subagent outputs were truncated or only showed initialization messages. Users now get better visibility into what subagents are doing, with clear indication when full output is available in tmux. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * delete redundant files and modify README * Fix HTML escaping in email notifications - Add _escapeHtml function to properly escape HTML entities in emails - Escape user-generated content to prevent HTML tags from being hidden - Fix issue where <developer> and other HTML-like strings disappeared in emails - Apply escaping to both main email content and subagent activity summaries 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add full execution trace to email notifications - Add getFullExecutionTrace method to capture complete terminal output - Include execution trace in a scrollable section within emails - Add CSS styling for visible scrollbars on desktop - Clean trace output by removing command prompt boxes - Add fallback message when trace is not available This addresses issue #11 - providing transparency about task execution 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Redesign email notification UI with improved terminal aesthetics - Reorganize content hierarchy: user request at top, response second, trace at bottom - Preserve terminal-style commands ($ cat, $ claude-code execute, etc.) - Remove redundant session info footer - Implement smart execution trace capture from user input to completion - Add TraceCapture utility to track user input timestamps - Improve visual hierarchy while maintaining terminal aesthetics - Use border colors (orange for user, green for success) for better distinction - Make execution trace collapsible and de-emphasized This improves readability while preserving the terminal charm of the project. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix execution trace filtering to show only recent activity - Change filter logic to find LAST user input instead of first - Search backwards through content to find most recent "> " prompt - Only include content from that point forward - Add fallback to show last 100 lines if no user input found - This ensures trace shows only relevant recent execution, not entire history 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove duplicate content from execution trace - Skip the first user input line (already shown in main content) - Skip the last Claude response (already shown in main content) - Only show intermediate execution steps and tool usage - Clean up empty lines at beginning and end - This avoids redundancy and focuses trace on execution details 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix execution trace to properly remove complete user input and output - Track multi-line user input and skip all continuation lines - Detect when user input ends (empty line or next command) - Stop before the last Claude response to avoid truncation - Only show intermediate execution steps between input and output This ensures the trace shows the complete execution process without duplicating content already displayed in the main sections. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix multi-line user input capture and trace filtering - Capture complete multi-line user input (not just first line) - Join continuation lines with spaces for proper display - Preserve all execution details in trace (tool calls, outputs) - Only skip user input and final response, keep everything in between * Add configuration toggle for subagent activities in email - Added 'showSubagentActivitiesInEmail' config option (default: false) - Modified claude-remote.js to check config before including subagent activities - Created documentation explaining the configuration - Allows users to choose between concise emails (execution trace only) or detailed emails (both summaries) This addresses the redundancy between execution trace and subagent activities summary, giving users control over email content. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix Git merge conflict in email template and document subagent config - Removed Git merge conflict markers from email text template - Added documentation for showSubagentActivitiesInEmail config in README - Explained that subagent activities are disabled by default for concise emails 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Update README changelog for PR #10 and issue #11 - Added entry for subagent notifications configuration (PR #10) - Added entry for execution trace feature (issue #11) - Maintained chronological order in changelog 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * delete redundant files * fix #15: add local database into .gitignore * Fix execution trace display in email notifications - Remove collapsible details tag for better email client compatibility - Add configuration option to toggle execution trace display - Fix HTML escaping issue for executionTraceSection variable 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove data files from git tracking These session-specific data files should not be tracked in version control as they are machine-specific and cause issues when pulling on other machines. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix typo(?) in changelog (#13) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Naich <an.naich@gmail.com> Co-authored-by: JessyTsui <51992423+JessyTsui@users.noreply.github.com>
2025-08-02 04:07:34 +08:00
/**
* 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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
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;