import imaplib
import sys
import re
import email
import time
import os
from datetime import datetime
from email import message_from_bytes
from email.header import decode_header, make_header

# ==================== 設定項目 ====================
IMAP_SERVER = "mail.university.ac.jp"   # 大学のIMAPサーバー
IMAP_PORT = 993                         # 通常はSSLの993
USER = "your_username"
PASSWORD = "your_password"

# 最初は True(削除しない)のまま数日間運用し、誤検知がないかログを確認してください。
# 安全が確認できたら False に変更すると、実際にサーバーからスパムを自動削除します。
DRY_RUN = True

# --- 非スパム新着メール通知設定 ---
# mail-notification-receiver.py とセットで機能します。
# <https://chronoflyer.ddo.jp/~trinary/misc/mail-notification-receiver.py.html>
ENABLE_NOTIFICATION = True             # Trueで通知有効、Falseで無効
FIFO_PATH = "/var/log/mail-notify.fifo" # 通知連携用の一時ファイル

# 1. Fromヘッダー内の偽装用制御文字を含むメールを削除します。
# 例)差出人 【ス ビーサ送配 n ozamA】
# 2. 件名や送信元のブラックリスト(正規表現パターン)
# ※リスト内で r"..." を使用しているため、re.search にそのまま投入可能です。
SPAM_SUBJECTS = [
    r"未承諾広告"
]
SPAM_ENVELOPE_FROM = [
    r"<news@cyagenbiomail\.com>",
    r"<akrso-.*@",
    r"@car-marked\.homes>",
    r"@night-flower\.pics>",
    r"<JP-[A-Za-z]+-[a-z]+.*=.*@[a-z]{4}\.[a-z]+\.[a-z]{3}",
    r"<noreply-.*=.*@tiw[0-9]{2}\.[a-z-]+\.cfd>"
]
SPAM_HEADER_FROM = [
    r"<news@cyagenbiomail\.com>",
    r"<akrso@",
    r"@car-marked\.homes>",
    r"@night-flower\.pics>",
    r"<JP-[A-Za-z]+-[a-z]+@[a-z]{4}\.[a-z]+\.[a-z]{3}",
    r"<noreply@tiw[0-9]{2}\.[a-z-]+\.cfd>",
    r"^サイヤジェン株式会社 <",
    r"】 <"
]
# 3. 一番下のReceivedヘッダーのブラックリスト(正規表現パターン)
SPAM_RECEIVED_KEYWORDS = [
    r"cp\.nov-stells\.pt",
    r"sendera[0-9]{2}\.zcsend\.net",
    r"mail\.viste-ram\.ru",
    r"tiw[0-9]{2}\.[a-z-]+\.cfd"
]
# ==================================================
#   # インストール手順
#   $ chmod +x /PATH/TO/YOUR/spam-filter-daemon.py
#   $ sudo touch /var/log/spam-filter.log
#   $ sudo vi /etc/logrotate.d/spam-filter
#   /var/log/spam-filter.log {
#       weekly
#       rotate 4
#       compress
#       delaycompress
#       missingok
#       notifempty
#       copytruncate
#       su root syslog
#       # send email for the weekly log to root
#       postrotate
#           grep "\[DELETED\]" /var/log/spam-filter.log.1
#       endscript
#   }
#
#   # logrotate のテスト
#   $ sudo logrotate -d /etc/logrotate.d/spam-filter
#
#   $ sudo vi /etc/systemd/system/spam-filter.service
#   [Unit]
#   Description=IMAP IDLE Spam Filter Daemon
#   After=network.target
#
#   [Service]
#   Type=simple
#   ExecStart=/usr/bin/python3 /PATH/TO/YOUR/spam-filter-daemon.py
#   Restart=always
#   User=root
#   Environment=PYTHONUNBUFFERED=1
#   StandardOutput=append:/var/log/spam-filter.log
#   StandardError=append:/var/log/spam-filter.log
#
#   [Install]
#   WantedBy=multi-user.target
#
#
#   # 設定を反映
#   $ sudo systemctl daemon-reload
#   # サーバー起動時に自動実行されるようにする
#   $ sudo systemctl enable spam-filter.service
#   # 今すぐ起動する
#   $ sudo systemctl start spam-filter.service
# ==================================================

def get_timestamp():
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

def decode_mime_words(s):
    if not s: return ""
    try:
        return str(make_header(decode_header(str(s))))
    except Exception:
        return str(s)

def send_desktop_notification(subject, header_from):
    """新着メールの情報を一時ファイルに書き込んで、ユーザー権限の通知スクリプトへ合図を送る"""
    if not ENABLE_NOTIFICATION:
        return
        
    try:
        # タブ区切りの1行テキストとして情報を末尾に追記
        with open(FIFO_PATH, "a", encoding="utf-8") as f:
            f.write(f"{subject}\t{header_from}\n")
    except Exception as e:
        print(f"[{get_timestamp()}] [WARNING] 通知ファイルへの書き込みに失敗: {e}")

