@@ -0,0 +1,27 @@ | |||
# 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 |
@@ -0,0 +1,56 @@ | |||
# 邮件删除工具 | |||
一个用于批量删除 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` - 邮箱功能测试脚本 | |||
@@ -0,0 +1,260 @@ | |||
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}`); | |||
}); |
@@ -0,0 +1,237 @@ | |||
<!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> |
@@ -0,0 +1,16 @@ | |||
{ | |||
"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" | |||
} | |||
} |
@@ -0,0 +1,59 @@ | |||
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(); |
@@ -0,0 +1,42 @@ | |||
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(); |