import os
import subprocess
import time
import threading
import gi
gi.require_version('Gio', '2.0')
from gi.repository import Gio, GLib
# ==================== 設定項目 ====================
# Thunderbirdを使う場合(ダウンロード版はPATHを設定してください)
MAIL_CLIENT_COMMAND = ["thunderbird"]
# ブラウザ(大学のWebメール等)を開きたい場合
# MAIL_CLIENT_COMMAND = ["gio", "open", "https://webmail.university.ac.jp/login"]
FIFO_PATH = "/var/log/mail-notify.fifo"
# ==================================================
# spam-filter-daemon.py とセットで機能します。
# https://chronoflyer.ddo.jp/~trinary/misc/spam-filter-daemon.py.html
#
# # インストール手順
# $ chmod +x /PATH/TO/YOUR/mail-notification-receiver.py
# $ sudo touch /var/log/mail-notify.fifo # FIFO_PATH
# $ sudo chmod 666 /var/log/mail-notify.fifo # FIFO_PATH
# $ mkdir -p ~/.config/autostart
# $ vi ~/.config/autostart/mail-notification.desktop
# [Desktop Entry]
# Type=Application
# Exec=/usr/bin/python3 /PATH/TO/YOUR/mail-notification-receiver.py
# Hidden=false
# NoDisplay=false
# X-GNOME-Autostart-enabled=true
# Name=Mail Notification Receiver
# Comment=Watch mail notify file and show dunstify
#
# # ログアウトして再ログインしてください。
# # ログを確認しながら動作を検証したい場合はプロセスを終了してから再起動してください
# $ pkill -f mail-notification-receiver.py
# $ python3 /PATH/TO/YOUR/mail-notification-receiver.py
# ==================================================
# 前回発行した通知IDを保存するファイル(上書き用)
ID_FILE = "/tmp/mail_notification_id.txt"
# GLibのメインループ初期化(D-Busイベントの待機用)
loop = GLib.MainLoop()
def get_previous_id():
if os.path.exists(ID_FILE):
try:
with open(ID_FILE, "r") as f:
return int(f.read().strip())
except ValueError:
pass
return 0
def save_current_id(notif_id):
with open(ID_FILE, "w") as f:
f.write(str(notif_id))
def on_signal_received(connection, sender_name, object_path, interface_name, signal_name, parameters, user_data):
"""D-Busからイベント(シグナル)を受け取ったときの処理"""
target_id = user_data
# ユーザーがボタンをクリックしたとき
if signal_name == "ActionInvoked":
notif_id, action_key = parameters.unpack()
if notif_id == target_id and action_key == "open_mail":
print(f"[通知ID: {notif_id}] 『メールを開く』がクリックされました。メーラーを起動します。", flush=True)
subprocess.Popen(MAIL_CLIENT_COMMAND, start_new_session=True)
loop.quit()
# ユーザーが通知を明示的に閉じた、または時間切れで消えたとき
elif signal_name == "NotificationClosed":
notif_id, reason = parameters.unpack()
if notif_id == target_id:
loop.quit()
def trigger_notification(subject, from_addr):
"""ネイティブD-Bus経由で通知を送信し、クリックを待ち受ける"""
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
replaces_id = get_previous_id()
# D-BusのNotifyメソッドの引数を組み立て
parameters = GLib.Variant('(susssasa{sv}i)', (
"NewEmailApp", # app_name
replaces_id, # replaces_id (前回のIDを指定して上書き)
"mail-unread", # app_icon
f"📧 新着メール: {subject}", # summary
f"From: {from_addr}", # body
["open_mail", "メールを開く"], # actions (キー, ボタン名)
{"sound-name": GLib.Variant('s', 'message-new-email')}, # hints (システム効果音)
-1 # expire_timeout
))
try:
result = bus.call_sync(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
"Notify",
parameters,
None,
Gio.DBusCallFlags.NONE,
-1,
None
)
new_id = result.unpack()[0] if isinstance(result.unpack(), tuple) else result.unpack()
save_current_id(new_id)
print(f"通知を送信しました。通知ID: {new_id} (クリックを待っています...)", flush=True)
# シグナル(クリックイベント)の購読
sub_id = bus.signal_subscribe(
"org.freedesktop.Notifications",
"org.freedesktop.Notifications",
None,
"/org/freedesktop/Notifications",
None,
Gio.DBusSignalFlags.NONE,
on_signal_received,
new_id
)
# ボタンクリックまたは通知が閉じるまで、このスレッドのループを回して待機
loop.run()
# 待機が終わったらこの通知用の購読を解除
bus.signal_unsubscribe(sub_id)
except Exception as e:
print(f"通知の送信または監視に失敗: {e}", flush=True)
def handle_notification_thread(subject, from_addr):
"""FIFO監視をブロックしないよう、通知とクリック待ち受けを別スレッドで実行"""
threading.Thread(target=trigger_notification, args=(subject, from_addr), daemon=True).start()
print("新着メール通知レシーバーを起動しました...", flush=True)
# === ステップ1: ログアウト中に溜まったメールがあれば一括で通知する ===
if os.path.exists(FIFO_PATH) and os.path.getsize(FIFO_PATH) > 0:
with open(FIFO_PATH, "r", encoding="utf-8") as f:
lines = f.readlines()
for line in lines:
if "\t" in line:
subject, from_addr = line.strip().split("\t", 1)
handle_notification_thread(subject, from_addr)
time.sleep(0.5) # 通知が重なりすぎて詰まらないように少しウェイトを入れる
# === ステップ2: 溜まった分の通知が終わったら、ファイルを安全に空(初期化)にする ===
try:
with open(FIFO_PATH, "w", encoding="utf-8") as f:
f.truncate(0)
except Exception as e:
print(f"ログイン時のファイル初期化に失敗: {e}", flush=True)
# === ステップ3: リアルタイムに追記されるメールを常時監視(受信後に即掃除) ===
while True:
if os.path.exists(FIFO_PATH) and os.path.getsize(FIFO_PATH) > 0:
try:
with open(FIFO_PATH, "r", encoding="utf-8") as f:
lines = f.readlines()
with open(FIFO_PATH, "w", encoding="utf-8") as f:
f.truncate(0)
for line in lines:
line_stripped = line.strip()
if not line_stripped or "\t" not in line_stripped:
continue
subject, from_addr = line_stripped.split("\t", 1)
if subject.strip() and from_addr.strip():
# リアルタイム受信時も別スレッドで通知を実行(メインのFIFO監視ループを止めない)
handle_notification_thread(subject, from_addr)
except Exception as e:
print(f"リアルタイム監視中の処理または初期化に失敗: {e}", flush=True)
time.sleep(1)