Merge pull request #2 from kevinsslin/fix/email-from-env-variable

Remove hardcoded values and implement environment-based configuration
This commit is contained in:
Kevin Lin 2025-07-28 14:44:37 +08:00 committed by GitHub
parent 5ddda6217e
commit e5de5a7932
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 196 additions and 69 deletions

View File

@ -1,13 +1,14 @@
# Claude Code Remote Email Configuration # Claude Code Remote Email Configuration Example
# Copy this file to .env and configure with your actual values
# ===== SMTP 发送邮件配置 ===== # ===== SMTP 发送邮件配置 =====
SMTP_HOST=smtp.gmail.com SMTP_HOST=smtp.gmail.com
SMTP_PORT=587 SMTP_PORT=465
SMTP_SECURE=false SMTP_SECURE=true
SMTP_USER=your-email@gmail.com SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password SMTP_PASS=your-app-password
# 发件人信息 # 发件人信息 (可选,默认使用 SMTP_USER)
EMAIL_FROM=your-email@gmail.com EMAIL_FROM=your-email@gmail.com
EMAIL_FROM_NAME=Claude Code Remote 通知系统 EMAIL_FROM_NAME=Claude Code Remote 通知系统
@ -20,19 +21,19 @@ IMAP_PASS=your-app-password
# ===== 邮件路由配置 ===== # ===== 邮件路由配置 =====
# 接收通知的邮箱地址 # 接收通知的邮箱地址
EMAIL_TO=your-notification-email@gmail.com EMAIL_TO=your-email@gmail.com
# 允许发送命令的邮箱地址(安全白名单) # 允许发送命令的邮箱地址(安全白名单)
ALLOWED_SENDERS=your-notification-email@gmail.com ALLOWED_SENDERS=your-email@gmail.com
# ===== 系统配置 ===== # ===== 系统配置 =====
# 会话映射文件路径 (请替换为你的实际路径) # 会话映射文件路径
SESSION_MAP_PATH=/Users/your-username/path/to/Claude-Code-Remote/src/data/session-map.json SESSION_MAP_PATH=/path/to/your/project/src/data/session-map.json
# 运行模式pty 或 tmux # 运行模式pty 或 tmux
INJECTION_MODE=pty INJECTION_MODE=pty
# Claude CLI 路径默认使用系统PATH中的claude # Claude CLI 路径(可选,默认使用系统PATH中的claude
CLAUDE_CLI_PATH=claude CLAUDE_CLI_PATH=claude
# 日志级别debug, info, warn, error # 日志级别debug, info, warn, error
@ -41,9 +42,55 @@ LOG_LEVEL=info
# 是否记录PTY输出调试用 # 是否记录PTY输出调试用
PTY_OUTPUT_LOG=false PTY_OUTPUT_LOG=false
# ===== 超时配置 =====
# 命令执行超时时间(毫秒)
COMMAND_TIMEOUT=10000
# SMTP 连接超时时间(毫秒)
SMTP_TIMEOUT=10000
# 通知超时时间(毫秒)
NOTIFICATION_TIMEOUT=3000
# 通知显示时间(毫秒)
NOTIFICATION_DISPLAY_TIME=10000
# ===== 邮件模板配置 ===== # ===== 邮件模板配置 =====
# 邮件检查间隔(秒) # 邮件检查间隔(秒)
CHECK_INTERVAL=30 CHECK_INTERVAL=20
# 会话超时时间(小时) # 会话超时时间(小时)
SESSION_TIMEOUT=24 SESSION_TIMEOUT=24
# ===== 测试配置(可选)=====
# 测试邮件使用的固定令牌(可选,默认动态生成)
TEST_TOKEN=
# Gmail 应用密码(用于测试脚本,可选)
GMAIL_APP_PASSWORD=
# ===== Gmail 配置说明 =====
# 1. 启用两步验证: https://myaccount.google.com/security
# 2. 生成应用密码: https://myaccount.google.com/apppasswords
# 3. 将生成的16位密码填入 SMTP_PASS 和 IMAP_PASS
# 4. 确保 SMTP_PORT=465 和 SMTP_SECURE=true (推荐SSL连接)
# ===== 其他邮件服务商配置示例 =====
# QQ邮箱:
# SMTP_HOST=smtp.qq.com
# SMTP_PORT=587 或 465
# IMAP_HOST=imap.qq.com
# IMAP_PORT=993
# 163邮箱:
# SMTP_HOST=smtp.163.com
# SMTP_PORT=587 或 465
# IMAP_HOST=imap.163.com
# IMAP_PORT=993
# Outlook:
# SMTP_HOST=smtp.live.com
# SMTP_PORT=587
# IMAP_HOST=imap-mail.outlook.com
# IMAP_PORT=993
EOF < /dev/null

