Initial commit: DVS Mail Server v1.0.0
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# Mail data
|
||||
mail_config.json
|
||||
email_data.json
|
||||
|
||||
# System files
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
desktop.ini
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 DVS (dvs-dvsxt)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,74 @@
|
||||
# ✉️ Mail Server — CLI Email Server
|
||||
|
||||
> A **command-line email server** (pure CLI) with SMTP receive/send support, mailbox management, and server control.
|
||||
|
||||
**Mail Server** is a Python CLI-based email server. It can receive email via SMTP (with configurable ports), send email, list/view received & sent messages, and manage SMTP server instances — all from the command line.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 📥 **Receive Email** | SMTP server with configurable ports (25 / 587 / 465) |
|
||||
| 📤 **Send Email** | Send plain-text or multipart emails |
|
||||
| 📋 **Mailbox List** | List & view received / sent emails |
|
||||
| 📎 **Download** | Get email message content |
|
||||
| 🖥️ **Multi-Server** | Start / stop multiple SMTP servers |
|
||||
| 🔐 **Admin Auth** | Password-protected server management |
|
||||
| 🗂️ **Persistent Storage** | Emails saved to JSON data file |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.8+
|
||||
- `dnspython`, `chardet` (for DNS & encoding)
|
||||
- Root/admin privileges (to bind SMTP ports)
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
python mail.py
|
||||
```
|
||||
|
||||
### CLI Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `help` | Show help / available commands |
|
||||
| `init` | Initialize configuration (domain, admin password) |
|
||||
| `start` | Start SMTP server |
|
||||
| `stop` | Stop SMTP server |
|
||||
| `status` | Show server status |
|
||||
| `send` | Send an email |
|
||||
| `list` | List emails |
|
||||
| `view` | View an email |
|
||||
| `download` | Download an email |
|
||||
| `clear` | Clear mailbox |
|
||||
| `exit` | Exit the program |
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
mail/
|
||||
├── mail.py # CLI email server (main program)
|
||||
└── README.md # This document
|
||||
```
|
||||
|
||||
> At runtime creates: `mail_config.json` (config) & `email_data.json` (email storage)
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Security Note
|
||||
|
||||
> Running an SMTP server on standard ports (25/587/465) requires **administrator/root** privileges. Be careful with open relays — configure authentication and restrict access. This project is for learning/reference.
|
||||
@@ -0,0 +1,917 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
命令行邮件服务器 - 纯CLI版本
|
||||
支持:接收邮件、发送邮件、查看邮件列表
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import socket
|
||||
import smtplib
|
||||
import dns.resolver
|
||||
import threading
|
||||
import socketserver
|
||||
import re
|
||||
import time
|
||||
import base64
|
||||
import json
|
||||
import hashlib
|
||||
import getpass
|
||||
from datetime import datetime
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.base import MIMEBase
|
||||
from email import encoders
|
||||
from email import message_from_string
|
||||
from email.header import decode_header
|
||||
import chardet
|
||||
|
||||
# ========== 配置 ==========
|
||||
CONFIG_FILE = "mail_config.json"
|
||||
DATA_FILE = "email_data.json"
|
||||
|
||||
# 全局配置
|
||||
config = {
|
||||
"domain": "", # 用户指定的域名
|
||||
"smtp_ports": [25, 587, 465],
|
||||
"admin_password_hash": ""
|
||||
}
|
||||
|
||||
# 数据存储
|
||||
email_storage = {
|
||||
'received': [],
|
||||
'sent': [],
|
||||
'servers': {}
|
||||
}
|
||||
|
||||
running_servers = {}
|
||||
|
||||
# ========== 颜色输出 ==========
|
||||
class Colors:
|
||||
GREEN = '\033[92m'
|
||||
RED = '\033[91m'
|
||||
YELLOW = '\033[93m'
|
||||
BLUE = '\033[94m'
|
||||
CYAN = '\033[96m'
|
||||
RESET = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
|
||||
def print_color(text, color=Colors.RESET):
|
||||
print(f"{color}{text}{Colors.RESET}")
|
||||
|
||||
def print_success(text):
|
||||
print_color(f"✅ {text}", Colors.GREEN)
|
||||
|
||||
def print_error(text):
|
||||
print_color(f"❌ {text}", Colors.RED)
|
||||
|
||||
def print_warning(text):
|
||||
print_color(f"⚠️ {text}", Colors.YELLOW)
|
||||
|
||||
def print_info(text):
|
||||
print_color(f"ℹ️ {text}", Colors.CYAN)
|
||||
|
||||
def print_title(text):
|
||||
print_color(f"\n{'='*60}", Colors.BOLD)
|
||||
print_color(f" {text}", Colors.BOLD)
|
||||
print_color(f"{'='*60}\n", Colors.BOLD)
|
||||
|
||||
# ========== 配置管理 ==========
|
||||
def load_config():
|
||||
"""加载配置文件"""
|
||||
global config
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
config.update(json.load(f))
|
||||
print_success(f"加载配置成功: {CONFIG_FILE}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print_error(f"加载配置失败: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
def save_config():
|
||||
"""保存配置文件"""
|
||||
try:
|
||||
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
print_success(f"配置已保存: {CONFIG_FILE}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print_error(f"保存配置失败: {e}")
|
||||
return False
|
||||
|
||||
def init_config():
|
||||
"""初始化配置(用户交互)"""
|
||||
print_title("邮件服务器初始化配置")
|
||||
|
||||
# 设置域名
|
||||
while True:
|
||||
domain = input("请输入您的邮件域名 (如: example.com): ").strip()
|
||||
if domain:
|
||||
config["domain"] = domain
|
||||
break
|
||||
print_error("域名不能为空,请重新输入")
|
||||
|
||||
# 设置管理员密码
|
||||
while True:
|
||||
password = getpass.getpass("请设置管理员密码: ")
|
||||
confirm = getpass.getpass("请再次输入密码: ")
|
||||
if password == confirm and len(password) >= 6:
|
||||
config["admin_password_hash"] = hashlib.sha256(password.encode('utf-8')).hexdigest()
|
||||
break
|
||||
print_error("密码不一致或长度小于6位,请重新设置")
|
||||
|
||||
# SMTP端口配置
|
||||
ports_input = input("请输入SMTP监听端口 (多个用逗号分隔,默认: 25,587,465): ").strip()
|
||||
if ports_input:
|
||||
try:
|
||||
ports = [int(p.strip()) for p in ports_input.split(',') if p.strip().isdigit()]
|
||||
if ports:
|
||||
config["smtp_ports"] = ports
|
||||
except:
|
||||
print_warning("端口格式错误,使用默认端口")
|
||||
|
||||
save_config()
|
||||
print_success("配置完成!")
|
||||
return True
|
||||
|
||||
# ========== 数据持久化 ==========
|
||||
def load_data():
|
||||
"""从JSON文件加载数据"""
|
||||
global email_storage
|
||||
if os.path.exists(DATA_FILE):
|
||||
try:
|
||||
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
||||
email_storage = json.load(f)
|
||||
print_success(f"加载数据成功: {DATA_FILE}")
|
||||
except Exception as e:
|
||||
print_error(f"加载数据失败: {e}")
|
||||
email_storage = {'received': [], 'sent': [], 'servers': {}}
|
||||
else:
|
||||
save_data()
|
||||
|
||||
def save_data():
|
||||
"""保存数据到JSON文件"""
|
||||
try:
|
||||
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(email_storage, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
print_error(f"保存数据失败: {e}")
|
||||
|
||||
# ========== 密码验证 ==========
|
||||
def verify_password():
|
||||
"""验证管理员密码"""
|
||||
if not config.get("admin_password_hash"):
|
||||
print_error("未设置管理员密码,请先运行 init 命令")
|
||||
return False
|
||||
|
||||
max_attempts = 3
|
||||
for attempt in range(max_attempts):
|
||||
password = getpass.getpass(f"请输入管理员密码 (尝试 {attempt+1}/{max_attempts}): ")
|
||||
password_hash = hashlib.sha256(password.encode('utf-8')).hexdigest()
|
||||
if password_hash == config["admin_password_hash"]:
|
||||
return True
|
||||
print_error("密码错误")
|
||||
|
||||
print_error("密码验证失败次数过多")
|
||||
return False
|
||||
|
||||
# ========== 邮件发送 ==========
|
||||
class MailSender:
|
||||
def __init__(self, domain):
|
||||
self.domain = domain
|
||||
|
||||
def find_mx_server(self, domain):
|
||||
"""查找域名的MX记录"""
|
||||
try:
|
||||
answers = dns.resolver.resolve(domain, 'MX')
|
||||
mx_records = []
|
||||
for rdata in answers:
|
||||
mx_records.append((rdata.preference, str(rdata.exchange).rstrip('.')))
|
||||
mx_records.sort()
|
||||
if mx_records:
|
||||
return mx_records[0][1]
|
||||
return domain
|
||||
except Exception as e:
|
||||
print_warning(f"MX查找失败: {e},使用域名直连")
|
||||
return domain
|
||||
|
||||
def send_email(self, from_addr, to_addr, subject, body, attachment_paths=None):
|
||||
"""发送邮件(支持附件)"""
|
||||
print_info(f"准备发送邮件: {from_addr} -> {to_addr}")
|
||||
|
||||
# 记录发送信息
|
||||
send_record = {
|
||||
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'from': from_addr,
|
||||
'to': to_addr,
|
||||
'subject': subject,
|
||||
'status': 'pending',
|
||||
'attachments': attachment_paths or []
|
||||
}
|
||||
|
||||
try:
|
||||
target_domain = to_addr.split('@')[1]
|
||||
mx_server = self.find_mx_server(target_domain)
|
||||
print_info(f"目标MX服务器: {mx_server}")
|
||||
|
||||
ports = [25, 587, 465]
|
||||
success = False
|
||||
|
||||
for port in ports:
|
||||
try:
|
||||
print_info(f"尝试连接 {mx_server}:{port}...")
|
||||
|
||||
if port == 465:
|
||||
server = smtplib.SMTP_SSL(mx_server, port, timeout=30)
|
||||
else:
|
||||
server = smtplib.SMTP(mx_server, port, timeout=30)
|
||||
if port == 587:
|
||||
server.starttls()
|
||||
|
||||
server.ehlo_or_helo_if_needed()
|
||||
|
||||
# 构建邮件
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = from_addr
|
||||
msg['To'] = to_addr
|
||||
msg['Subject'] = subject
|
||||
msg['Date'] = time.strftime('%a, %d %b %Y %H:%M:%S +0000', time.gmtime())
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
|
||||
# 添加附件
|
||||
if attachment_paths:
|
||||
for filepath in attachment_paths:
|
||||
if os.path.exists(filepath):
|
||||
filename = os.path.basename(filepath)
|
||||
with open(filepath, 'rb') as f:
|
||||
file_data = f.read()
|
||||
part = MIMEBase('application', 'octet-stream')
|
||||
part.set_payload(file_data)
|
||||
encoders.encode_base64(part)
|
||||
part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
|
||||
msg.attach(part)
|
||||
print_info(f"已添加附件: {filename}")
|
||||
|
||||
server.sendmail(from_addr, [to_addr], msg.as_string())
|
||||
server.quit()
|
||||
|
||||
print_success(f"邮件发送成功!(端口 {port})")
|
||||
success = True
|
||||
send_record['status'] = 'success'
|
||||
send_record['details'] = f'通过端口 {port} 发送成功'
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print_warning(f"端口 {port} 失败: {e}")
|
||||
continue
|
||||
|
||||
if not success:
|
||||
print_error("所有端口都尝试失败")
|
||||
send_record['status'] = 'failed'
|
||||
send_record['details'] = '所有端口尝试失败'
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"发送失败: {e}")
|
||||
send_record['status'] = 'error'
|
||||
send_record['details'] = str(e)
|
||||
|
||||
email_storage['sent'].append(send_record)
|
||||
save_data()
|
||||
return send_record['status'] == 'success'
|
||||
|
||||
# ========== 邮件接收服务器 ==========
|
||||
class SMTPHandler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
client_ip = self.client_address[0]
|
||||
port = self.server.server_address[1]
|
||||
print_info(f"新连接来自: {client_ip} (端口 {port})")
|
||||
|
||||
self.request.send(b'220 Mail Server Ready\r\n')
|
||||
|
||||
sender = None
|
||||
recipients = []
|
||||
data_buffer = []
|
||||
in_data = False
|
||||
expecting_data = False
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = self.request.recv(4096).decode('utf-8', errors='ignore').strip()
|
||||
if not data:
|
||||
break
|
||||
|
||||
lines = data.split('\r\n')
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
if line.upper().startswith(('HELO', 'EHLO')):
|
||||
self.request.send(b'250-mail.example.com\r\n250-PIPELINING\r\n250-SIZE 52428800\r\n250 OK\r\n')
|
||||
|
||||
elif line.upper().startswith('MAIL FROM:'):
|
||||
match = re.search(r'MAIL FROM:\s*<(.+?)>', line, re.IGNORECASE)
|
||||
if match:
|
||||
sender = match.group(1)
|
||||
self.request.send(b'250 OK\r\n')
|
||||
else:
|
||||
self.request.send(b'501 Bad sender address\r\n')
|
||||
|
||||
elif line.upper().startswith('RCPT TO:'):
|
||||
match = re.search(r'RCPT TO:\s*<(.+?)>', line, re.IGNORECASE)
|
||||
if match:
|
||||
recipient = match.group(1)
|
||||
recipients.append(recipient)
|
||||
self.request.send(b'250 OK\r\n')
|
||||
else:
|
||||
self.request.send(b'501 Bad recipient address\r\n')
|
||||
|
||||
elif line.upper() == 'DATA':
|
||||
if not recipients:
|
||||
self.request.send(b'503 No valid recipients\r\n')
|
||||
else:
|
||||
self.request.send(b'354 End data with <CR><LF>.<CR><LF>\r\n')
|
||||
in_data = True
|
||||
expecting_data = True
|
||||
|
||||
elif line == '.' and expecting_data:
|
||||
in_data = False
|
||||
expecting_data = False
|
||||
full_message = '\r\n'.join(data_buffer)
|
||||
self.process_email(sender, recipients, full_message, port)
|
||||
self.request.send(b'250 OK Message received\r\n')
|
||||
sender = None
|
||||
recipients = []
|
||||
data_buffer = []
|
||||
|
||||
elif line.upper() == 'QUIT':
|
||||
self.request.send(b'221 Bye\r\n')
|
||||
return
|
||||
|
||||
elif in_data:
|
||||
data_buffer.append(line)
|
||||
|
||||
else:
|
||||
self.request.send(b'250 OK\r\n')
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"处理错误: {e}")
|
||||
|
||||
def process_email(self, sender, recipients, raw_data, port):
|
||||
"""处理接收的邮件"""
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
print_success(f"收到邮件 - 端口 {port}")
|
||||
print_info(f"发件人: {sender}")
|
||||
print_info(f"收件人: {', '.join(recipients)}")
|
||||
|
||||
email_info = {
|
||||
'timestamp': timestamp,
|
||||
'port': port,
|
||||
'sender': sender,
|
||||
'recipients': recipients,
|
||||
'raw_data': raw_data[:5000], # 截断存储
|
||||
'parsed': {},
|
||||
'attachments': []
|
||||
}
|
||||
|
||||
try:
|
||||
parsed, attachments = self.parse_email(raw_data)
|
||||
email_info['parsed'] = parsed
|
||||
email_info['attachments'] = attachments
|
||||
|
||||
if parsed.get('subject'):
|
||||
print_info(f"主题: {parsed['subject']}")
|
||||
if parsed.get('body'):
|
||||
print_info(f"正文预览: {parsed['body'][:100]}...")
|
||||
if attachments:
|
||||
print_info(f"附件: {', '.join([a['name'] for a in attachments])}")
|
||||
|
||||
except Exception as e:
|
||||
print_warning(f"邮件解析失败: {e}")
|
||||
email_info['error'] = str(e)
|
||||
|
||||
email_storage['received'].append(email_info)
|
||||
save_data()
|
||||
|
||||
def parse_email(self, raw_data):
|
||||
"""解析邮件内容"""
|
||||
email_msg = message_from_string(raw_data)
|
||||
|
||||
parsed = {'subject': '', 'date': '', 'body': '', 'html_body': ''}
|
||||
attachments = []
|
||||
|
||||
parsed['subject'] = self.decode_header(email_msg.get('Subject', ''))
|
||||
parsed['date'] = email_msg.get('Date', '')
|
||||
|
||||
if email_msg.is_multipart():
|
||||
for part in email_msg.walk():
|
||||
content_type = part.get_content_type()
|
||||
content_disposition = str(part.get('Content-Disposition', ''))
|
||||
|
||||
if 'attachment' in content_disposition:
|
||||
filename = self.decode_header(part.get_filename())
|
||||
if filename:
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
attachments.append({
|
||||
'name': filename,
|
||||
'data': base64.b64encode(payload).decode('utf-8'),
|
||||
'size': len(payload)
|
||||
})
|
||||
continue
|
||||
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
charset = part.get_content_charset() or 'utf-8'
|
||||
try:
|
||||
text = payload.decode(charset, errors='replace')
|
||||
if content_type == 'text/plain':
|
||||
parsed['body'] = text
|
||||
elif content_type == 'text/html':
|
||||
parsed['html_body'] = text
|
||||
except:
|
||||
parsed['body'] = str(payload)
|
||||
else:
|
||||
payload = email_msg.get_payload(decode=True)
|
||||
if payload:
|
||||
charset = email_msg.get_content_charset() or 'utf-8'
|
||||
try:
|
||||
parsed['body'] = payload.decode(charset, errors='replace')
|
||||
except:
|
||||
parsed['body'] = str(payload)
|
||||
|
||||
return parsed, attachments
|
||||
|
||||
def decode_header(self, header):
|
||||
"""解码邮件头"""
|
||||
if not header:
|
||||
return ""
|
||||
try:
|
||||
parts = []
|
||||
for part, encoding in decode_header(header):
|
||||
if isinstance(part, bytes):
|
||||
if encoding:
|
||||
parts.append(part.decode(encoding, errors='replace'))
|
||||
else:
|
||||
detected = chardet.detect(part)
|
||||
enc = detected['encoding'] or 'utf-8'
|
||||
parts.append(part.decode(enc, errors='replace'))
|
||||
else:
|
||||
parts.append(part)
|
||||
return ''.join(parts)
|
||||
except:
|
||||
return str(header)
|
||||
|
||||
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
def start_smtp_server(port):
|
||||
"""启动SMTP服务器"""
|
||||
if port in running_servers:
|
||||
print_warning(f"端口 {port} 已在运行")
|
||||
return False
|
||||
|
||||
try:
|
||||
print_info(f"启动SMTP服务器在端口 {port}...")
|
||||
server = ThreadedTCPServer(('0.0.0.0', port), SMTPHandler)
|
||||
running_servers[port] = server
|
||||
|
||||
email_storage['servers'][str(port)] = {
|
||||
'status': 'running',
|
||||
'host': '0.0.0.0',
|
||||
'start_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
save_data()
|
||||
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
print_success(f"SMTP服务器已启动 (端口 {port})")
|
||||
return True
|
||||
|
||||
except PermissionError:
|
||||
print_error(f"需要管理员权限绑定端口 {port}")
|
||||
email_storage['servers'][str(port)] = {'status': 'error', 'error': 'Permission denied'}
|
||||
save_data()
|
||||
return False
|
||||
except OSError as e:
|
||||
print_error(f"端口 {port} 已被占用")
|
||||
email_storage['servers'][str(port)] = {'status': 'error', 'error': 'Port in use'}
|
||||
save_data()
|
||||
return False
|
||||
except Exception as e:
|
||||
print_error(f"启动失败: {e}")
|
||||
return False
|
||||
|
||||
def stop_smtp_server(port):
|
||||
"""停止SMTP服务器"""
|
||||
if port not in running_servers:
|
||||
print_warning(f"端口 {port} 未运行")
|
||||
return False
|
||||
|
||||
try:
|
||||
running_servers[port].shutdown()
|
||||
running_servers[port].server_close()
|
||||
del running_servers[port]
|
||||
|
||||
if str(port) in email_storage['servers']:
|
||||
email_storage['servers'][str(port)]['status'] = 'stopped'
|
||||
email_storage['servers'][str(port)]['stop_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
save_data()
|
||||
|
||||
print_success(f"SMTP服务器已停止 (端口 {port})")
|
||||
return True
|
||||
except Exception as e:
|
||||
print_error(f"停止失败: {e}")
|
||||
return False
|
||||
|
||||
# ========== 命令行界面 ==========
|
||||
def cmd_help():
|
||||
"""显示帮助信息"""
|
||||
print_title("帮助信息")
|
||||
print("""
|
||||
命令列表:
|
||||
init - 初始化配置(设置域名和密码)
|
||||
start [端口] - 启动SMTP服务器(默认所有配置端口)
|
||||
stop [端口] - 停止SMTP服务器
|
||||
status - 查看服务器状态
|
||||
send - 发送邮件(交互式)
|
||||
list - 列出所有邮件
|
||||
list in - 列出接收的邮件
|
||||
list out - 列出已发送的邮件
|
||||
view [ID] - 查看邮件详情
|
||||
download [ID] - 下载附件
|
||||
clear - 清空所有邮件记录
|
||||
help - 显示此帮助
|
||||
exit/quit - 退出程序
|
||||
""")
|
||||
|
||||
def cmd_init():
|
||||
"""初始化配置"""
|
||||
init_config()
|
||||
|
||||
def cmd_start(args):
|
||||
"""启动SMTP服务器"""
|
||||
if not config.get("domain"):
|
||||
print_error("请先运行 init 配置域名")
|
||||
return
|
||||
|
||||
ports = []
|
||||
if args:
|
||||
try:
|
||||
ports = [int(p.strip()) for p in args.split(',') if p.strip().isdigit()]
|
||||
except:
|
||||
pass
|
||||
|
||||
if not ports:
|
||||
ports = config.get("smtp_ports", [25, 587, 465])
|
||||
|
||||
for port in ports:
|
||||
start_smtp_server(port)
|
||||
|
||||
print_info(f"当前运行端口: {list(running_servers.keys())}")
|
||||
|
||||
def cmd_stop(args):
|
||||
"""停止SMTP服务器"""
|
||||
if not args:
|
||||
# 停止所有
|
||||
for port in list(running_servers.keys()):
|
||||
stop_smtp_server(port)
|
||||
return
|
||||
|
||||
try:
|
||||
port = int(args)
|
||||
stop_smtp_server(port)
|
||||
except:
|
||||
print_error("请输入有效的端口号")
|
||||
|
||||
def cmd_status():
|
||||
"""查看服务器状态"""
|
||||
print_title("服务器状态")
|
||||
|
||||
if not email_storage['servers']:
|
||||
print_info("没有配置任何服务器")
|
||||
return
|
||||
|
||||
for port, info in email_storage['servers'].items():
|
||||
status = info.get('status', 'unknown')
|
||||
status_color = Colors.GREEN if status == 'running' else Colors.YELLOW if status == 'stopped' else Colors.RED
|
||||
print(f"端口 {port}: {status_color}{status.upper()}{Colors.RESET}")
|
||||
if info.get('start_time'):
|
||||
print(f" 启动时间: {info['start_time']}")
|
||||
if info.get('error'):
|
||||
print(f" 错误: {info['error']}")
|
||||
|
||||
def cmd_send():
|
||||
"""交互式发送邮件"""
|
||||
if not config.get("domain"):
|
||||
print_error("请先运行 init 配置域名")
|
||||
return
|
||||
|
||||
if not verify_password():
|
||||
return
|
||||
|
||||
print_title("发送邮件")
|
||||
|
||||
# 发件人
|
||||
default_from = f"admin@{config['domain']}"
|
||||
from_addr = input(f"发件人邮箱 (默认: {default_from}): ").strip()
|
||||
if not from_addr:
|
||||
from_addr = default_from
|
||||
|
||||
# 收件人
|
||||
to_addr = input("收件人邮箱: ").strip()
|
||||
while not to_addr or '@' not in to_addr:
|
||||
print_error("请输入有效的邮箱地址")
|
||||
to_addr = input("收件人邮箱: ").strip()
|
||||
|
||||
# 主题
|
||||
subject = input("邮件主题: ").strip()
|
||||
if not subject:
|
||||
subject = "无主题"
|
||||
|
||||
# 正文
|
||||
print("请输入邮件内容 (输入空行结束,或输入 'END' 结束):")
|
||||
lines = []
|
||||
while True:
|
||||
line = input()
|
||||
if line == 'END' or (not line and lines and lines[-1] == ''):
|
||||
break
|
||||
lines.append(line)
|
||||
body = '\n'.join(lines[:-1]) if lines else ""
|
||||
|
||||
# 附件
|
||||
attachments = []
|
||||
while True:
|
||||
add = input("添加附件? (y/n): ").strip().lower()
|
||||
if add == 'y':
|
||||
filepath = input("请输入文件路径: ").strip()
|
||||
if os.path.exists(filepath):
|
||||
attachments.append(filepath)
|
||||
print_success(f"已添加: {filepath}")
|
||||
else:
|
||||
print_error("文件不存在")
|
||||
else:
|
||||
break
|
||||
|
||||
# 确认发送
|
||||
print("\n" + "="*50)
|
||||
print(f"发件人: {from_addr}")
|
||||
print(f"收件人: {to_addr}")
|
||||
print(f"主题: {subject}")
|
||||
print(f"正文: {body[:100]}...")
|
||||
if attachments:
|
||||
print(f"附件: {', '.join(attachments)}")
|
||||
print("="*50)
|
||||
|
||||
confirm = input("\n确认发送? (y/n): ").strip().lower()
|
||||
if confirm != 'y':
|
||||
print_info("已取消发送")
|
||||
return
|
||||
|
||||
sender = MailSender(config['domain'])
|
||||
success = sender.send_email(from_addr, to_addr, subject, body, attachments)
|
||||
|
||||
if success:
|
||||
print_success("邮件发送成功!")
|
||||
else:
|
||||
print_error("邮件发送失败")
|
||||
|
||||
def cmd_list(args):
|
||||
"""列出邮件"""
|
||||
if args == 'in':
|
||||
emails = email_storage['received']
|
||||
title = "接收的邮件"
|
||||
elif args == 'out':
|
||||
emails = email_storage['sent']
|
||||
title = "已发送邮件"
|
||||
else:
|
||||
print("用法: list in | list out")
|
||||
return
|
||||
|
||||
print_title(f"{title} (共 {len(emails)} 封)")
|
||||
|
||||
if not emails:
|
||||
print_info("暂无邮件")
|
||||
return
|
||||
|
||||
for i, email in enumerate(emails):
|
||||
if 'parsed' in email:
|
||||
subject = email['parsed'].get('subject', '无主题')
|
||||
else:
|
||||
subject = email.get('subject', '无主题')
|
||||
|
||||
sender = email.get('sender') or email.get('from', '未知')
|
||||
timestamp = email.get('timestamp', '未知时间')
|
||||
status = email.get('status', '')
|
||||
|
||||
status_str = f" [{status}]" if status else ""
|
||||
print(f" [{i}] {timestamp} | {sender} | {subject[:40]}{status_str}")
|
||||
|
||||
def cmd_view(args):
|
||||
"""查看邮件详情"""
|
||||
if not args:
|
||||
print_error("请指定邮件ID,如: view 0")
|
||||
return
|
||||
|
||||
try:
|
||||
idx = int(args)
|
||||
except:
|
||||
print_error("请输入有效的数字ID")
|
||||
return
|
||||
|
||||
print_title("邮件详情")
|
||||
|
||||
# 先尝试在接收邮件中查找
|
||||
if idx < len(email_storage['received']):
|
||||
email = email_storage['received'][idx]
|
||||
print(f"类型: 接收邮件")
|
||||
print(f"时间: {email.get('timestamp', '未知')}")
|
||||
print(f"端口: {email.get('port', '未知')}")
|
||||
print(f"发件人: {email.get('sender', '未知')}")
|
||||
print(f"收件人: {', '.join(email.get('recipients', []))}")
|
||||
|
||||
parsed = email.get('parsed', {})
|
||||
print(f"主题: {parsed.get('subject', '无')}")
|
||||
print(f"日期: {parsed.get('date', '无')}")
|
||||
|
||||
body = parsed.get('body', '')
|
||||
if body:
|
||||
print("\n正文:")
|
||||
print(body[:500] + ("..." if len(body) > 500 else ""))
|
||||
|
||||
attachments = email.get('attachments', [])
|
||||
if attachments:
|
||||
print(f"\n附件 ({len(attachments)}个):")
|
||||
for a in attachments:
|
||||
print(f" - {a['name']} ({a.get('size', 0)} bytes)")
|
||||
|
||||
return
|
||||
|
||||
# 再尝试在已发送邮件中查找
|
||||
sent_idx = idx - len(email_storage['received'])
|
||||
if sent_idx < len(email_storage['sent']):
|
||||
email = email_storage['sent'][sent_idx]
|
||||
print(f"类型: 已发送邮件")
|
||||
print(f"时间: {email.get('timestamp', '未知')}")
|
||||
print(f"发件人: {email.get('from', '未知')}")
|
||||
print(f"收件人: {email.get('to', '未知')}")
|
||||
print(f"主题: {email.get('subject', '无')}")
|
||||
print(f"状态: {email.get('status', '未知')}")
|
||||
print(f"详情: {email.get('details', '无')}")
|
||||
|
||||
attachments = email.get('attachments', [])
|
||||
if attachments:
|
||||
print(f"\n附件: {', '.join(attachments)}")
|
||||
return
|
||||
|
||||
print_error("未找到对应的邮件")
|
||||
|
||||
def cmd_download(args):
|
||||
"""下载附件"""
|
||||
if not args:
|
||||
print_error("请指定邮件ID,如: download 0")
|
||||
return
|
||||
|
||||
try:
|
||||
idx = int(args)
|
||||
except:
|
||||
print_error("请输入有效的数字ID")
|
||||
return
|
||||
|
||||
if idx >= len(email_storage['received']):
|
||||
print_error("邮件不存在")
|
||||
return
|
||||
|
||||
email = email_storage['received'][idx]
|
||||
attachments = email.get('attachments', [])
|
||||
|
||||
if not attachments:
|
||||
print_info("该邮件没有附件")
|
||||
return
|
||||
|
||||
print(f"找到 {len(attachments)} 个附件:")
|
||||
for i, a in enumerate(attachments):
|
||||
print(f" [{i}] {a['name']} ({a.get('size', 0)} bytes)")
|
||||
|
||||
try:
|
||||
attach_idx = int(input("选择要下载的附件编号: ").strip())
|
||||
if attach_idx < 0 or attach_idx >= len(attachments):
|
||||
print_error("无效编号")
|
||||
return
|
||||
except:
|
||||
print_error("请输入有效的数字")
|
||||
return
|
||||
|
||||
attachment = attachments[attach_idx]
|
||||
file_data = base64.b64decode(attachment['data'])
|
||||
|
||||
# 创建下载目录
|
||||
if not os.path.exists('downloads'):
|
||||
os.makedirs('downloads')
|
||||
|
||||
filename = attachment['name']
|
||||
filepath = os.path.join('downloads', filename)
|
||||
|
||||
# 处理重名
|
||||
counter = 1
|
||||
while os.path.exists(filepath):
|
||||
name, ext = os.path.splitext(filename)
|
||||
filepath = os.path.join('downloads', f"{name}_{counter}{ext}")
|
||||
counter += 1
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(file_data)
|
||||
|
||||
print_success(f"附件已保存: {filepath}")
|
||||
|
||||
def cmd_clear():
|
||||
"""清空邮件记录"""
|
||||
if not verify_password():
|
||||
return
|
||||
|
||||
confirm = input("确定要清空所有邮件记录吗? (yes/no): ").strip().lower()
|
||||
if confirm != 'yes':
|
||||
print_info("已取消")
|
||||
return
|
||||
|
||||
email_storage['received'] = []
|
||||
email_storage['sent'] = []
|
||||
save_data()
|
||||
print_success("所有邮件记录已清空")
|
||||
|
||||
def cmd_exit():
|
||||
"""退出程序"""
|
||||
print_info("正在停止所有服务器...")
|
||||
for port in list(running_servers.keys()):
|
||||
stop_smtp_server(port)
|
||||
print_success("再见!")
|
||||
sys.exit(0)
|
||||
|
||||
# ========== 主函数 ==========
|
||||
def main():
|
||||
"""主入口"""
|
||||
print_title("命令行邮件服务器 v2.0")
|
||||
print(f"当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# 加载配置和数据
|
||||
config_loaded = load_config()
|
||||
load_data()
|
||||
|
||||
if not config_loaded:
|
||||
print_warning("未找到配置文件,请运行 init 初始化")
|
||||
|
||||
print(f"域名: {config.get('domain', '未设置')}")
|
||||
print(f"SMTP端口: {config.get('smtp_ports', [])}")
|
||||
print(f"接收邮件: {len(email_storage.get('received', []))} 封")
|
||||
print(f"发送邮件: {len(email_storage.get('sent', []))} 封")
|
||||
print(f"运行端口: {list(running_servers.keys())}")
|
||||
print("\n输入 'help' 查看命令列表\n")
|
||||
|
||||
# 命令循环
|
||||
while True:
|
||||
try:
|
||||
cmd = input("mail> ").strip().lower()
|
||||
if not cmd:
|
||||
continue
|
||||
|
||||
parts = cmd.split(maxsplit=1)
|
||||
command = parts[0]
|
||||
args = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
if command in ['exit', 'quit']:
|
||||
cmd_exit()
|
||||
elif command == 'help':
|
||||
cmd_help()
|
||||
elif command == 'init':
|
||||
cmd_init()
|
||||
elif command == 'start':
|
||||
cmd_start(args)
|
||||
elif command == 'stop':
|
||||
cmd_stop(args)
|
||||
elif command == 'status':
|
||||
cmd_status()
|
||||
elif command == 'send':
|
||||
cmd_send()
|
||||
elif command == 'list':
|
||||
cmd_list(args)
|
||||
elif command == 'view':
|
||||
cmd_view(args)
|
||||
elif command == 'download':
|
||||
cmd_download(args)
|
||||
elif command == 'clear':
|
||||
cmd_clear()
|
||||
else:
|
||||
print_error(f"未知命令: {command},输入 'help' 查看帮助")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n")
|
||||
cmd_exit()
|
||||
except Exception as e:
|
||||
print_error(f"错误: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user