# Logs | |||||
logs | |||||
*.log | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
# Dependencies | |||||
node_modules/ | |||||
package-lock.json | |||||
yarn.lock | |||||
# Build output | |||||
dist/ | |||||
out/ | |||||
build/ | |||||
# Electron | |||||
.DS_Store | |||||
Thumbs.db | |||||
# Environment | |||||
.env | |||||
.env.local | |||||
.env.development.local | |||||
.env.test.local | |||||
.env.production.local |
# 邮件删除工具 | |||||
一个用于批量删除 Lolipop 邮箱服务邮件的工具,通过网页界面操作,可以指定特定页码范围进行删除。 | |||||
## 功能特点 | |||||
- 登录 Lolipop 邮箱服务 | |||||
- 指定起始页码和结束页码批量删除邮件 | |||||
- 实时显示删除进度和状态 | |||||
- 包含连接测试功能 | |||||
## 安装 | |||||
项目依赖已本地化安装。如需重新安装依赖,请执行: | |||||
```bash | |||||
npm install | |||||
``` | |||||
## 使用方法 | |||||
1. 启动服务器: | |||||
```bash | |||||
npm start | |||||
``` | |||||
2. 在浏览器中访问:`http://localhost:3120` | |||||
3. 填写以下信息: | |||||
- 邮箱账号 | |||||
- 邮箱密码 | |||||
- 起始页码 | |||||
- 结束页码(可选,留空则删除到最后一页) | |||||
4. 点击"开始删除"按钮开始批量删除操作 | |||||
## 测试连接 | |||||
如需测试与服务器的连接是否正常,可以点击界面上的"测试连接"按钮。 | |||||
## 技术栈 | |||||
- Node.js | |||||
- Express.js - Web服务器框架 | |||||
- Playwright - 浏览器自动化 | |||||
- Server-Sent Events (SSE) - 实时进度反馈 | |||||
## 文件结构 | |||||
- `app.js` - 主应用文件,包含服务器逻辑和邮件删除功能 | |||||
- `index.html` - Web界面 | |||||
- `test.js` - 测试脚本 | |||||
- `test-email.js` - 邮箱功能测试脚本 | |||||
const { chromium } = require('playwright'); | |||||
const express = require('express'); | |||||
const path = require('path'); | |||||
const app = express(); | |||||
const port = 3120; | |||||
app.use(express.json()); | |||||
app.use(express.static(path.join(__dirname))); | |||||
let clients = []; | |||||
function sendToAllClients(message, paginationInfo = null) { | |||||
const data = JSON.stringify({ | |||||
message: message, | |||||
paginationInfo: paginationInfo | |||||
}); | |||||
console.log(`Sending to ${clients.length} clients: ${message}`); | |||||
clients.forEach(client => { | |||||
try { | |||||
client.res.write(`data: ${data}\n\n`); | |||||
} catch (error) { | |||||
console.error(`Error sending to client ${client.id}:`, error); | |||||
// Remove problematic client | |||||
clients = clients.filter(c => c.id !== client.id); | |||||
} | |||||
}); | |||||
} | |||||
app.get('/delete-progress', (req, res) => { | |||||
res.setHeader('Content-Type', 'text/event-stream'); | |||||
res.setHeader('Cache-Control', 'no-cache'); | |||||
res.setHeader('Connection', 'keep-alive'); | |||||
res.setHeader('Access-Control-Allow-Origin', '*'); | |||||
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); | |||||
const clientId = Date.now(); | |||||
clients.push({ | |||||
id: clientId, | |||||
res | |||||
}); | |||||
// Send initial connection confirmation | |||||
res.write(`data: ${JSON.stringify({message: '事件流连接已建立'})}\n\n`); | |||||
console.log(`Client ${clientId} connected to event stream`); | |||||
req.on('close', () => { | |||||
clients = clients.filter(client => client.id !== clientId); | |||||
console.log(`Client ${clientId} disconnected from event stream`); | |||||
}); | |||||
}); | |||||
async function deleteEmails(email, password, startPage, endPage) { | |||||
let browser = null; | |||||
let context = null; | |||||
let page = null; | |||||
console.log(`deleteEmails called with: email=${email}, startPage=${startPage}, endPage=${endPage || 'not specified'}`); | |||||
try { | |||||
console.log('Launching browser...'); | |||||
sendToAllClients('正在启动浏览器...'); | |||||
try { | |||||
browser = await chromium.launch({ | |||||
headless: true, | |||||
channel: 'chrome', | |||||
args: ['--no-sandbox', '--disable-setuid-sandbox'] | |||||
}); | |||||
console.log('Browser launched successfully'); | |||||
} catch (browserError) { | |||||
console.error('Failed to launch browser:', browserError); | |||||
sendToAllClients(`浏览器启动失败: ${browserError.message}`); | |||||
return; | |||||
} | |||||
try { | |||||
context = await browser.newContext(); | |||||
page = await context.newPage(); | |||||
console.log('Browser context and page created'); | |||||
} catch (contextError) { | |||||
console.error('Failed to create browser context or page:', contextError); | |||||
sendToAllClients(`浏览器上下文创建失败: ${contextError.message}`); | |||||
return; | |||||
} | |||||
console.log('Navigating to login page...'); | |||||
sendToAllClients('正在登录...'); | |||||
await page.goto('https://webmail.lolipop.jp/login'); | |||||
await page.fill('input[type="email"]', email); | |||||
await page.fill('input[type="password"]', password); | |||||
await page.click('button[type="submit"]'); | |||||
await page.waitForSelector('.css-1f8bwsm', { timeout: 10000 }); | |||||
sendToAllClients('登录成功'); | |||||
let currentPage = startPage; | |||||
let totalProcessed = 0; | |||||
let continueDeleting = true; | |||||
while (continueDeleting) { | |||||
// Navigate to specific page | |||||
await page.goto(`https://webmail.lolipop.jp/mail/INBOX?p=${currentPage}`); | |||||
await page.waitForSelector('.css-1f8bwsm', { timeout: 100000 }); | |||||
// Select all emails on current page | |||||
const emailsToDelete = await page.evaluate(() => { | |||||
const checkboxes = document.querySelectorAll('.css-1f8bwsm .css-1rlbz42 .css-1m9pwf3'); | |||||
let count = 0; | |||||
checkboxes.forEach((checkbox) => { | |||||
checkbox.click(); | |||||
count++; | |||||
}); | |||||
return count; | |||||
}); | |||||
if (emailsToDelete > 0) { | |||||
totalProcessed += emailsToDelete; | |||||
sendToAllClients(`当前页面选中 ${emailsToDelete} 封邮件`); | |||||
// Delete selected emails | |||||
await page.click('.css-i9gxme .css-zy67up .css-1yxmbwk[aria-label="削除"]'); | |||||
await page.waitForTimeout(1000); | |||||
sendToAllClients(`已删除第 ${currentPage} 页的 ${emailsToDelete} 封邮件`); | |||||
} | |||||
// Check if we should continue to next page | |||||
if (endPage && currentPage >= endPage) { | |||||
continueDeleting = false; | |||||
} else { | |||||
// Check if there is a next page | |||||
const nextPageButton = await page.$('.MuiBox-root.css-0 .css-1yxmbwk:nth-child(3)'); | |||||
const isNextPageDisabled = await nextPageButton.evaluate(button => | |||||
button.disabled || button.classList.contains('disabled') | |||||
); | |||||
if (isNextPageDisabled) { | |||||
continueDeleting = false; | |||||
} else { | |||||
currentPage++; | |||||
sendToAllClients(`进入第 ${currentPage} 页`); | |||||
} | |||||
} | |||||
} | |||||
sendToAllClients(`删除完成,共处理 ${totalProcessed} 封邮件`); | |||||
} catch (error) { | |||||
sendToAllClients(`发生错误: ${error.message}`); | |||||
console.error('Delete emails error:', error); | |||||
} finally { | |||||
try { | |||||
if (page) await page.close().catch(e => console.error('Error closing page:', e)); | |||||
if (context) await context.close().catch(e => console.error('Error closing context:', e)); | |||||
if (browser) await browser.close().catch(e => console.error('Error closing browser:', e)); | |||||
} catch (closeError) { | |||||
console.error('Error in cleanup:', closeError); | |||||
} | |||||
} | |||||
} | |||||
app.post('/start-delete', async (req, res) => { | |||||
const { email, password, startPage, endPage } = req.body; | |||||
console.log('Received delete request:', { email, startPage, endPage }); | |||||
if (!email || !password || !startPage) { | |||||
console.error('Missing required parameters'); | |||||
return res.status(400).json({ status: 'error', message: '缺少必要参数' }); | |||||
} | |||||
// Send response immediately | |||||
res.json({ status: 'started' }); | |||||
console.log('Starting delete operation...'); | |||||
// Set timeout to allow response to be sent before potentially long operation | |||||
setTimeout(() => { | |||||
console.log('Executing deleteEmails function...'); | |||||
// Call the function without await to not block | |||||
deleteEmails(email, password, parseInt(startPage), endPage ? parseInt(endPage) : null) | |||||
.then(() => console.log('Delete emails operation completed')) | |||||
.catch(err => console.error('Delete emails operation failed:', err)); | |||||
}, 100); | |||||
}); | |||||
async function getLastPageDate() { | |||||
let browser = null; | |||||
let context = null; | |||||
let page = null; | |||||
try { | |||||
browser = await chromium.launch({ | |||||
headless: true, | |||||
channel: 'chrome', | |||||
args: ['--no-sandbox', '--disable-setuid-sandbox'] | |||||
}); | |||||
context = await browser.newContext(); | |||||
page = await context.newPage(); | |||||
await page.goto('https://webmail.lolipop.jp/login'); | |||||
await page.fill('input[type="email"]', 'spdrakuten@spdsystem.com'); | |||||
await page.fill('input[type="password"]', 'YzFiMTJlYT2a4-4a'); | |||||
await page.click('button[type="submit"]'); | |||||
await page.waitForSelector('.css-1f8bwsm', { timeout: 10000 }); | |||||
// 点击最后一页按钮 | |||||
await page.waitForSelector('.MuiBox-root.css-0 .css-1yxmbwk'); | |||||
await page.click('.MuiBox-root.css-0 .css-1yxmbwk:nth-child(4)'); | |||||
await page.waitForTimeout(1000); | |||||
// 获取分页信息和最后一封邮件的日期 | |||||
const lastDate = await page.evaluate(() => { | |||||
const dates = document.querySelectorAll('.css-1e7qmj6'); | |||||
if (dates.length > 0) { | |||||
const lastDateStr = dates[dates.length - 1].textContent.replace(/:\d+0*$/, ''); | |||||
return lastDateStr; | |||||
} | |||||
return null; | |||||
}); | |||||
return lastDate; | |||||
} catch (error) { | |||||
console.error('Error getting last date:', error); | |||||
return null; | |||||
} finally { | |||||
try { | |||||
if (page) await page.close().catch(e => console.error('Error closing page:', e)); | |||||
if (context) await context.close().catch(e => console.error('Error closing context:', e)); | |||||
if (browser) await browser.close().catch(e => console.error('Error closing browser:', e)); | |||||
} catch (closeError) { | |||||
console.error('Error in cleanup:', closeError); | |||||
} | |||||
} | |||||
} | |||||
app.get('/get-last-date', async (req, res) => { | |||||
const lastDate = await getLastPageDate(); | |||||
res.json({ lastDate }); | |||||
}); | |||||
// Add a test endpoint | |||||
app.get('/test-event', (req, res) => { | |||||
console.log('Current clients:', clients.length); | |||||
clients.forEach((client, index) => { | |||||
console.log(`Client ${index} (ID: ${client.id}): ${typeof client.res}`); | |||||
}); | |||||
sendToAllClients('测试事件 - ' + new Date().toISOString()); | |||||
res.json({ | |||||
status: 'Test event sent to ' + clients.length + ' clients', | |||||
clientInfo: clients.map(c => ({ id: c.id })) | |||||
}); | |||||
}); | |||||
app.listen(port, () => { | |||||
console.log(`Server running at http://localhost:${port}`); | |||||
}); |
<!DOCTYPE html> | |||||
<html> | |||||
<head> | |||||
<title>邮件删除工具</title> | |||||
<style> | |||||
body { | |||||
font-family: Arial, sans-serif; | |||||
max-width: 800px; | |||||
margin: 20px auto; | |||||
padding: 20px; | |||||
} | |||||
.container { | |||||
border: 1px solid #ccc; | |||||
padding: 20px; | |||||
border-radius: 5px; | |||||
} | |||||
.form-group { | |||||
margin-bottom: 15px; | |||||
} | |||||
label { | |||||
display: block; | |||||
margin-bottom: 5px; | |||||
} | |||||
input[type="datetime-local"] { | |||||
padding: 5px; | |||||
width: 250px; | |||||
} | |||||
input[type="text"], | |||||
input[type="password"] { | |||||
padding: 5px; | |||||
width: 250px; | |||||
margin-bottom: 10px; | |||||
border: 1px solid #ccc; | |||||
border-radius: 4px; | |||||
} | |||||
button { | |||||
background-color: #4CAF50; | |||||
color: white; | |||||
padding: 10px 20px; | |||||
border: none; | |||||
border-radius: 4px; | |||||
cursor: pointer; | |||||
} | |||||
button:hover { | |||||
background-color: #45a049; | |||||
} | |||||
#logArea { | |||||
margin-top: 20px; | |||||
padding: 10px; | |||||
border: 1px solid #ddd; | |||||
border-radius: 4px; | |||||
height: 300px; | |||||
overflow-y: auto; | |||||
background-color: #f9f9f9; | |||||
} | |||||
.log-entry { | |||||
margin: 5px 0; | |||||
padding: 5px; | |||||
border-bottom: 1px solid #eee; | |||||
} | |||||
.pagination-info { | |||||
margin: 10px 0; | |||||
padding: 5px; | |||||
background-color: #f0f0f0; | |||||
border-radius: 4px; | |||||
font-size: 14px; | |||||
color: #666; | |||||
} | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<div class="container"> | |||||
<h2>邮件删除工具</h2> | |||||
<div class="form-group"> | |||||
<label for="email">邮箱账号:</label> | |||||
<input type="text" id="email" required value="spdrakuten@spdsystem.com"> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="password">邮箱密码:</label> | |||||
<input type="password" id="password" required value="YzFiMTJlYT2a4-4a"> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="startPage">起始页码:</label> | |||||
<input type="number" id="startPage" required min="1" value="100"> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="endPage">结束页码:</label> | |||||
<input type="number" id="endPage" min="1" placeholder="留空则删除到最后一页"> | |||||
</div> | |||||
<div class="pagination-info" id="paginationInfo"></div> | |||||
<button onclick="startDelete()">开始删除</button> | |||||
<button onclick="testConnection()" style="background-color: #2196F3;">测试连接</button> | |||||
<div id="logArea"></div> | |||||
</div> | |||||
<script> | |||||
function addLog(message) { | |||||
const logArea = document.getElementById('logArea'); | |||||
const logEntry = document.createElement('div'); | |||||
logEntry.className = 'log-entry'; | |||||
logEntry.textContent = `${new Date().toLocaleTimeString()} - ${message}`; | |||||
logArea.insertBefore(logEntry, logArea.firstChild); | |||||
} | |||||
async function startDelete() { | |||||
const email = document.getElementById('email').value; | |||||
const password = document.getElementById('password').value; | |||||
const startPage = parseInt(document.getElementById('startPage').value); | |||||
const endPage = document.getElementById('endPage').value ? parseInt(document.getElementById('endPage').value) : null; | |||||
if (!email || !password) { | |||||
alert('请输入邮箱账号和密码'); | |||||
return; | |||||
} | |||||
if (!startPage || startPage < 1) { | |||||
alert('请输入有效的起始页码'); | |||||
return; | |||||
} | |||||
if (endPage !== null && endPage < startPage) { | |||||
alert('结束页码必须大于或等于起始页码'); | |||||
return; | |||||
} | |||||
console.log('Selected page range:', { startPage, endPage }); | |||||
addLog('开始删除邮件...'); | |||||
try { | |||||
const response = await fetch('/start-delete', { | |||||
method: 'POST', | |||||
headers: { | |||||
'Content-Type': 'application/json' | |||||
}, | |||||
body: JSON.stringify({ | |||||
email, | |||||
password, | |||||
startPage, | |||||
endPage | |||||
}) | |||||
}); | |||||
const result = await response.json(); | |||||
if (!response.ok) { | |||||
throw new Error(result.message || '删除请求失败'); | |||||
} | |||||
addLog('删除请求已发送,开始监听进度...'); | |||||
// Close any existing EventSource | |||||
if (window.eventSource) { | |||||
window.eventSource.close(); | |||||
} | |||||
// Create new EventSource | |||||
window.eventSource = new EventSource('/delete-progress'); | |||||
addLog('正在建立事件流连接...'); | |||||
window.eventSource.onopen = () => { | |||||
addLog('与服务器的连接已建立'); | |||||
}; | |||||
window.eventSource.onmessage = (event) => { | |||||
try { | |||||
const data = JSON.parse(event.data); | |||||
addLog(data.message); | |||||
if (data.paginationInfo) { | |||||
document.getElementById('paginationInfo').textContent = data.paginationInfo; | |||||
} | |||||
// Check if deletion is complete | |||||
if (data.message.includes('删除完成')) { | |||||
window.eventSource.close(); | |||||
addLog('删除操作完成,连接已关闭'); | |||||
} | |||||
} catch (error) { | |||||
console.error('Error parsing message:', error); | |||||
addLog('收到未格式化消息: ' + event.data); // 如果解析失败,直接显示原始消息 | |||||
} | |||||
}; | |||||
window.eventSource.onerror = (error) => { | |||||
addLog('连接错误,尝试重新连接...'); | |||||
console.error('EventSource error:', error); | |||||
// Close the connection on error and try to reconnect | |||||
if (window.eventSource) { | |||||
window.eventSource.close(); | |||||
// Try to reconnect after a short delay | |||||
setTimeout(() => { | |||||
addLog('尝试重新连接...'); | |||||
window.eventSource = new EventSource('/delete-progress'); | |||||
}, 3000); | |||||
} | |||||
}; | |||||
} catch (error) { | |||||
addLog(`错误: ${error.message}`); | |||||
} | |||||
} | |||||
async function testConnection() { | |||||
addLog('测试连接中...'); | |||||
// Create test event source | |||||
if (window.testEventSource) { | |||||
window.testEventSource.close(); | |||||
} | |||||
window.testEventSource = new EventSource('/delete-progress'); | |||||
window.testEventSource.onopen = () => { | |||||
addLog('测试连接成功!'); | |||||
}; | |||||
window.testEventSource.onmessage = (event) => { | |||||
addLog('收到消息: ' + event.data); | |||||
}; | |||||
window.testEventSource.onerror = (error) => { | |||||
addLog('测试连接失败'); | |||||
console.error('Test connection error:', error); | |||||
window.testEventSource.close(); | |||||
}; | |||||
// Also call the test endpoint | |||||
try { | |||||
const response = await fetch('/test-event'); | |||||
const data = await response.json(); | |||||
addLog('服务器响应: ' + JSON.stringify(data)); | |||||
} catch (error) { | |||||
addLog('请求测试端点失败: ' + error.message); | |||||
} | |||||
} | |||||
</script> | |||||
</body> | |||||
</html> |
{ | |||||
"name": "lolipop-del-mail", | |||||
"author": "lizhuang", | |||||
"version": "1.0.0", | |||||
"description": "邮件删除工具", | |||||
"main": "app.js", | |||||
"scripts": { | |||||
"start": "node app.js", | |||||
"test": "node test.js" | |||||
}, | |||||
"dependencies": { | |||||
"express": "^4.21.2", | |||||
"playwright": "^1.42.1", | |||||
"playwright-chromium": "^1.42.1" | |||||
} | |||||
} |
const { chromium } = require('playwright'); | |||||
async function testEmailLogin() { | |||||
console.log('Starting email login test...'); | |||||
let browser = null; | |||||
try { | |||||
console.log('Launching browser...'); | |||||
browser = await chromium.launch({ | |||||
headless: true, | |||||
channel: 'chrome', | |||||
args: ['--no-sandbox', '--disable-setuid-sandbox'] | |||||
}); | |||||
console.log('Browser launched successfully'); | |||||
const context = await browser.newContext(); | |||||
console.log('Browser context created'); | |||||
const page = await context.newPage(); | |||||
console.log('Page created'); | |||||
console.log('Navigating to lolipop login...'); | |||||
await page.goto('https://webmail.lolipop.jp/login'); | |||||
console.log('Navigation successful'); | |||||
// Fill login form | |||||
console.log('Filling login form...'); | |||||
await page.fill('input[type="email"]', 'spdrakuten@spdsystem.com'); | |||||
await page.fill('input[type="password"]', 'YzFiMTJlYT2a4-4a'); | |||||
console.log('Form filled, clicking submit...'); | |||||
// Click login button | |||||
await page.click('button[type="submit"]'); | |||||
// Wait for successful login | |||||
console.log('Waiting for login to complete...'); | |||||
try { | |||||
await page.waitForSelector('.css-1f8bwsm', { timeout: 10000 }); | |||||
console.log('Login successful!'); | |||||
} catch (error) { | |||||
console.error('Login failed:', error); | |||||
// Take screenshot of the failure | |||||
await page.screenshot({ path: 'login-failed.png' }); | |||||
console.log('Screenshot saved as login-failed.png'); | |||||
} | |||||
} catch (error) { | |||||
console.error('Test failed:', error); | |||||
} finally { | |||||
if (browser) { | |||||
console.log('Closing browser...'); | |||||
await browser.close(); | |||||
console.log('Browser closed'); | |||||
} | |||||
} | |||||
} | |||||
testEmailLogin(); |
const { chromium } = require('playwright'); | |||||
async function testBrowser() { | |||||
console.log('Starting browser test...'); | |||||
let browser = null; | |||||
try { | |||||
console.log('Launching browser...'); | |||||
browser = await chromium.launch({ | |||||
headless: true, | |||||
channel: 'chrome', | |||||
args: ['--no-sandbox', '--disable-setuid-sandbox'] | |||||
}); | |||||
console.log('Browser launched successfully'); | |||||
const context = await browser.newContext(); | |||||
console.log('Browser context created'); | |||||
const page = await context.newPage(); | |||||
console.log('Page created'); | |||||
console.log('Navigating to google.com...'); | |||||
await page.goto('https://www.google.com'); | |||||
console.log('Navigation successful'); | |||||
const title = await page.title(); | |||||
console.log('Page title:', title); | |||||
console.log('Test completed successfully'); | |||||
} catch (error) { | |||||
console.error('Test failed:', error); | |||||
} finally { | |||||
if (browser) { | |||||
console.log('Closing browser...'); | |||||
await browser.close(); | |||||
console.log('Browser closed'); | |||||
} | |||||
} | |||||
} | |||||
testBrowser(); |