1
.gitignore vendored
View File

@ -50,3 +50,4 @@ tmp/
temp/src/data/sessions/ temp/src/data/sessions/
src/data/processed-messages.json src/data/processed-messages.json
src/data/session-map.json src/data/session-map.json
src/data/sessions/*.json

View File

@ -178,7 +178,8 @@ class RemoteControlSetup {
} }
// If clauderun fails, try using full path command // If clauderun fails, try using full path command
console.log('🔄 Trying full path command...'); console.log('🔄 Trying full path command...');
const fallbackCommand = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" /Users/jessytsui/.nvm/versions/node/v18.17.0/bin/claude --dangerously-skip-permissions`; const claudePath = process.env.CLAUDE_CLI_PATH || 'claude';
const fallbackCommand = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" ${claudePath} --dangerously-skip-permissions`;
exec(fallbackCommand, (fallbackError) => { exec(fallbackCommand, (fallbackError) => {
if (fallbackError) { if (fallbackError) {
console.log(`❌ Full path command also failed: ${fallbackError.message}`); console.log(`❌ Full path command also failed: ${fallbackError.message}`);
@ -234,7 +235,7 @@ class RemoteControlSetup {
console.log('📱 Email testing:'); console.log('📱 Email testing:');
console.log(' Token will include session information, automatically routing to correct tmux session'); console.log(' Token will include session information, automatically routing to correct tmux session');
console.log(' Recipient email: jiaxicui446@gmail.com'); console.log(` Recipient email: ${process.env.EMAIL_TO}`);
console.log(' Reply with command: echo "Remote control test"\n'); console.log(' Reply with command: echo "Remote control test"\n');
console.log('🚨 Important reminders:'); console.log('🚨 Important reminders:');

View File

@ -5,6 +5,9 @@
* Main entry point for the CLI tool * Main entry point for the CLI tool
*/ */
// Load environment variables
require('dotenv').config();
const Logger = require('./src/core/logger'); const Logger = require('./src/core/logger');
const Notifier = require('./src/core/notifier'); const Notifier = require('./src/core/notifier');
const ConfigManager = require('./src/core/config'); const ConfigManager = require('./src/core/config');

View File

@ -6,32 +6,7 @@
}, },
"email": { "email": {
"type": "email", "type": "email",
"enabled": true, "enabled": true
"config": {
"smtp": {
"host": "smtp.feishu.cn",
"port": 465,
"secure": true,
"auth": {
"user": "noreply@pandalla.ai",
"pass": "kKgS3tNReRTL3RQC"
}
},
"imap": {
"host": "imap.feishu.cn",
"port": 993,
"secure": true,
"auth": {
"user": "noreply@pandalla.ai",
"pass": "kKgS3tNReRTL3RQC"
}
},
"from": "Claude-Code-Remote Notification System <noreply@pandalla.ai>",
"to": "jiaxicui446@gmail.com",
"template": {
"checkInterval": 30
}
}
}, },
"discord": { "discord": {
"type": "chat", "type": "chat",

View File

@ -8,24 +8,45 @@ require('dotenv').config();
async function sendTestReply() { async function sendTestReply() {
console.log('📧 Sending test email reply...\n'); console.log('📧 Sending test email reply...\n');
// Create test SMTP transporter (using Gmail) // Create test SMTP transporter (using environment variables)
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
service: 'gmail', host: process.env.SMTP_HOST || 'smtp.gmail.com',
port: parseInt(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === 'true',
auth: { auth: {
user: 'jiaxicui446@gmail.com', user: process.env.SMTP_USER,
pass: process.env.GMAIL_APP_PASSWORD || 'your-app-password' pass: process.env.SMTP_PASS
} }
}); });
// Use latest token // Generate or use test token from environment
const testToken = 'V5UPZ1UE'; // Latest token from session-map.json let testToken = process.env.TEST_TOKEN;
if (!testToken) {
// Try to read latest token from session map
try {
const sessionMapPath = process.env.SESSION_MAP_PATH || './src/data/session-map.json';
if (require('fs').existsSync(sessionMapPath)) {
const sessionMap = JSON.parse(require('fs').readFileSync(sessionMapPath, 'utf8'));
const tokens = Object.keys(sessionMap);
testToken = tokens[tokens.length - 1]; // Use latest token
}
} catch (error) {
console.log('Could not read session map, using generated token');
}
// Fallback: generate a test token
if (!testToken) {
testToken = Math.random().toString(36).substr(2, 8).toUpperCase();
}
}
const mailOptions = { const mailOptions = {
from: 'jiaxicui446@gmail.com', from: process.env.SMTP_USER,
to: 'noreply@pandalla.ai', to: process.env.SMTP_USER, // Self-send for testing
subject: `Re: [Claude-Code-Remote #${testToken}] Claude Code Task Completed - Claude-Code-Remote`, subject: `Re: [Claude-Code-Remote #${testToken}] Claude Code Task Completed - Claude-Code-Remote`,
text: 'Please explain the basic principles of quantum computing', text: 'Please explain the basic principles of quantum computing',
replyTo: 'jiaxicui446@gmail.com' replyTo: process.env.EMAIL_TO || process.env.ALLOWED_SENDERS
}; };
try { try {

View File

@ -58,9 +58,9 @@ class EmailChannel extends NotificationChannel {
pass: this.config.smtp.auth.pass pass: this.config.smtp.auth.pass
}, },
// Add timeout settings // Add timeout settings
connectionTimeout: 10000, connectionTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000,
greetingTimeout: 10000, greetingTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000,
socketTimeout: 10000 socketTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000
}); });
this.logger.debug('Email transporter initialized'); this.logger.debug('Email transporter initialized');

