吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 174|回复: 12
上一主题 下一主题
收起左侧

[Python 转载] 公网IP变更邮箱提醒工具

[复制链接]
跳转到指定楼层
楼主
再贱就再见 发表于 2026-8-20 19:32 回帖奖励
本帖最后由 再贱就再见 于 2026-8-21 01:26 编辑

找人写要450元,AI手搓了个公网IP变更邮箱提醒,设置完要点保存配置,可以最小化到托盘,软件同目录要有app.ico图标,才能最小化到系统托盘.

https://wwbmj.lanzouu.com/iYhTX43peyyf


用的过程中出现了些问题,现在更新到最新版了,应该不会出啥问题了...






[Python] 纯文本查看 复制代码
import requests
import smtplib
import time
import logging
import json
import os
import threading
import queue
import sys
import ctypes
from email.mime.text import MIMEText
from email.header import Header
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import winreg
import ipaddress

try:
    from pystray import Icon, MenuItem, Menu
    from PIL import Image, ImageDraw
    HAS_TRAY = True
except ModuleNotFoundError:
    HAS_TRAY = False
    print("警告:未安装pystray pillow,托盘功能不可用,请执行 pip install requests pystray pillow")

# Windows互斥锁
MUTEX_NAME = "IPMonitor_Tool_20260820_UniqueMutex"
h_mutex = None
if sys.platform == "win32":
    kernel32 = ctypes.windll.kernel32
    h_mutex = kernel32.CreateMutexW(None, False, MUTEX_NAME)
    last_err = kernel32.GetLastError()
    if last_err == 183:
        messagebox.showinfo("提示", "软件已经在运行中,请勿重复启动!")
        sys.exit(0)

CONFIG_FILE = "config.json"
IPV4_SAVE_FILE = "last_ipv4.txt"
IPV6_SAVE_FILE = "last_ipv6.txt"
LOG_FILE = "ip_log.txt"


def get_app_dir():
    """获取exe/脚本所在目录(同目录)"""
    if getattr(sys, 'frozen', False):
        return os.path.dirname(sys.executable)
    else:
        return os.path.dirname(os.path.abspath(sys.argv[0]))


def get_exe_fullpath():
    if getattr(sys, 'frozen', False):
        return sys.executable
    else:
        return os.path.abspath(sys.argv[0])


# 日志
logger = logging.getLogger("IPMonitor")
logger.setLevel(logging.INFO)
if not logger.handlers:
    log_path = os.path.join(get_app_dir(), LOG_FILE)
    fh = logging.FileHandler(log_path, encoding="utf-8")
    fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
    fh.setFormatter(fmt)
    logger.addHandler(fh)


def make_fallback_icon():
    """生成兜底托盘图标,内存绘制,不需要外部文件"""
    img = Image.new("RGB", (64, 64), color=(20, 90, 180))
    draw = ImageDraw.Draw(img)
    draw.ellipse((12, 12, 52, 52), fill=(255, 255, 255))
    return img


def is_valid_ipv4(ip_str: str) -> bool:
    try:
        ipaddress.IPv4Address(ip_str.strip())
        return True
    except (ipaddress.AddressValueError, ValueError):
        return False


def is_valid_ipv6(ip_str: str) -> bool:
    try:
        ipaddress.IPv6Address(ip_str.strip())
        return True
    except (ipaddress.AddressValueError, ValueError):
        return False


class IPMonitorGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("公网IP变更邮件提醒工具")
        self.root.geometry("720x620")
        self.running = False
        self.monitor_thread = None
        self.tray_icon = None
        self.tray_thread = None
        self.autostart_key_name = "IPMonitorTool"
        self.log_queue = queue.Queue()
        self.http_session = requests.Session()
        self.http_session.trust_env = False

        # 待发送告警缓存:保存未发送成功的IP变更邮件任务
        self.pending_alert = None  # {"subject":"xxx","body":"xxx"}

        self.app_dir = get_app_dir()
        self.CONFIG_FILE = os.path.join(self.app_dir, CONFIG_FILE)
        self.IPV4_SAVE_FILE = os.path.join(self.app_dir, IPV4_SAVE_FILE)
        self.IPV6_SAVE_FILE = os.path.join(self.app_dir, IPV6_SAVE_FILE)
        self.LOG_FILE = os.path.join(self.app_dir, LOG_FILE)

        self._poll_log_queue()

        frame_cfg = ttk.LabelFrame(root, text="邮箱配置")
        frame_cfg.pack(fill="x", padx=10, pady=5)
        ttk.Label(frame_cfg, text="SMTP服务器:").grid(row=0, column=0, padx=5, pady=3, sticky="w")
        self.var_smtp = tk.StringVar(value="smtp.qq.com")
        ttk.Entry(frame_cfg, textvariable=self.var_smtp, width=32).grid(row=0, column=1, padx=5, pady=3)
        ttk.Label(frame_cfg, text="端口:").grid(row=0, column=2, padx=5, pady=3, sticky="w")
        self.var_port = tk.StringVar(value="465")
        ttk.Entry(frame_cfg, textvariable=self.var_port, width=10).grid(row=0, column=3, padx=5, pady=3)

        ttk.Label(frame_cfg, text="发件邮箱:").grid(row=1, column=0, padx=5, pady=3, sticky="w")
        self.var_sender = tk.StringVar()
        ttk.Entry(frame_cfg, textvariable=self.var_sender, width=32).grid(row=1, column=1, padx=5, pady=3)
        ttk.Label(frame_cfg, text="授权码:").grid(row=1, column=2, padx=5, pady=3, sticky="w")
        self.var_pwd = tk.StringVar()
        ttk.Entry(frame_cfg, textvariable=self.var_pwd, width=18, show="*").grid(row=1, column=3, padx=5, pady=3)

        ttk.Label(frame_cfg, text="接收邮箱:").grid(row=2, column=0, padx=5, pady=3, sticky="w")
        self.var_receiver = tk.StringVar()
        ttk.Entry(frame_cfg, textvariable=self.var_receiver, width=32).grid(row=2, column=1, padx=5, pady=3)
        ttk.Label(frame_cfg, text="检测间隔(秒):").grid(row=2, column=2, padx=5, pady=3, sticky="w")
        self.var_interval = tk.StringVar(value="300")
        ttk.Entry(frame_cfg, textvariable=self.var_interval, width=10).grid(row=2, column=3, padx=5, pady=3)

        frame_adv = ttk.LabelFrame(root, text="高级设置")
        frame_adv.pack(fill="x", padx=10, pady=5)

        ttk.Label(frame_adv, text="日志保留天数:").grid(row=0, column=0, padx=5, pady=3, sticky="w")
        self.var_log_days = tk.StringVar(value="30")
        ttk.Entry(frame_adv, textvariable=self.var_log_days, width=3).grid(row=0, column=1, padx=5, pady=3)
        ttk.Label(frame_adv, text="(到期自动清理旧日志)").grid(row=0, column=2, padx=2, pady=3, sticky="w")

        self.var_monitor_ipv6 = tk.BooleanVar(value=False)
        ttk.Checkbutton(frame_adv, text="同时监控IPv6", variable=self.var_monitor_ipv6).grid(row=0, column=3, padx=8, pady=3)

        frame_boot = ttk.Frame(frame_adv)
        frame_boot.grid(row=1, column=0, columnspan=5, padx=5, pady=5)

        self.btn_clean_log = ttk.Button(frame_boot, text="手动清理日志", command=self.manual_clean_log)
        self.btn_clean_log.grid(row=0, column=0, padx=3)
        self.btn_toggle_monitor = ttk.Button(frame_boot, text="开始监控", command=self.toggle_monitor)
        self.btn_toggle_monitor.grid(row=0, column=1, padx=3)
        self.btn_autostart = ttk.Button(frame_boot, text="开启开机自启", command=self.toggle_auto_start)
        self.btn_autostart.grid(row=0, column=2, padx=3)
        self.btn_save = ttk.Button(frame_boot, text="保存配置", command=self.save_config)
        self.btn_save.grid(row=0, column=3, padx=3)

        frame_log = ttk.LabelFrame(root, text="运行日志")
        frame_log.pack(fill="both", expand=True, padx=10, pady=5)
        self.log_text = scrolledtext.ScrolledText(frame_log, height=14)
        self.log_text.pack(fill="both", expand=True, padx=5, pady=5)

        self.update_autostart_btn_text()
        self.update_monitor_button_text()
        self.init_tray()
        self.root.protocol("WM_DELETE_WINDOW", self.on_window_close)
        self.load_config()

        cfg = self.read_raw_config()
        if cfg.get("monitor_running", False):
            self.log("检测到上次监控为开启状态,自动启动监控")
            self.root.after(300, self.start_monitor_inner)

    def update_monitor_button_text(self):
        if self.running:
            self.btn_toggle_monitor.config(text="停止监控")
        else:
            self.btn_toggle_monitor.config(text="开始监控")

    def read_raw_config(self):
        if os.path.exists(self.CONFIG_FILE):
            try:
                with open(self.CONFIG_FILE, "r", encoding="utf-8") as f:
                    return json.load(f)
            except Exception:
                return {}
        return {}

    def is_autostart_enabled(self):
        try:
            key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_READ)
            val, _ = winreg.QueryValueEx(key, self.autostart_key_name)
            winreg.CloseKey(key)
            return True
        except FileNotFoundError:
            return False

    def update_autostart_btn_text(self):
        if self.is_autostart_enabled():
            self.btn_autostart.config(text="关闭开机自启")
        else:
            self.btn_autostart.config(text="开启开机自启")

    def toggle_auto_start(self):
        exe_full = get_exe_fullpath()
        exe_dir = get_app_dir()
        is_exe = getattr(sys, 'frozen', False)

        if not os.path.exists(exe_full):
            messagebox.showerror("错误", "程序路径不存在,无法设置自启!")
            return

        try:
            if self.is_autostart_enabled():
                key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_WRITE)
                winreg.DeleteValue(key, self.autostart_key_name)
                winreg.CloseKey(key)
                messagebox.showinfo("提示", "已关闭开机自启")
                self.log("✅已关闭Windows开机自启")
            else:
                if not is_exe:
                    messagebox.showwarning("警告", "当前直接运行py源码,开机自启不会正常工作!\n请打包为exe程序后再开启自启。")
                cmd_str = f'cmd /c "cd /d "{exe_dir}" && start "" "{exe_full}" "'
                key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_WRITE)
                winreg.SetValueEx(key, self.autostart_key_name, 0, winreg.REG_SZ, cmd_str)
                winreg.CloseKey(key)
                messagebox.showinfo("提示", f"已开启开机自启")
                self.log(f"✅已开启Windows开机自启,exe路径:{exe_full}")
            self.update_autostart_btn_text()
        except Exception as e:
            messagebox.showerror("错误", f"操作开机自启失败:{e}")
            self.log(f"开机自启操作异常:{e}")

    def toggle_monitor(self):
        if self.running:
            self.stop_monitor_inner()
        else:
            self.start_monitor_inner()

    def start_monitor_inner(self):
        if self.running:
            messagebox.showwarning("提示", "监控已经在运行!")
            return
        try:
            port = int(self.var_port.get())
            interval = int(self.var_interval.get())
            log_days = int(self.var_log_days.get())
            if interval <= 0 or log_days <= 0:
                raise ValueError
        except Exception:
            messagebox.showerror("错误", "端口、检测间隔、日志保留天数必须填写正整数!")
            return
        self.running = True
        self.save_config(monitor_running=True)
        self.monitor_thread = threading.Thread(target=self.monitor_loop, daemon=True)
        self.monitor_thread.start()
        self.update_monitor_button_text()

    def stop_monitor_inner(self):
        self.running = False
        self.pending_alert = None
        self.save_config(monitor_running=False)
        self.update_monitor_button_text()

    def init_tray(self):
        if not HAS_TRAY:
            self.log("pystray未安装,托盘功能禁用")
            return
        icon_img = None
        local_ico = os.path.join(self.app_dir, "app.ico")
        try:
            if os.path.exists(local_ico):
                icon_img = Image.open(local_ico)
                self.log(f"&#9989;成功加载【程序同目录】app.ico托盘图标: {local_ico}")
            else:
                self.log(f"&#9888;&#65039;同目录未找到app.ico({local_ico}),使用程序内置兜底托盘图标")
                icon_img = make_fallback_icon()
        except Exception as e:
            self.log(f"&#9888;&#65039;加载app.ico失败,切换内置兜底图标:{e}")
            icon_img = make_fallback_icon()

        menu = Menu(
            MenuItem("显示主窗口", self.show_window, default=True),
            MenuItem("彻底退出", self.quit_app)
        )
        try:
            self.tray_icon = Icon("IPMonitor", icon_img, "IP监控工具", menu)
            self.tray_thread = threading.Thread(target=self.tray_icon.run, daemon=True)
            self.tray_thread.start()
            self.log("&#9989;系统托盘初始化完成")
        except Exception as e:
            self.log(f"&#10060;托盘启动失败:{e}")

    def on_window_close(self):
        if HAS_TRAY and self.tray_icon:
            self.root.withdraw()
        else:
            self.quit_app()

    def show_window(self, icon=None, item=None):
        self.root.after(0, lambda: (self.root.deiconify(), self.root.lift()))

    def quit_app(self):
        self.log("正在执行程序退出...")
        self.running = False
        self.pending_alert = None
        time.sleep(0.2)
        try:
            self.http_session.close()
        except Exception:
            pass
        if HAS_TRAY and self.tray_icon:
            try:
                self.tray_icon.stop()
            except Exception:
                pass
        try:
            self.root.destroy()
        except Exception:
            pass
        global h_mutex
        if h_mutex is not None:
            try:
                ctypes.windll.kernel32.ReleaseMutex(h_mutex)
                ctypes.windll.kernel32.CloseHandle(h_mutex)
            except Exception:
                pass
        sys.exit(0)

    def _poll_log_queue(self):
        while not self.log_queue.empty():
            msg = self.log_queue.get()
            self.log_text.insert(tk.END, f"{time.strftime('%Y-%m-%d %H:%M:%S')} {msg}\n")
            self.log_text.see(tk.END)
        self.root.after(100, self._poll_log_queue)

    def log(self, msg):
        self.log_queue.put(msg)
        logger.info(msg)

    def auto_clean_log(self, keep_days: int, max_size_mb=20):
        if keep_days <= 0:
            return
        if not os.path.exists(self.LOG_FILE):
            return
        try:
            fsize = os.path.getsize(self.LOG_FILE)
            max_byte = max_size_mb * 1024 * 1024
            if fsize > max_byte:
                self.log(f"&#9888;&#65039;日志文件超过{max_size_mb}MB,执行截断")
                with open(self.LOG_FILE, "r", encoding="utf-8") as f:
                    lines = f.readlines()
                new_lines = lines[len(lines) // 2:]
                with open(self.LOG_FILE, "w", encoding="utf-8") as f:
                    f.writelines(new_lines)

            now_ts = time.time()
            keep_sec = keep_days * 86400
            new_lines = []
            with open(self.LOG_FILE, "r", encoding="utf-8") as f:
                lines = f.readlines()
            for line in lines:
                keep_line = True
                try:
                    time_str = line.split(" - ", 1)[0]
                    log_time = time.strptime(time_str, "%Y-%m-%d %H:%M:%S")
                    log_ts = time.mktime(log_time)
                    if now_ts - log_ts > keep_sec:
                        keep_line = False
                except Exception:
                    pass
                if keep_line:
                    new_lines.append(line)
            with open(self.LOG_FILE, "w", encoding="utf-8") as f:
                f.writelines(new_lines)
            return True
        except Exception as e:
            self.log(f"&#9888;&#65039;日志清理异常:{e}")
            return False

    def manual_clean_log(self):
        try:
            days = int(self.var_log_days.get().strip())
            if days <= 0:
                messagebox.showerror("错误", "日志保留天数必须大于0")
                return
            self.log_text.delete(1.0, tk.END)
            ok = self.auto_clean_log(days)
            if ok:
                messagebox.showinfo("完成", f"已清理,保留最近 {days} 天日志")
                self.log(f"&#9989;手动执行日志清理,保留{days}天")
        except Exception as e:
            messagebox.showerror("错误", f"清理失败:{e}")

    def load_config(self):
        if os.path.exists(self.CONFIG_FILE):
            try:
                with open(self.CONFIG_FILE, "r", encoding="utf-8") as f:
                    cfg = json.load(f)
                self.var_smtp.set(cfg.get("smtp_server", "smtp.qq.com"))
                self.var_port.set(cfg.get("smtp_port", "465"))
                self.var_sender.set(cfg.get("sender_email", ""))
                self.var_pwd.set(cfg.get("sender_pwd", ""))
                self.var_receiver.set(cfg.get("receiver_email", ""))
                self.var_interval.set(cfg.get("check_interval", "300"))
                self.var_log_days.set(cfg.get("log_keep_days", "30"))
                self.var_monitor_ipv6.set(cfg.get("monitor_ipv6", False))
                self.log("已读取本地保存的配置")
            except Exception as e:
                self.log(f"读取配置失败: {e}")

    def save_config(self, monitor_running=None):
        try:
            port = int(self.var_port.get().strip())
            interval = int(self.var_interval.get().strip())
            log_days = int(self.var_log_days.get().strip())
            if interval <= 0 or log_days <= 0:
                raise ValueError("数值必须大于0")
            old_cfg = self.read_raw_config()
            cfg = {
                "smtp_server": self.var_smtp.get().strip(),
                "smtp_port": port,
                "sender_email": self.var_sender.get().strip(),
                "sender_pwd": self.var_pwd.get().strip(),
                "receiver_email": self.var_receiver.get().strip(),
                "check_interval": interval,
                "log_keep_days": log_days,
                "monitor_ipv6": self.var_monitor_ipv6.get(),
                "monitor_running": monitor_running if monitor_running is not None else old_cfg.get("monitor_running", False)
            }
            with open(self.CONFIG_FILE, "w", encoding="utf-8") as f:
                json.dump(cfg, f, ensure_ascii=False, indent=2)
            self.log("&#9989; 配置保存成功!")
        except Exception as e:
            self.log(f"&#10060; 保存配置失败: {e}")
            messagebox.showerror("错误", f"保存失败:{e}")

    def get_ipv4(self):
        api_list = [
            "https://api.ipify.org",
            "https://ip.3322.net",
            "https://ifconfig.me/ip"
        ]
        for api in api_list:
            try:
                resp = self.http_session.get(api, timeout=8)
                ip = resp.text.strip()
                if ip and is_valid_ipv4(ip):
                    return ip
            except Exception:
                continue
        return None

    def get_ipv6(self):
        api_list = [
            "https://ipv6.icanhazip.com",
            "https://ifconfig.me/ipv6"
        ]
        for api in api_list:
            try:
                resp = self.http_session.get(api, timeout=8)
                ip = resp.text.strip()
                if ip and is_valid_ipv6(ip):
                    return ip
            except Exception:
                continue
        return None

    def read_saved_ip(self, filepath):
        try:
            with open(filepath, "r", encoding="utf-8") as f:
                return f.read().strip()
        except (FileNotFoundError, PermissionError, OSError):
            return None

    def save_saved_ip(self, filepath, ip):
        try:
            with open(filepath, "w", encoding="utf-8") as f:
                f.write(ip)
        except (PermissionError, OSError) as e:
            self.log(f"&#9888;&#65039;保存IP文件失败 {filepath} : {e}")

    def send_mail(self, subject, content, cfg):
        msg = MIMEText(content, "plain", "utf-8")
        msg["Subject"] = Header(subject, "utf-8")
        msg["From"] = cfg["sender_email"]
        msg["To"] = cfg["receiver_email"]
        try:
            with smtplib.SMTP_SSL(cfg["smtp_server"], cfg["smtp_port"], timeout=10) as server:
                server.login(cfg["sender_email"], cfg["sender_pwd"])
                server.sendmail(cfg["sender_email"], cfg["receiver_email"], msg.as_string())
            return True
        except Exception as e:
            self.log(f"&#10060;邮件发送失败:{e}")
            return False

    def monitor_loop(self):
        cfg = {
            "smtp_server": self.var_smtp.get().strip(),
            "smtp_port": int(self.var_port.get().strip()),
            "sender_email": self.var_sender.get().strip(),
            "sender_pwd": self.var_pwd.get().strip(),
            "receiver_email": self.var_receiver.get().strip(),
            "check_interval": int(self.var_interval.get().strip()),
            "log_keep_days": int(self.var_log_days.get().strip()),
            "monitor_ipv6": self.var_monitor_ipv6.get()
        }
        if not cfg["sender_email"] or not cfg["sender_pwd"] or not cfg["receiver_email"]:
            self.log("&#10060; 邮箱配置不全,请填写完整邮箱、授权码!")
            self.running = False
            self.root.after(0, self.update_monitor_button_text)
            return

        self.log("==== IP监控已启动 ====")
        self.auto_clean_log(cfg["log_keep_days"])
        self.pending_alert = None

        ipv4_now = self.get_ipv4()
        ipv6_now = self.get_ipv6() if cfg["monitor_ipv6"] else None

        start_content = "IP监控程序已成功启动!\n"
        if ipv4_now:
            start_content += f"当前IPv4:{ipv4_now}\n"
            old4 = self.read_saved_ip(self.IPV4_SAVE_FILE)
            if old4 is None:
                self.save_saved_ip(self.IPV4_SAVE_FILE, ipv4_now)
                self.log("首次运行记录IPv4")
        else:
            self.log("&#9888;&#65039;启动获取IPv4失败")
        if cfg["monitor_ipv6"]:
            if ipv6_now:
                start_content += f"当前IPv6:{ipv6_now}\n"
                old6 = self.read_saved_ip(self.IPV6_SAVE_FILE)
                if old6 is None:
                    self.save_saved_ip(self.IPV6_SAVE_FILE, ipv6_now)
                    self.log("首次运行记录IPv6")
            else:
                self.log("&#9888;&#65039;启动获取IPv6失败(无IPv6网络属于正常)")

        start_content += "后续IP发生变动将会推送邮件通知"
        def async_send_start_mail():
            ok = self.send_mail("【IP监控启动通知】", start_content, cfg)
            if ok:
                self.log("&#9989;启动测试邮件发送成功,请查收邮箱!")
            else:
                self.log("&#9888;&#65039;启动通知邮件发送失败,不再重试")
        threading.Thread(target=async_send_start_mail, daemon=True).start()

        ipv4_fail_cnt = 0
        ipv4_max_fail_log = 5
        ipv6_fail_cnt = 0
        ipv6_max_fail_log = 5

        while self.running:
            # 优先重试上一轮遗留的告警任务
            if self.pending_alert is not None:
                job = self.pending_alert
                self.log(f"&#9203;重试未发送告警邮件 [{job['subject']}]")
                send_ok = self.send_mail(job["subject"], job["body"], cfg)
                if send_ok:
                    self.log(f"&#9989;告警邮件重试发送成功,清除待发送任务")
                    self.pending_alert = None
                else:
                    self.log(f"&#9888;&#65039;告警邮件重试依旧失败,等待下一轮检测间隔")

            ipv4_now = self.get_ipv4()
            ipv6_now = self.get_ipv6() if cfg["monitor_ipv6"] else None

            if ipv4_now:
                ipv4_fail_cnt = 0
                old4 = self.read_saved_ip(self.IPV4_SAVE_FILE)
                self.log(f"IPv4: {ipv4_now} | 上次:{old4}")
                if old4 is not None and ipv4_now != old4:
                    self.log("&#9888;&#65039;IPv4发生变更!立即尝试发送通知邮件")
                    subj = "【公网IPv4变更提醒】"
                    body = f"检测到公网IPv4变更\n旧IPv4:{old4}\n新IPv4:{ipv4_now}"
                    # IP变更立刻发送一次
                    send_ok = self.send_mail(subj, body, cfg)
                    if send_ok:
                        self.log("&#9989;IP变更邮件发送成功")
                        self.pending_alert = None
                    else:
                        self.log("&#9888;&#65039;IP变更即时发送失败,加入待发送队列,后续周期自动重试")
                        self.pending_alert = {"subject": subj, "body": body}
                    self.save_saved_ip(self.IPV4_SAVE_FILE, ipv4_now)

            else:
                ipv4_fail_cnt += 1
                if ipv4_fail_cnt % ipv4_max_fail_log == 0:
                    self.log(f"&#9888;&#65039;连续{ipv4_fail_cnt}次获取IPv4失败")

            if cfg["monitor_ipv6"]:
                if ipv6_now:
                    ipv6_fail_cnt = 0
                    old6 = self.read_saved_ip(self.IPV6_SAVE_FILE)
                    self.log(f"IPv6: {ipv6_now} | 上次:{old6}")
                    if old6 is not None and ipv6_now != old6:
                        self.log("&#9888;&#65039;IPv6发生变更!立即尝试发送通知邮件")
                        subj = "【公网IPv6变更提醒】"
                        body = f"检测到公网IPv6变更\n旧IPv6:{old6}\n新IPv6:{ipv6_now}"
                        send_ok = self.send_mail(subj, body, cfg)
                        if send_ok:
                            self.log("&#9989;IP变更邮件发送成功")
                            self.pending_alert = None
                        else:
                            self.log("&#9888;&#65039;IP变更即时发送失败,加入待发送队列,后续周期自动重试")
                            self.pending_alert = {"subject": subj, "body": body}
                        self.save_saved_ip(self.IPV6_SAVE_FILE, ipv6_now)
                else:
                    ipv6_fail_cnt += 1
                    if ipv6_fail_cnt % ipv6_max_fail_log == 0:
                        self.log(f"&#9888;&#65039;连续{ipv6_fail_cnt}次获取IPv6失败(无IPv6网络可忽略)")

            sleep_cnt = 0
            total_sleep = cfg["check_interval"]
            while sleep_cnt < total_sleep and self.running:
                time.sleep(1)
                sleep_cnt += 1

        self.log("==== IP监控已停止 ====")
        self.root.after(0, self.update_monitor_button_text)


if __name__ == "__main__":
    root = tk.Tk()
    local_ico = os.path.join(get_app_dir(), "app.ico")
    try:
        if os.path.exists(local_ico):
            root.iconbitmap(local_ico)
    except Exception:
        pass
    app = IPMonitorGUI(root)
    try:
        root.mainloop()
    except KeyboardInterrupt:
        app.quit_app()

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
 楼主| 再贱就再见 发表于 2026-8-21 00:43 |楼主
galefeng 发表于 2026-8-21 00:32
没搞懂这个发件邮箱和接收邮箱,授权码咋填,QQ邮箱是否需要设置什么,开会员吗,有设置教程吗,有点懵,虽 ...

POP3/IMAP/SMTP/Exchange/CardDAV 服务(已开启)
你可以在 授权码管理 页面管理已生成的授权码。查看 配置方法

邮箱设置里的授权码啊

发送跟接收填你自己的就可以了,或者要发送给谁写谁的,微信有收邮件的话,微信也会提醒.
推荐
richzhl 发表于 2026-8-21 00:09
加多一个功能上去就更好!
检测到IP变更后可以通过PC版微信发送到指定群或联系人上就非常好!

也不局限PC微信也可以支持一下企业微信/钉钉之类的  这样大大方便使用动态公网IP的用户
沙发
fyufx 发表于 2026-8-20 22:31
3#
fllow 发表于 2026-8-20 23:11
我下午4点多刚写了个类似功能的脚本,晚上就刷到你这个了,哈哈哈哈。不过我的是linux的
4#
cool98eng 发表于 2026-8-20 23:13
这个有什么用途?
5#
 楼主| 再贱就再见 发表于 2026-8-20 23:34 |楼主
fyufx 发表于 2026-8-20 22:31
感谢,这授权码是多少来着

邮箱的授权码啊,邮箱里有.设置里找下
6#
 楼主| 再贱就再见 发表于 2026-8-20 23:36 |楼主
cool98eng 发表于 2026-8-20 23:13
这个有什么用途?

又懒,又土获取公网IP,就可以看电脑上电影了.
8#
 楼主| 再贱就再见 发表于 2026-8-21 00:27 |楼主
richzhl 发表于 2026-8-21 00:09
加多一个功能上去就更好!
检测到IP变更后可以通过PC版微信发送到指定群或联系人上就非常好!

这个是最懒的方法,微信的话,还要指定微信版本,更麻烦,企业微信跟钉钉加个功能就可以,还要自己去弄机器人,就没加上这个企业微信跟钉钉了.
9#
galefeng 发表于 2026-8-21 00:32
没搞懂这个发件邮箱和接收邮箱,授权码咋填,QQ邮箱是否需要设置什么,开会员吗,有设置教程吗,有点懵,虽然这个也就玩玩
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-21 01:51

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表