def check_spam(msg):
    subject = decode_mime_words(msg.get("Subject", ""))
    envelope_from = msg.get("Return-Path", "")
    header_from = decode_mime_words(msg.get("From", ""))

    # 1. Fromヘッダー内の偽装用制御文字トラップ
    hidden_chars_pattern = r"[\u200b\u200c\u200d\u202a\u202b\u202c\u202d\u202e\ufeff]"
    if re.search(hidden_chars_pattern, header_from):
        return True, "Fromヘッダー内に偽装用の非表示制御文字(ZWJ/RLO等)を検出"

    # 2. ブラックリストチェック
    for k in SPAM_SUBJECTS:
        if re.search(k, subject):
            return True, f"件名ブラックリストマッチ: {k}"
    for l in SPAM_ENVELOPE_FROM:
        if re.search(l, envelope_from):
            return True, f"Return-Pathブラックリストマッチ: {l}"
    for m in SPAM_HEADER_FROM:
        if re.search(m, header_from):
            return True, f"差出人ブラックリストマッチ: {m}"

    # 3. 一番下の Received ヘッダーのブラックリスト照合
    received_headers = msg.get_all('Received', [])
    if received_headers:
        oldest_received_str = str(received_headers[-1])
        for keyword in SPAM_RECEIVED_KEYWORDS:
            if re.search(keyword, oldest_received_str):
                return True, f"一番下のReceivedヘッダーにブラックリストキーワードを検知: '{keyword}'"

    return False, ""

def process_unseen_emails(mail):
    """未読メールをチェックしてスパムを処理し、正常メールなら通知シグナルを送る"""
    mail.select("INBOX")
    status, response = mail.search(None, "UNSEEN")
    mail_ids = response[0].split()
    
    if not mail_ids:
        return

    deleted_count = 0
    for m_id in mail_ids:
        status, msg_data = mail.fetch(m_id, "(BODY[HEADER])")
        for response_part in msg_data:
            if isinstance(response_part, tuple):
                msg = message_from_bytes(response_part[1])
                is_spam, reason = check_spam(msg)

                msg_subject = decode_mime_words(msg.get("Subject", "(No Subject)"))
                msg_header_from = decode_mime_words(msg.get("From", "(No From)"))

                if is_spam:
                    msg_envelope_from = msg.get("Return-Path", "(No Return-Path)")
                    received_headers = msg.get_all('Received', [])
                    oldest_received = received_headers[-1].replace('\r', '').replace('\n', ' ') if received_headers else "(No Received Header)"

                    if DRY_RUN:
                        print(f"[{get_timestamp()}] [DRY-RUN MATCH] Reason: {reason} | Return-Path: {msg_envelope_from} | From: {msg_header_from} | Subject: {msg_subject} | Oldest Received: {oldest_received}")
                        mail.store(m_id, '-FLAGS', '\\Seen')
                    else:
                        print(f"[{get_timestamp()}] [DELETED] Reason: {reason} | Return-Path: {msg_envelope_from} | From: {msg_header_from} | Subject: {msg_subject} | Oldest Received: {oldest_received}")
                        mail.store(m_id, '+FLAGS', '\\Deleted')
                        deleted_count += 1
                else:
                    # --- スパムではない正常な新着メールの場合 ---
                    # 一時ファイルに書き込んでUIプロセス側に通知を委託
                    send_desktop_notification(msg_subject, msg_header_from)
                    
                    # 未読状態(UNSEEN)を維持する
                    mail.store(m_id, '-FLAGS', '\\Seen')

    if not DRY_RUN and deleted_count > 0:
        mail.expunge()

# imaplibのSSLソケットを確実に捕まえるための拡張クラス
class MyIMAP4_SSL(imaplib.IMAP4_SSL):
    def open(self, host='', port=993, timeout=None):
        super().open(host, port, timeout)
        self.ssl_socket = self.sock

def main():
    print(f"[{get_timestamp()}] Starting IMAP IDLE Spam Filter Daemon...")
    
    while True:
        try:
            mail = MyIMAP4_SSL(IMAP_SERVER, IMAP_PORT)
            mail.login(USER, PASSWORD)
            mail.select("INBOX")
            
            # 起動直後に、現在溜まっている未読メールを一度すべて処理する
            process_unseen_emails(mail)

            print(f"[{get_timestamp()}] Entering IDLE mode. Waiting for new messages...")

            while True:
                mail.send(f'{mail._new_tag()} IDLE\r\n'.encode())
                
                idle_start = time.time()
                has_new_mail = False
                
                while time.time() - idle_start < 1200:
                    mail.ssl_socket.settimeout(10)
                    try:
                        resp = mail.ssl_socket.recv(4096).decode('utf-8', errors='ignore')
                        if "EXISTS" in resp or "RECENT" in resp or "FETCH" in resp:
                            has_new_mail = True
                            break
                    except socket.timeout:
                        continue
                    except Exception:
                        raise
                
                mail.send(b'DONE\r\n')
                mail.readline()
                
                if has_new_mail:
                    print(f"[{get_timestamp()}] New mail notification received!")
                    process_unseen_emails(mail)

        except (imaplib.IMAP4.abort, ConnectionResetError, socket.error) as e:
            print(f"[{get_timestamp()}] Connection lost ({e}). Reconnecting in 30 seconds...", file=sys.stderr)
            time.sleep(30)
        except Exception as e:
            print(f"[{get_timestamp()}] Unexpected error: {e}. Retrying in 60 seconds...", file=sys.stderr)
            time.sleep(60)

if __name__ == "__main__":
    import socket
    main()