mail_https_check.py

  • 通过脚本巡检即将到期的https域名证书,并发送邮件到相关人

  • 只需要修改***就可以添加发送人和接收人

  • 巡检内容地址为:/data/https_list,格式为域名+空格备注(liwork.cn 博客网站)

  • 效果示意:

  • 执行命令:

python3 mail_https_check.py

代码内容

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import sys
import subprocess
import smtplib
import ssl
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

# ------------------ 配置参数 ------------------
SMTP_HOST = "***"
SMTP_PORT = ***
SMTP_USER = "***"      # 发件人邮箱
SMTP_PASSWORD = "***"      # 发件人密码
RECIPIENTS = [                     # 收件人列表(重要修改点)
    "***",            # 主收件人
    "***"           # 备用收件人
]
HTTPS_LIST_FILE = "/data/https_list"
OUTPUT_FILE = "/data/https_lists"
# ---------------------------------------------

def get_cert_expiry(domain):
    """获取证书过期时间"""
    try:
        cmd = f"echo | timeout 30 openssl s_client -servername {domain} -connect {domain}:443 2>/dev/null | openssl x509 -noout -enddate"
        result = subprocess.run(
            cmd,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=True
        )
        
        if result.returncode != 0:
            raise Exception(f"OpenSSL错误: {result.stderr.strip()}")
            
        end_time_str = result.stdout.split('=')[1].strip()
        return datetime.strptime(end_time_str, '%b %d %H:%M:%S %Y GMT')
    except Exception as e:
        print(f"[ERROR] {domain} 检测失败: {str(e)}")
        return None

def check_certificates():
    """主检查函数"""
    if not os.path.exists(HTTPS_LIST_FILE):
        print(f"[FATAL] 配置文件 {HTTPS_LIST_FILE} 不存在")
        sys.exit(1)
    
    warning_domains = []
    
    with open(HTTPS_LIST_FILE, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            
            parts = line.split(maxsplit=1)
            domain = parts[0]
            note = parts[1] if len(parts) > 1 else ''
            
            print(f"\n{'='*80}")
            print(f"正在检测: {domain}")
            
            expiry_time = get_cert_expiry(domain)
            if not expiry_time:
                print("连接失败,跳过该域名")
                continue
                
            now = datetime.utcnow()
            days_left = (expiry_time - now).days
            
            print(f"剩余天数: {days_left}")
            
            if days_left < 10:
                print("[WARNING] 证书即将过期")
                with open(OUTPUT_FILE, 'a', encoding='utf-8') as f:
                    f.write(f"{domain}\n")
                
                warning_domains.append({
                    'domain': domain,
                    'expire': expiry_time.strftime('%Y-%m-%d %H:%M:%S'),
                    'note': note
                })
    
    return warning_domains

def send_email(warnings):
    """发送专业告警邮件"""
    if not warnings:
        print("无证书过期预警")
        return
    
    current_date = datetime.now().strftime("%Y-%m-%d")
    
    msg = MIMEMultipart('alternative')
    msg['Subject'] = f'[{current_date}] SSL证书过期预警 - 需人工处理'
    msg['From'] = f"Liwork自动巡检 <{SMTP_USER}>"
    msg['To'] = ", ".join(RECIPIENTS)  # 多个收件人用逗号分隔
    
    # 生成表格内容
    table_rows = []
    for item in warnings:
        table_rows.append(f"""
        <tr style="border-bottom: 1px solid #eee;">
            <td style="padding: 12px 0; color: #2c3e50; width: 40%;">{item['domain']}</td>
            <td style="padding: 12px 0; color: #e74c3c; width: 30%;">{item['expire']}</td>
            <td style="padding: 12px 0; color: #95a5a6; width: 30%;">{item['note']}</td>
        </tr>
        """)
    
    html_content = f"""
<html>
<head>
<meta charset="UTF-8">
<style>
    body {{
        font-family: Helvetica, Arial, sans-serif;
        margin: 0;
        padding: 30px;
        background: #fafafa;
    }}
    .header {{
        background: #2c3e50;
        color: white;
        padding: 25px 30px;
        border-radius: 8px;
        margin-bottom: 25px;
    }}
    .alert-section {{
        background: #ffe5e5;
        color: #c0392b;
        padding: 15px;
        border-radius: 6px;
        margin-bottom: 25px;
        border-left: 4px solid #c0392b;
    }}
    .report-table {{
        width: 100%;
        background: white;
        border-radius: 8px;
        box-shadow: 0 2px 4px rgba(0,0,0,0.05);
        border-collapse: collapse;
        margin-bottom: 30px;
    }}
    .report-table th {{
        background: #f8f9fb;
        padding: 16px 0;
        color: #7f8c8d;
        font-weight: 500;
        text-align: left;
        border-bottom: 2px solid #eee;
    }}
    .report-table td {{
        padding: 14px 0;
    }}
    .footer {{
        color: #95a5a6;
        font-size: 0.9em;
        padding-left: 10px;
        line-height: 1.5;
    }}
</style>
</head>
<body>
    <div class="header">
        <h1 style="margin:0; font-weight:300;">SSL证书巡检报告</h1>
        <h3 style="margin:10px 0 0; font-weight:400;">检测日期:{current_date}</h3>
    </div>

    <div class="alert-section">
        发现 {len(warnings)} 个证书即将过期(剩余天数 &lt; 10)
    </div>

    <table class="report-table">
        <thead>
            <tr>
                <th style="padding-left: 30px;">域名</th>
                <th>到期时间</th>
                <th style="padding-right: 30px;">备注信息</th>
            </tr>
        </thead>
        <tbody>
            {''.join(table_rows)}
        </tbody>
    </table>

    <div class="footer">
        <p>技术支持:zhouli@liwork.cn</p>
        <p style="margin-top:8px;">此邮件由系统自动生成,请勿直接回复</p>
    </div>
</body>
</html>
    """
    
    msg.attach(MIMEText(html_content, 'html', 'utf-8'))
    
    try:
        context = ssl.create_default_context()
        with smtplib.SMTP_SSL(
            host=SMTP_HOST,
            port=SMTP_PORT,
            context=context,
            timeout=30
        ) as server:
            server.login(SMTP_USER, SMTP_PASSWORD)
            # 发送给所有收件人(重要修改点)
            server.sendmail(SMTP_USER, RECIPIENTS, msg.as_string())
        print("告警邮件已发送至:", RECIPIENTS)
    except Exception as e:
        print(f"[ERROR] 邮件发送失败: {str(e)}")

if __name__ == "__main__":
    # 初始化输出文件
    with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
        f.write("")
    
    print("=== SSL证书巡检开始 ===")
    warnings = check_certificates()
    
    if warnings:
        print(f"\n发现 {len(warnings)} 个证书即将过期")
        send_email(warnings)
    else:
        print("\n所有证书状态正常")
    
    print("=== 检查完成 ===")