错误原因:
diff --git a/test/manual/smtp/sendEmailTest.py b/test/manual/smtp/sendEmailTest.py
index c275c94..3f25a58 100644
--- a/test/manual/smtp/sendEmailTest.py
+++ b/test/manual/smtp/sendEmailTest.py
@@ -1,10 +1,11 @@
from email.mime.text import MIMEText
import smtplib
+import os
msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
from_addr = "admin@domain.com"
-password = "admin"
+password = os.environ["SMAIL_TEST_SMTP_PASSWORD"]
to_addr = "admin@domain.com"
smtp_server = "127.0.0.1"
@@ -12,4 +13,4 @@ server = smtplib.SMTP(smtp_server, 25)
server.starttls()
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
-server.quit()
\ No newline at end of file
+server.quit()
diff --git a/test/run_deploy.py b/test/run_deploy.py
index cf3d216..e07a86b 100644
--- a/test/run_deploy.py
+++ b/test/run_deploy.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from tools.remote_ssh import connect_remote
+connect_remote(ssh)
sftp = ssh.open_sftp()
print('Uploading out/smail to /opt/smail/smail.new ...')
sftp.put('out/smail', '/opt/smail/smail.new')
diff --git a/test/tools/add_dns.py b/test/tools/add_dns.py
index f4b46ca..bfd07a4 100644
--- a/test/tools/add_dns.py
+++ b/test/tools/add_dns.py
@@ -1,8 +1,9 @@
import urllib.request, urllib.parse, hmac, hashlib, base64, time, json, paramiko, subprocess
client = paramiko.SSHClient()
-client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-client.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+client.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(client)
stdin, stdout, stderr = client.exec_command('cat /opt/smail/ssl/dkim/dkim.priv')
priv_key = stdout.read().decode('utf-8')
client.close()
diff --git a/test/tools/cat_remote_configs.py b/test/tools/cat_remote_configs.py
index 84f9988..c2abe5a 100644
--- a/test/tools/cat_remote_configs.py
+++ b/test/tools/cat_remote_configs.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
_, out, _ = ssh.exec_command('cat /opt/smail/config/smtp.json; echo ===; cat /opt/smail/config/email.json')
print(out.read().decode('utf-8', errors='ignore'))
ssh.close()
diff --git a/test/tools/check_auth_codes.py b/test/tools/check_auth_codes.py
index 0436ff6..c584fc5 100644
--- a/test/tools/check_auth_codes.py
+++ b/test/tools/check_auth_codes.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command("sqlite3 /opt/smail/data/smail.db \"SELECT * FROM client_credential;\"")
print('client_credential:\n', stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command("sqlite3 /opt/smail/data/smail.db \"SELECT * FROM protocol_credential;\"")
diff --git a/test/tools/check_db_full.py b/test/tools/check_db_full.py
index 1507a45..d6ef444 100644
--- a/test/tools/check_db_full.py
+++ b/test/tools/check_db_full.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, type, subject, from_address, send_user_id, status, error, create_time FROM email WHERE id >= 20;"')
print("Security questions:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, user_id, email_id, is_read, group_id, status, create FROM user_email WHERE email_id >= 20;"')
diff --git a/test/tools/check_emails_10.py b/test/tools/check_emails_10.py
index a4f29d2..4fc0aa5 100644
--- a/test/tools/check_emails_10.py
+++ b/test/tools/check_emails_10.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, type, subject, from_address, [to], send_user_id, status FROM email WHERE id >= 10;"')
print("Emails >= 10:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, user_id, email_id, is_read, group_id, status FROM user_email WHERE email_id >= 10;"')
diff --git a/test/tools/check_err24.py b/test/tools/check_err24.py
index d029cc9..7575b02 100644
--- a/test/tools/check_err24.py
+++ b/test/tools/check_err24.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, error FROM email WHERE id = 24;"')
print("Email 24 error:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT * FROM email WHERE id = 24;"')
diff --git a/test/tools/check_external.py b/test/tools/check_external.py
index 299cc3b..a1476df 100644
--- a/test/tools/check_external.py
+++ b/test/tools/check_external.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command("sqlite3 /opt/smail/data/smail.db \"SELECT sql FROM sqlite_master WHERE name='external_email';\"")
print('SCHEMA:\n', stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command("sqlite3 /opt/smail/data/smail.db \"SELECT * FROM external_email;\"")
diff --git a/test/tools/check_inbox_query.py b/test/tools/check_inbox_query.py
index d69b109..bde97cf 100644
--- a/test/tools/check_inbox_query.py
+++ b/test/tools/check_inbox_query.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT e.id, e.subject, e.type, ue.status, ue.group_id, ue.is_read FROM email e LEFT JOIN user_email ue ON e.id = ue.email_id WHERE ue.user_id = 1 AND ue.status = 0 AND e.type = 0 AND (ue.group_id = 0 OR ue.group_id = 1) ORDER BY e.id DESC;"')
print("Inbox query result:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT e.id, e.subject, e.type, e.status, e.error FROM email e WHERE e.send_user_id = 1 ORDER BY e.id DESC LIMIT 5;"')
diff --git a/test/tools/check_log.py b/test/tools/check_log.py
index ab29699..64c7836 100644
--- a/test/tools/check_log.py
+++ b/test/tools/check_log.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command("tail -n 25 /opt/smail/log/stdout.log")
print(stdout.read().decode("utf-8", errors="ignore"))
stdin, stdout, stderr = ssh.exec_command("ps aux | grep smail")
diff --git a/test/tools/check_log_send.py b/test/tools/check_log_send.py
index 220428f..6d26368 100644
--- a/test/tools/check_log_send.py
+++ b/test/tools/check_log_send.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('tail -n 40 /opt/smail/log/runtime-2026-09-14.log')
print("Grep 6567567:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('tail -n 40 /opt/smail/log/stdout.log')
diff --git a/test/tools/check_remote_db.py b/test/tools/check_remote_db.py
index ba5e905..48231fd 100644
--- a/test/tools/check_remote_db.py
+++ b/test/tools/check_remote_db.py
@@ -1,7 +1,8 @@
import paramiko
client = paramiko.SSHClient()
-client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-client.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=8)
+client.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(client)
cmd = 'systemctl list-unit-files | grep -E "(mysql|mariadb|postgresql)"'
stdin, stdout, stderr = client.exec_command(cmd)
print("DB units:\n", stdout.read().decode('utf-8'))
diff --git a/test/tools/check_remote_services.py b/test/tools/check_remote_services.py
index 6ada672..c8a38c6 100644
--- a/test/tools/check_remote_services.py
+++ b/test/tools/check_remote_services.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
_, out, _ = ssh.exec_command('ss -tulpn; ps aux | grep smail')
print(out.read().decode('utf-8', errors='ignore'))
ssh.close()
\ No newline at end of file
diff --git a/test/tools/check_remote_status.py b/test/tools/check_remote_status.py
index 5e075fa..04a6727 100644
--- a/test/tools/check_remote_status.py
+++ b/test/tools/check_remote_status.py
@@ -1,8 +1,9 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!")
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
sftp = ssh.open_sftp()
with sftp.open("/tmp/test_api.py", "w") as f:
diff --git a/test/tools/check_row22.py b/test/tools/check_row22.py
index 76a12a8..26c4deb 100644
--- a/test/tools/check_row22.py
+++ b/test/tools/check_row22.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "date"')
print("Security questions:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "date -u"')
diff --git a/test/tools/check_srv.py b/test/tools/check_srv.py
index 455edf8..7401c92 100644
--- a/test/tools/check_srv.py
+++ b/test/tools/check_srv.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('date')
print("Server Date:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('uptime')
diff --git a/test/tools/check_ue.py b/test/tools/check_ue.py
index d648dcc..f4011cc 100644
--- a/test/tools/check_ue.py
+++ b/test/tools/check_ue.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email_id FROM user_email WHERE email_id = 22;"')
print("Security questions:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email_id FROM user_email WHERE email_id = 23;"')
diff --git a/test/tools/debug_ssh.py b/test/tools/debug_ssh.py
index 9cdb79f..8ca519f 100644
--- a/test/tools/debug_ssh.py
+++ b/test/tools/debug_ssh.py
@@ -1,8 +1,9 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
def run(cmd):
stdin, stdout, stderr = ssh.exec_command(cmd)
diff --git a/test/tools/decrypt_external.py b/test/tools/decrypt_external.py
index fa883be..4b06768 100644
--- a/test/tools/decrypt_external.py
+++ b/test/tools/decrypt_external.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
s = '''import sqlite3, base64, glob
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
diff --git a/test/tools/deploy_review.py b/test/tools/deploy_review.py
new file mode 100644
index 0000000..764adf2
--- /dev/null
+++ b/test/tools/deploy_review.py
@@ -0,0 +1,121 @@
+"""仅在 47 服务器备份并升级 Smail,保留双库和失败证据。"""
+
+import argparse
+import hashlib
+import json
+import os
+from pathlib import Path
+import shutil
+import sqlite3
+import subprocess
+import time
+
+
+ROOT = Path('/opt/smail')
+STAGING = Path('/var/tmp/smail-review-20260917')
+DATABASES = ('smail.db', 'smail-state.db')
+
+
+def run(*args):
+ return subprocess.check_output(args, text=True).strip()
+
+
+def digest(path):
+ with path.open('rb') as stream:
+ return hashlib.file_digest(stream, 'sha256').hexdigest()
+
+
+def snapshot(source, target):
+ with sqlite3.connect(f'file:{source}?mode=ro', uri=True) as original:
+ with sqlite3.connect(target) as copied:
+ original.backup(copied)
+ assert copied.execute('PRAGMA integrity_check').fetchone()[0] == 'ok'
+ target.chmod(0o600)
+
+
+def wait_ready():
+ for _ in range(60):
+ result = subprocess.run([
+ 'curl', '--silent', '--fail', '--max-time', '2', '--noproxy', '*',
+ '--cacert', str(ROOT / 'ssl/custom.crt'), '--resolve',
+ 'mail.solit.cn:443:127.0.0.1', 'https://mail.solit.cn/api/ping',
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ if result.returncode == 0 and run('systemctl', 'is-active', 'smail.service') == 'active':
+ return
+ time.sleep(1)
+ raise RuntimeError('升级后服务未就绪')
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--main-sha', required=True)
+ parser.add_argument('--admin-sha', required=True)
+ args = parser.parse_args()
+ os.umask(0o077)
+ assert os.geteuid() == 0 and ROOT.resolve() == ROOT and not ROOT.is_symlink()
+ assert run('hostname') == 'ikun2'
+ assert '172.31.134.42/20' in run('ip', '-4', 'address', 'show')
+ assert run('systemctl', 'show', 'smail.service', '--property=User', '--value') == 'smail'
+ assert run('systemctl', 'is-active', 'smail.service') == 'active'
+ expected = {'smail': args.main_sha, 'smail-admin': args.admin_sha}
+ for name, checksum in expected.items():
+ path = STAGING / name
+ with path.open('rb') as binary:
+ header = binary.read(20)
+ assert header[:5] == b'\x7fELF\x02' and header[18:20] == b'\x3e\x00'
+ assert digest(path) == checksum
+ assert not (ROOT / name).is_symlink()
+
+ backup = Path('/var/backups') / time.strftime('smail-before-outbox-%Y%m%d-%H%M%S')
+ backup.mkdir(mode=0o700)
+ installed = False
+ subprocess.run(['systemctl', 'stop', 'smail.service'], check=True)
+ try:
+ # 停服归档包含配置、密钥和日志,仅保存在远程受保护目录。
+ archive = backup / 'smail.tar.gz'
+ subprocess.run(['tar', '-czf', str(archive), '-C', '/opt', 'smail'], check=True)
+ subprocess.run(['tar', '-dzf', str(archive), '-C', '/opt'], check=True)
+ archive.chmod(0o600)
+ for name in DATABASES:
+ source = ROOT / 'data' / name
+ assert source.resolve() == source and not source.is_symlink()
+ snapshot(source, backup / name)
+ for name in expected:
+ shutil.copy2(ROOT / name, backup / name)
+ installed = True
+ for name, checksum in expected.items():
+ subprocess.run(['install', '-m', '0755', '-o', 'root', '-g', 'root',
+ str(STAGING / name), str(ROOT / name)], check=True)
+ assert digest(ROOT / name) == checksum
+ subprocess.run(['systemctl', 'start', 'smail.service'], check=True)
+ wait_ready()
+ for name in DATABASES:
+ with sqlite3.connect(f'file:{ROOT / "data" / name}?mode=ro', uri=True) as database:
+ assert database.execute('PRAGMA integrity_check').fetchone()[0] == 'ok'
+ with sqlite3.connect(f'file:{ROOT / "data/smail.db"}?mode=ro', uri=True) as database:
+ version = database.execute('SELECT MAX(version) FROM schema_migration').fetchone()[0]
+ assert version == 23
+ states = dict(database.execute('SELECT state,COUNT(*) FROM outbound_delivery GROUP BY state'))
+ result = {'backup': str(backup), 'archive_sha256': digest(archive),
+ 'artifacts': expected, 'migration': version, 'queue_states': states,
+ 'service': run('systemctl', 'is-active', 'smail.service'),
+ 'admin_version': run(str(ROOT / 'smail-admin'), 'version'),
+ 'database_integrity': 'ok', 'database_reset': False}
+ (backup / 'result.json').write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
+ print(json.dumps(result, ensure_ascii=False, indent=2))
+ except BaseException:
+ if installed:
+ subprocess.run(['systemctl', 'stop', 'smail.service'], check=True)
+ for name in DATABASES:
+ snapshot(ROOT / 'data' / name, backup / ('failed-' + name))
+ snapshot(backup / name, ROOT / 'data' / name)
+ shutil.chown(ROOT / 'data' / name, user='smail', group='smail')
+ for name in expected:
+ shutil.copy2(backup / name, ROOT / name)
+ subprocess.run(['systemctl', 'start', 'smail.service'], check=True)
+ print('升级失败,备份及故障证据保留于:' + str(backup))
+ raise
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/tools/deploy_run.py b/test/tools/deploy_run.py
index ceba383..4820dc4 100644
--- a/test/tools/deploy_run.py
+++ b/test/tools/deploy_run.py
@@ -1,7 +1,8 @@
import paramiko, time
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
s_in, s_out, s_err = ssh.exec_command("chmod +x /opt/smail/smail_new && pkill -9 -f smail || true")
s_out.channel.recv_exit_status()
time.sleep(1)
diff --git a/test/tools/dump_emails.py b/test/tools/dump_emails.py
index 7b61a1a..632f984 100644
--- a/test/tools/dump_emails.py
+++ b/test/tools/dump_emails.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, subject, send_user_id, send_time FROM email ;"')
print("Emails:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, user_id, email_id, is_read, is_trash FROM user_email ;"')
diff --git a/test/tools/fix_remote_config_digest.py b/test/tools/fix_remote_config_digest.py
index b5381b8..e2e64b8 100644
--- a/test/tools/fix_remote_config_digest.py
+++ b/test/tools/fix_remote_config_digest.py
@@ -2,8 +2,9 @@
import paramiko, time
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
cmd = """python3 -c '
import hashlib, json
diff --git a/test/tools/get_dkim.py b/test/tools/get_dkim.py
index 8f74e1c..8395c41 100644
--- a/test/tools/get_dkim.py
+++ b/test/tools/get_dkim.py
@@ -1,7 +1,8 @@
import paramiko
client = paramiko.SSHClient()
-client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-client.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+client.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(client)
cmd = 'openssl rsa -in /opt/smail/ssl/dkim/dkim.priv -pubout -outform PEM'
stdin, stdout, stderr = client.exec_command(cmd)
lines = [l.strip() for l in stdout.read().decode('utf-8').splitlines() if '---' not in l and l.strip()]
diff --git a/test/tools/inspect_schema.py b/test/tools/inspect_schema.py
index c6b9b4e..dc9d884 100644
--- a/test/tools/inspect_schema.py
+++ b/test/tools/inspect_schema.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
_, stdout, _ = ssh.exec_command('sqlite3 /opt/smail/data/smail.db ".schema client_credential"')
print(stdout.read().decode())
_, stdout, _ = ssh.exec_command('sqlite3 /opt/smail/data/smail.db ".schema protocol_credential"')
diff --git a/test/tools/query_arbitrary.py b/test/tools/query_arbitrary.py
index cb3f8e1..949016d 100644
--- a/test/tools/query_arbitrary.py
+++ b/test/tools/query_arbitrary.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, type, subject, from_address, send_user_id, status FROM email ORDER BY id ASC;"')
print("Security questions:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, count(*), sum(consumed) FROM recovery_code GROUP BY user_id;"')
diff --git a/test/tools/query_emails.py b/test/tools/query_emails.py
index 686202b..4d5f154 100644
--- a/test/tools/query_emails.py
+++ b/test/tools/query_emails.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, user_id, email_id, is_read, group_id, status FROM user_email ORDER BY id DESC LIMIT 10;"')
print("Security questions:\n", stdout.read().decode("utf-8", errors="ignore").encode("utf-8"))
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, type, subject, send_user_id, status FROM email ORDER BY id DESC LIMIT 10;"')
diff --git a/test/tools/query_remote_details.py b/test/tools/query_remote_details.py
index e650a3f..921e566 100644
--- a/test/tools/query_remote_details.py
+++ b/test/tools/query_remote_details.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, question FROM security_question;"')
print("Security questions:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, count(*), sum(consumed) FROM recovery_code GROUP BY user_id;"')
diff --git a/test/tools/query_remote_tables.py b/test/tools/query_remote_tables.py
index 01727eb..df1bc2c 100644
--- a/test/tools/query_remote_tables.py
+++ b/test/tools/query_remote_tables.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db ".schema security_question"')
print("Schema security_question:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db ".schema recovery_code"')
diff --git a/test/tools/query_remote_testuser.py b/test/tools/query_remote_testuser.py
index ffccfb8..1405a7a 100644
--- a/test/tools/query_remote_testuser.py
+++ b/test/tools/query_remote_testuser.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT * FROM security_question WHERE user_id = 2;"')
print("Testuser2 security question:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT * FROM recovery_code WHERE user_id = 2;"')
diff --git a/test/tools/query_remote_users.py b/test/tools/query_remote_users.py
index 878b15e..cdac4f5 100644
--- a/test/tools/query_remote_users.py
+++ b/test/tools/query_remote_users.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, account, name, role, is_admin, disabled FROM user;"')
print("Users in DB:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db ".tables"')
diff --git a/test/tools/remote_review.py b/test/tools/remote_review.py
new file mode 100644
index 0000000..1c03934
--- /dev/null
+++ b/test/tools/remote_review.py
@@ -0,0 +1,45 @@
+import argparse
+import pathlib
+import shlex
+import sys
+
+import paramiko
+
+from remote_ssh import connect_remote
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--command")
+ parser.add_argument("--upload", nargs=2, action="append", default=[])
+ args = parser.parse_args()
+ client = connect_remote(paramiko.SSHClient())
+ try:
+ with client.open_sftp() as sftp:
+ for source, destination in args.upload:
+ target = pathlib.PurePosixPath(destination)
+ if (not target.is_absolute() or ".." in target.parts
+ or len(target.parts) < 5 or target.parts[1:3] != ("var", "tmp")
+ or not target.parts[3].startswith("smail-review-")):
+ raise ValueError("上传路径必须位于项目审查临时目录")
+ command = "install -d -m 0755 " + shlex.quote(str(target.parent))
+ _, output, error = client.exec_command(command)
+ if output.channel.recv_exit_status() != 0:
+ raise RuntimeError(error.read().decode("utf-8", "replace"))
+ sftp.put(source, destination)
+ sftp.chmod(destination, 0o755 if destination.endswith(".test") else 0o644)
+ if args.command:
+ _, output, error = client.exec_command(args.command, timeout=900)
+ for line in output:
+ print(line, end="", flush=True)
+ errors = error.read().decode("utf-8", "replace")
+ if errors:
+ print(errors, file=sys.stderr)
+ return output.channel.recv_exit_status()
+ return 0
+ finally:
+ client.close()
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/test/tools/remote_ssh.py b/test/tools/remote_ssh.py
new file mode 100644
index 0000000..1110f4c
--- /dev/null
+++ b/test/tools/remote_ssh.py
@@ -0,0 +1,28 @@
+import os
+
+import paramiko
+
+
+def connect_remote(client):
+ host = os.environ.get("SMAIL_SSH_HOST", "47.108.78.12")
+ user = os.environ.get("SMAIL_SSH_USER", "root")
+ password = os.environ.get("SMAIL_SSH_PASSWORD") or None
+ key = os.environ.get("SMAIL_SSH_KEY") or None
+ client.load_system_host_keys()
+ known_hosts = os.environ.get("SMAIL_SSH_KNOWN_HOSTS")
+ if known_hosts:
+ client.load_host_keys(known_hosts)
+ client.set_missing_host_key_policy(paramiko.RejectPolicy())
+ client.connect(
+ host,
+ port=int(os.environ.get("SMAIL_SSH_PORT", "22")),
+ username=user,
+ password=password,
+ key_filename=key,
+ look_for_keys=password is None,
+ allow_agent=password is None,
+ timeout=10,
+ auth_timeout=10,
+ banner_timeout=10,
+ )
+ return client
diff --git a/test/tools/review_tests.sh b/test/tools/review_tests.sh
new file mode 100644
index 0000000..77bc780
--- /dev/null
+++ b/test/tools/review_tests.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+cd /var/tmp/smail-review-20260917
+for name in outbox utils-send utils-smtp controllers-email db-migration res_init services-dbmigration; do
+ if runuser -u smail -- "./$name.test" -test.v -test.timeout=180s > "$name.result.log" 2>&1; then
+ printf '%s PASS\n' "$name"
+ tail -n 3 "$name.result.log"
+ else
+ cat "$name.result.log"
+ exit 1
+ fi
+done
diff --git a/test/tools/run_check_ext.py b/test/tools/run_check_ext.py
index 09bc9d3..b9e2f34 100644
--- a/test/tools/run_check_ext.py
+++ b/test/tools/run_check_ext.py
@@ -2,8 +2,9 @@
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
s = '''
import sqlite3
diff --git a/test/tools/run_check_hex.py b/test/tools/run_check_hex.py
index 82a8c75..62e4511 100644
--- a/test/tools/run_check_hex.py
+++ b/test/tools/run_check_hex.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
s = '''
import sqlite3
diff --git a/test/tools/run_check_hex2.py b/test/tools/run_check_hex2.py
index 0e399d4..4df5b0b 100644
--- a/test/tools/run_check_hex2.py
+++ b/test/tools/run_check_hex2.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
s = '''
import sqlite3
diff --git a/test/tools/run_check_hex3.py b/test/tools/run_check_hex3.py
index e17a7e6..484aaba 100644
--- a/test/tools/run_check_hex3.py
+++ b/test/tools/run_check_hex3.py
@@ -2,8 +2,9 @@
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
_, out, err = ssh.exec_command('python3 /tmp/check_hex2.py')
print('OUT:\n', out.read().decode('utf-8', errors='ignore'))
diff --git a/test/tools/run_check_logs.py b/test/tools/run_check_logs.py
index a08cf5e..fe9511f 100644
--- a/test/tools/run_check_logs.py
+++ b/test/tools/run_check_logs.py
@@ -2,8 +2,9 @@
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
s = '''
import sqlite3
diff --git a/test/tools/run_check_names.py b/test/tools/run_check_names.py
index cb522f0..807159b 100644
--- a/test/tools/run_check_names.py
+++ b/test/tools/run_check_names.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
s = '''
import sqlite3
diff --git a/test/tools/run_check_users.py b/test/tools/run_check_users.py
index fe3baef..00285f9 100644
--- a/test/tools/run_check_users.py
+++ b/test/tools/run_check_users.py
@@ -2,8 +2,9 @@
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
s = '''
import sqlite3
diff --git a/test/tools/run_decrypt_creds.py b/test/tools/run_decrypt_creds.py
index 1db71fe..6fe0234 100644
--- a/test/tools/run_decrypt_creds.py
+++ b/test/tools/run_decrypt_creds.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
s = '''
import sqlite3, base64, glob
diff --git a/test/tools/run_visual_setup.py b/test/tools/run_visual_setup.py
index 29bf1a1..b9980b5 100644
--- a/test/tools/run_visual_setup.py
+++ b/test/tools/run_visual_setup.py
@@ -32,11 +32,11 @@ with sync_playwright() as p:
page.screenshot(path=os.path.join(test_log_dir, 'step2_admin_password.png'))
# Step 2: 输入管理员密码
- print('输入管理员密码 10102442Lb!...')
+ print('输入管理员密码 [redacted]...')
pwd_inputs = page.locator('.form input')
print('密码页输入框数量:', pwd_inputs.count())
for i in range(pwd_inputs.count()):
- pwd_inputs.nth(i).fill('10102442Lb!')
+ pwd_inputs.nth(i).fill(os.environ["SMAIL_TEST_ADMIN_PASSWORD"])
time.sleep(1)
page.screenshot(path=os.path.join(test_log_dir, 'step2_password_filled.png'))
diff --git a/test/tools/run_visual_wizard.py b/test/tools/run_visual_wizard.py
index bd787ed..79c189c 100644
--- a/test/tools/run_visual_wizard.py
+++ b/test/tools/run_visual_wizard.py
@@ -38,7 +38,7 @@ with sync_playwright() as p:
inputs = page.locator('.database-form input, .ctn input')
print('找到输入框数量:', inputs.count())
for i in range(inputs.count()):
- inputs.nth(i).fill('10102442Lb!')
+ inputs.nth(i).fill(os.environ["SMAIL_TEST_ADMIN_PASSWORD"])
time.sleep(1)
page.screenshot(path=os.path.join(test_log_dir, 'step2_password_filled.png'))
diff --git a/test/tools/send_test_email.py b/test/tools/send_test_email.py
index 75724bd..328636e 100644
--- a/test/tools/send_test_email.py
+++ b/test/tools/send_test_email.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
s = '''import smtplib, ssl
from email.mime.text import MIMEText
diff --git a/test/tools/test_echo.py b/test/tools/test_echo.py
index 852f693..eb924fd 100644
--- a/test/tools/test_echo.py
+++ b/test/tools/test_echo.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command("echo HELLO")
print(stdout:, stdout.read().decode())
print(stderr:, stderr.read().decode())
diff --git a/test/tools/test_inbound_info.py b/test/tools/test_inbound_info.py
index 281b76b..5265fc6 100644
--- a/test/tools/test_inbound_info.py
+++ b/test/tools/test_inbound_info.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
stdin, stdout, stderr = ssh.exec_command('python3 -c "import sqlite3; conn=sqlite3.connect(\x27/opt/smail/data/smail.db\x27); c=conn.cursor(); print(c.execute(\x27SELECT id, user_id, email_id, is_read, is_trash FROM user_email ORDER BY id DESC LIMIT 5\x27).fetchall())"')
print("User emails top 5:\n", stdout.read().decode())
stdin, stdout, stderr = ssh.exec_command('python3 -c "import sqlite3; conn=sqlite3.connect(\x27/opt/smail/data/smail.db\x27); c=conn.cursor(); print(c.execute(\x27SELECT id, subject, send_time FROM email ORDER BY id DESC LIMIT 5\x27).fetchall())"')
diff --git a/test/tools/test_remote_setup.py b/test/tools/test_remote_setup.py
index e47cecc..3af848a 100644
--- a/test/tools/test_remote_setup.py
+++ b/test/tools/test_remote_setup.py
@@ -1,7 +1,8 @@
import paramiko
client = paramiko.SSHClient()
-client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-client.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=8)
+client.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(client)
client.exec_command('systemctl stop smail-init; pkill -9 smail')
setup_script = '''#!/bin/bash
diff --git a/test/tools/update_remote_email_config.py b/test/tools/update_remote_email_config.py
index 5878b2f..75b177b 100644
--- a/test/tools/update_remote_email_config.py
+++ b/test/tools/update_remote_email_config.py
@@ -1,7 +1,8 @@
import paramiko
ssh = paramiko.SSHClient()
-ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10)
+ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
+from remote_ssh import connect_remote
+connect_remote(ssh)
cmd = """python3 -c '
import json