Browse Source

first commit

master
lizhuang 2 weeks ago
commit
1e406d6e5b
7 changed files with 697 additions and 0 deletions
  1. 27
    0
      .gitignore
  2. 56
    0
      README.md
  3. 260
    0
      app.js
  4. 237
    0
      index.html
  5. 16
    0
      package.json
  6. 59
    0
      test-email.js
  7. 42
    0
      test.js

+ 27
- 0
.gitignore View File

@@ -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

+ 56
- 0
README.md View File

@@ -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` - 邮箱功能测试脚本



+ 260
- 0
app.js View File

@@ -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}`);
});

+ 237
- 0
index.html View File

@@ -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>

+ 16
- 0
package.json View File

@@ -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"
}
}

+ 59
- 0
test-email.js View File

@@ -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();

+ 42
- 0
test.js View File

@@ -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();

Loading…
Cancel
Save