View File

@ -44,12 +44,12 @@ class DesktopChannel extends NotificationChannel {
// Try terminal-notifier first // Try terminal-notifier first
try { try {
const cmd = `terminal-notifier -title "${title}" -message "${message}" -sound "${sound}" -group "claude-code-remote"`; const cmd = `terminal-notifier -title "${title}" -message "${message}" -sound "${sound}" -group "claude-code-remote"`;
execSync(cmd, { timeout: 3000 }); execSync(cmd, { timeout: parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000 });
return true; return true;
} catch (e) { } catch (e) {
// Fallback to osascript // Fallback to osascript
const script = `display notification "${message}" with title "${title}"`; const script = `display notification "${message}" with title "${title}"`;
execSync(`osascript -e '${script}'`, { timeout: 3000 }); execSync(`osascript -e '${script}'`, { timeout: parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000 });
// Play sound separately // Play sound separately
this._playSound(sound); this._playSound(sound);
@ -63,7 +63,9 @@ class DesktopChannel extends NotificationChannel {
_sendLinux(title, message, sound) { _sendLinux(title, message, sound) {
try { try {
execSync(`notify-send "${title}" "${message}" -t 10000`, { timeout: 3000 }); const notificationTimeout = parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000;
const displayTime = parseInt(process.env.NOTIFICATION_DISPLAY_TIME) || 10000;
execSync(`notify-send "${title}" "${message}" -t ${displayTime}`, { timeout: notificationTimeout });
this._playSound(sound); this._playSound(sound);
return true; return true;
} catch (error) { } catch (error) {

View File

@ -58,19 +58,31 @@ class ConfigManager {
}, },
email: { email: {
type: 'email', type: 'email',
enabled: false, enabled: process.env.SMTP_USER ? true : false,
config: { config: {
smtp: { smtp: {
host: '', host: process.env.SMTP_HOST || 'smtp.gmail.com',
port: 587, port: parseInt(process.env.SMTP_PORT) || 587,
secure: false, secure: process.env.SMTP_SECURE === 'true',
auth: { auth: {
user: '', user: process.env.SMTP_USER || '',
pass: '' pass: process.env.SMTP_PASS || ''
} }
}, },
from: '', imap: {
to: [] host: process.env.IMAP_HOST || 'imap.gmail.com',
port: parseInt(process.env.IMAP_PORT) || 993,
secure: process.env.IMAP_SECURE !== 'false',
auth: {
user: process.env.IMAP_USER || process.env.SMTP_USER || '',
pass: process.env.IMAP_PASS || process.env.SMTP_PASS || ''
}
},
from: process.env.EMAIL_FROM || `${process.env.EMAIL_FROM_NAME || 'Claude Code Remote'} <${process.env.SMTP_USER}>`,
to: process.env.EMAIL_TO || '',
template: {
checkInterval: parseInt(process.env.CHECK_INTERVAL) || 30
}
} }
}, },
discord: { discord: {
@ -125,7 +137,7 @@ class ConfigManager {
try { try {
if (fs.existsSync(this.channelsConfigPath)) { if (fs.existsSync(this.channelsConfigPath)) {
const fileChannels = JSON.parse(fs.readFileSync(this.channelsConfigPath, 'utf8')); const fileChannels = JSON.parse(fs.readFileSync(this.channelsConfigPath, 'utf8'));
this._channels = { ...this._channels, ...fileChannels }; this._channels = this._deepMerge(this._channels, fileChannels);
} }
} catch (error) { } catch (error) {
this.logger.warn('Failed to load channels config:', error.message); this.logger.warn('Failed to load channels config:', error.message);

View File

@ -89,12 +89,11 @@ function isAllowed(fromAddress) {
return ALLOWED_SENDERS.some(allowed => addr.includes(allowed)); return ALLOWED_SENDERS.some(allowed => addr.includes(allowed));
} }
// Extract TaskPing token from subject // Extract Claude-Code-Remote token from subject
function extractTokenFromSubject(subject = '') { function extractTokenFromSubject(subject = '') {
const patterns = [ const patterns = [
/\[TaskPing\s+#([A-Za-z0-9_-]+)\]/, /\[Claude-Code-Remote\s+#([A-Z0-9]+)\]/,
/\[TaskPing\s+([A-Za-z0-9_-]+)\]/, /Re:\s*\[Claude-Code-Remote\s+#([A-Z0-9]+)\]/
/TaskPing:\s*([A-Za-z0-9_-]+)/i
]; ];
for (const pattern of patterns) { for (const pattern of patterns) {
@ -118,7 +117,7 @@ function cleanEmailText(text = '') {
line.includes('On') && line.includes('wrote:') || line.includes('On') && line.includes('wrote:') ||
line.includes('Session ID:') || line.includes('Session ID:') ||
line.includes('Session ID:') || line.includes('Session ID:') ||
line.includes('<noreply@pandalla.ai>') || line.includes(`<${process.env.SMTP_USER}>`) ||
line.includes('Claude-Code-Remote Notification System') || line.includes('Claude-Code-Remote Notification System') ||
line.includes('on 2025') && line.includes('wrote:') || line.includes('on 2025') && line.includes('wrote:') ||
line.match(/^>.*/) || // Quote lines start with > line.match(/^>.*/) || // Quote lines start with >
@ -161,7 +160,7 @@ function cleanEmailText(text = '') {
// Skip remaining email quotes // Skip remaining email quotes
if (trimmedLine.includes('Claude-Code-Remote Notification System') || if (trimmedLine.includes('Claude-Code-Remote Notification System') ||
trimmedLine.includes('<noreply@pandalla.ai>') || trimmedLine.includes(`<${process.env.SMTP_USER}>`) ||
trimmedLine.includes('on 2025')) { trimmedLine.includes('on 2025')) {
continue; continue;
} }

66
test-smtp.js Normal file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env node
/**
* Test SMTP connection using environment variables
*/
require('dotenv').config();
const nodemailer = require('nodemailer');
async function testSMTP() {
console.log('🔧 Testing SMTP connection...\n');
const config = {
host: process.env.SMTP_HOST || 'smtp.gmail.com',
port: parseInt(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
};
console.log('📋 SMTP Configuration:');
console.log(` Host: ${config.host}`);
console.log(` Port: ${config.port}`);
console.log(` Secure: ${config.secure}`);
console.log(` User: ${config.auth.user}`);
console.log(` Pass: ${'*'.repeat(config.auth.pass?.length || 0)}\n`);
try {
const transporter = nodemailer.createTransport(config);
console.log('🔄 Verifying connection...');
await transporter.verify();
console.log('✅ SMTP connection successful!\n');
console.log('📧 Sending test email...');
const info = await transporter.sendMail({
from: process.env.EMAIL_FROM || `Claude Code Remote <${process.env.SMTP_USER}>`,
to: process.env.EMAIL_TO,
subject: 'Claude Code Remote - SMTP Test',
text: 'This is a test email from Claude Code Remote. If you receive this, SMTP is working correctly!'
});
console.log(`✅ Test email sent successfully!`);
console.log(`📧 Message ID: ${info.messageId}`);
console.log(`📬 To: ${process.env.EMAIL_TO}`);
} catch (error) {
console.error('❌ SMTP test failed:', error.message);
if (error.code === 'EAUTH') {
console.error('\n💡 Authentication failed. Please check:');
console.error(' - Gmail App Password is correct');
console.error(' - Two-factor authentication is enabled');
console.error(' - Email credentials in .env file');
} else if (error.code === 'ETIMEDOUT') {
console.error('\n💡 Connection timeout. Please check:');
console.error(' - Internet connection');
console.error(' - Firewall settings');
console.error(' - SMTP host and port');
}
}
}
testSMTP().catch(console.error);