Skip to content

Commit a188841

Browse files
committed
Add IMAP email poll trigger
A poll-based watcher logs into IMAP mailboxes on a configurable interval and runs an action JSON file once per matching message. Subject, sender, body, message-id, and uid are seeded into the variable scope so scripts can branch on email content via ${email.subject} placeholders. The watcher tracks already-fired UIDs in process and optionally marks messages \Seen so the same mail is not handled twice. Driven from headless code via AC_email_trigger_* and a new Email Triggers GUI tab.
1 parent 87b2796 commit a188841

11 files changed

Lines changed: 988 additions & 1 deletion

File tree

je_auto_control/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@
148148
from je_auto_control.utils.triggers.webhook_server import (
149149
WebhookTrigger, WebhookTriggerServer, default_webhook_server,
150150
)
151+
from je_auto_control.utils.triggers.email_trigger import (
152+
EmailTrigger, EmailTriggerWatcher, default_email_trigger_watcher,
153+
)
151154
# Recording editor (headless helpers)
152155
from je_auto_control.utils.recording_edit.editor import (
153156
adjust_delays, filter_actions, insert_action, remove_action,
@@ -315,6 +318,8 @@ def start_autocontrol_gui(*args, **kwargs):
315318
"ImageAppearsTrigger", "WindowAppearsTrigger",
316319
"PixelColorTrigger", "FilePathTrigger",
317320
"WebhookTrigger", "WebhookTriggerServer", "default_webhook_server",
321+
"EmailTrigger", "EmailTriggerWatcher",
322+
"default_email_trigger_watcher",
318323
# Profiler
319324
"ActionProfiler", "ActionStats", "default_profiler",
320325
# Secret manager
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
"""Email Triggers tab: bind IMAP mailboxes to action scripts."""
2+
from typing import Optional
3+
4+
from PySide6.QtCore import QTimer, Qt
5+
from PySide6.QtWidgets import (
6+
QAbstractItemView, QCheckBox, QFileDialog, QGroupBox, QHBoxLayout,
7+
QHeaderView, QLabel, QLineEdit, QMessageBox, QPushButton,
8+
QSpinBox, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
9+
)
10+
11+
from je_auto_control.gui._i18n_helpers import TranslatableMixin
12+
from je_auto_control.gui.language_wrapper.multi_language_wrapper import (
13+
language_wrapper,
14+
)
15+
from je_auto_control.utils.triggers.email_trigger import (
16+
default_email_trigger_watcher,
17+
)
18+
19+
20+
_REFRESH_MS = 1500
21+
22+
23+
def _t(key: str) -> str:
24+
return language_wrapper.translate(key, key)
25+
26+
27+
class EmailTriggersTab(TranslatableMixin, QWidget):
28+
"""GUI front-end for :data:`default_email_trigger_watcher`."""
29+
30+
def __init__(self, parent: Optional[QWidget] = None) -> None:
31+
super().__init__(parent)
32+
self._tr_init()
33+
self._host_input = QLineEdit()
34+
self._host_input.setPlaceholderText("imap.example.com")
35+
self._port_input = QSpinBox()
36+
self._port_input.setRange(0, 65535)
37+
self._port_input.setValue(993)
38+
self._user_input = QLineEdit()
39+
self._user_input.setPlaceholderText("user@example.com")
40+
self._password_input = QLineEdit()
41+
self._password_input.setEchoMode(QLineEdit.Password)
42+
self._password_input.setPlaceholderText(_t("eml_password_placeholder"))
43+
self._mailbox_input = QLineEdit("INBOX")
44+
self._search_input = QLineEdit("UNSEEN")
45+
self._poll_input = QSpinBox()
46+
self._poll_input.setRange(5, 86_400)
47+
self._poll_input.setValue(60)
48+
self._script_input = QLineEdit()
49+
self._ssl_check = self._tr(QCheckBox(), "eml_ssl")
50+
self._ssl_check.setChecked(True)
51+
self._mark_seen_check = self._tr(QCheckBox(), "eml_mark_seen")
52+
self._mark_seen_check.setChecked(True)
53+
self._status_label = QLabel()
54+
self._table = QTableWidget(0, 7)
55+
self._table.setEditTriggers(QAbstractItemView.NoEditTriggers)
56+
self._table.setSelectionBehavior(QAbstractItemView.SelectRows)
57+
self._table.verticalHeader().setVisible(False)
58+
self._table.horizontalHeader().setSectionResizeMode(
59+
QHeaderView.Interactive,
60+
)
61+
self._table.horizontalHeader().setStretchLastSection(True)
62+
self._apply_table_headers()
63+
self._build_layout()
64+
self._timer = QTimer(self)
65+
self._timer.setInterval(_REFRESH_MS)
66+
self._timer.timeout.connect(self._refresh)
67+
self._timer.start()
68+
self._refresh()
69+
70+
def retranslate(self) -> None:
71+
TranslatableMixin.retranslate(self)
72+
self._apply_table_headers()
73+
self._refresh()
74+
75+
def _apply_table_headers(self) -> None:
76+
self._table.setHorizontalHeaderLabels([
77+
_t("eml_col_id"), _t("eml_col_host"), _t("eml_col_user"),
78+
_t("eml_col_mailbox"), _t("eml_col_script"),
79+
_t("eml_col_fired"), _t("eml_col_error"),
80+
])
81+
82+
def _build_layout(self) -> None:
83+
root = QVBoxLayout(self)
84+
85+
engine_box = self._tr(QGroupBox(), "eml_engine_group")
86+
engine_layout = QHBoxLayout(engine_box)
87+
start_btn = self._tr(QPushButton(), "eml_start")
88+
start_btn.clicked.connect(self._on_start)
89+
engine_layout.addWidget(start_btn)
90+
stop_btn = self._tr(QPushButton(), "eml_stop")
91+
stop_btn.clicked.connect(self._on_stop)
92+
engine_layout.addWidget(stop_btn)
93+
poll_btn = self._tr(QPushButton(), "eml_poll_now")
94+
poll_btn.clicked.connect(self._on_poll_now)
95+
engine_layout.addWidget(poll_btn)
96+
engine_layout.addWidget(self._status_label)
97+
engine_layout.addStretch()
98+
root.addWidget(engine_box)
99+
100+
add_box = self._tr(QGroupBox(), "eml_add_group")
101+
add_layout = QVBoxLayout(add_box)
102+
host_row = QHBoxLayout()
103+
host_row.addWidget(self._tr(QLabel(), "eml_host_label"))
104+
host_row.addWidget(self._host_input)
105+
host_row.addWidget(self._tr(QLabel(), "eml_port_label"))
106+
host_row.addWidget(self._port_input)
107+
host_row.addWidget(self._ssl_check)
108+
add_layout.addLayout(host_row)
109+
creds_row = QHBoxLayout()
110+
creds_row.addWidget(self._tr(QLabel(), "eml_user_label"))
111+
creds_row.addWidget(self._user_input)
112+
creds_row.addWidget(self._tr(QLabel(), "eml_password_label"))
113+
creds_row.addWidget(self._password_input)
114+
add_layout.addLayout(creds_row)
115+
mb_row = QHBoxLayout()
116+
mb_row.addWidget(self._tr(QLabel(), "eml_mailbox_label"))
117+
mb_row.addWidget(self._mailbox_input)
118+
mb_row.addWidget(self._tr(QLabel(), "eml_search_label"))
119+
mb_row.addWidget(self._search_input)
120+
add_layout.addLayout(mb_row)
121+
poll_row = QHBoxLayout()
122+
poll_row.addWidget(self._tr(QLabel(), "eml_poll_label"))
123+
poll_row.addWidget(self._poll_input)
124+
poll_row.addWidget(self._mark_seen_check)
125+
poll_row.addStretch()
126+
add_layout.addLayout(poll_row)
127+
script_row = QHBoxLayout()
128+
script_row.addWidget(self._tr(QLabel(), "eml_script_label"))
129+
script_row.addWidget(self._script_input)
130+
browse_btn = self._tr(QPushButton(), "eml_browse")
131+
browse_btn.clicked.connect(self._on_browse)
132+
script_row.addWidget(browse_btn)
133+
register_btn = self._tr(QPushButton(), "eml_register")
134+
register_btn.clicked.connect(self._on_register)
135+
script_row.addWidget(register_btn)
136+
add_layout.addLayout(script_row)
137+
root.addWidget(add_box)
138+
139+
root.addWidget(self._table, stretch=1)
140+
action_row = QHBoxLayout()
141+
remove_btn = self._tr(QPushButton(), "eml_remove")
142+
remove_btn.clicked.connect(self._on_remove)
143+
action_row.addWidget(remove_btn)
144+
action_row.addStretch()
145+
root.addLayout(action_row)
146+
147+
def _on_start(self) -> None:
148+
default_email_trigger_watcher.start()
149+
self._refresh()
150+
151+
def _on_stop(self) -> None:
152+
default_email_trigger_watcher.stop()
153+
self._refresh()
154+
155+
def _on_poll_now(self) -> None:
156+
try:
157+
fired = default_email_trigger_watcher.poll_once()
158+
except (OSError, RuntimeError) as error:
159+
QMessageBox.warning(self, _t("eml_poll_now"), str(error))
160+
return
161+
QMessageBox.information(
162+
self, _t("eml_poll_now"),
163+
_t("eml_poll_done").replace("{n}", str(fired)),
164+
)
165+
self._refresh()
166+
167+
def _on_browse(self) -> None:
168+
path, _ = QFileDialog.getOpenFileName(
169+
self, _t("eml_browse"), "", "JSON (*.json)",
170+
)
171+
if path:
172+
self._script_input.setText(path)
173+
174+
def _on_register(self) -> None:
175+
host = self._host_input.text().strip()
176+
user = self._user_input.text().strip()
177+
password = self._password_input.text()
178+
script = self._script_input.text().strip()
179+
if not host or not user or not password or not script:
180+
QMessageBox.warning(self, _t("eml_register"),
181+
_t("eml_required_fields"))
182+
return
183+
try:
184+
default_email_trigger_watcher.add(
185+
host=host, username=user, password=password,
186+
script_path=script,
187+
port=int(self._port_input.value()),
188+
use_ssl=self._ssl_check.isChecked(),
189+
mailbox=self._mailbox_input.text().strip() or "INBOX",
190+
search_criteria=self._search_input.text().strip() or "UNSEEN",
191+
mark_seen=self._mark_seen_check.isChecked(),
192+
poll_seconds=float(self._poll_input.value()),
193+
)
194+
except ValueError as error:
195+
QMessageBox.warning(self, _t("eml_register"), str(error))
196+
return
197+
self._password_input.clear()
198+
self._refresh()
199+
200+
def _on_remove(self) -> None:
201+
row = self._table.currentRow()
202+
if row < 0:
203+
return
204+
item = self._table.item(row, 0)
205+
if item is None:
206+
return
207+
default_email_trigger_watcher.remove(item.text())
208+
self._refresh()
209+
210+
def _refresh(self) -> None:
211+
running = default_email_trigger_watcher.is_running
212+
self._status_label.setText(
213+
_t("eml_running") if running else _t("eml_stopped"),
214+
)
215+
rows = default_email_trigger_watcher.list_triggers()
216+
self._table.setRowCount(len(rows))
217+
for row, trigger in enumerate(rows):
218+
values = (
219+
trigger.trigger_id,
220+
f"{trigger.host}:{trigger.port}",
221+
trigger.username,
222+
trigger.mailbox,
223+
trigger.script_path,
224+
str(trigger.fired),
225+
trigger.last_error or "",
226+
)
227+
for col, text in enumerate(values):
228+
item = QTableWidgetItem(text)
229+
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
230+
self._table.setItem(row, col, item)

je_auto_control/gui/language_wrapper/english.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"tab_hotkeys": "Hotkeys",
2727
"tab_triggers": "Triggers",
2828
"tab_webhooks": "Webhooks",
29+
"tab_email_triggers": "Email Triggers",
2930
"tab_plugins": "Plugins",
3031
"tab_screen_record": "Screen Recording",
3132
"tab_shell": "Shell Command",
@@ -666,6 +667,38 @@
666667
"rh_preview_empty": "Select a run to preview.",
667668
"rh_preview_no_artifact": "No screenshot for this run.",
668669

670+
# Email triggers tab
671+
"eml_engine_group": "Polling engine",
672+
"eml_add_group": "New IMAP trigger",
673+
"eml_start": "Start polling",
674+
"eml_stop": "Stop polling",
675+
"eml_poll_now": "Poll now",
676+
"eml_poll_done": "Fired {n} message(s) on this pass.",
677+
"eml_running": "Polling is active.",
678+
"eml_stopped": "Polling is stopped.",
679+
"eml_host_label": "Host:",
680+
"eml_port_label": "Port:",
681+
"eml_user_label": "User:",
682+
"eml_password_label": "Password:",
683+
"eml_password_placeholder": "IMAP password / app password",
684+
"eml_mailbox_label": "Mailbox:",
685+
"eml_search_label": "Search:",
686+
"eml_poll_label": "Poll (s):",
687+
"eml_script_label": "Script:",
688+
"eml_browse": "Browse",
689+
"eml_register": "Register trigger",
690+
"eml_remove": "Remove selected",
691+
"eml_ssl": "Use SSL",
692+
"eml_mark_seen": "Mark as seen after firing",
693+
"eml_required_fields": "Host, user, password, and script are required.",
694+
"eml_col_id": "ID",
695+
"eml_col_host": "Host",
696+
"eml_col_user": "User",
697+
"eml_col_mailbox": "Mailbox",
698+
"eml_col_script": "Script",
699+
"eml_col_fired": "Fired",
700+
"eml_col_error": "Last error",
701+
669702
# Webhooks tab
670703
"wh_server_group": "HTTP server",
671704
"wh_add_group": "New webhook",

je_auto_control/gui/language_wrapper/japanese.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"tab_hotkeys": "ホットキー",
2525
"tab_triggers": "トリガー",
2626
"tab_webhooks": "Webhook",
27+
"tab_email_triggers": "Email トリガー",
2728
"tab_plugins": "プラグイン",
2829
"tab_screen_record": "画面録画",
2930
"tab_shell": "シェル",
@@ -664,6 +665,38 @@
664665
"rh_preview_empty": "実行を選択するとプレビューが表示されます。",
665666
"rh_preview_no_artifact": "この実行のスクリーンショットはありません。",
666667

668+
# Email triggers tab
669+
"eml_engine_group": "ポーリングエンジン",
670+
"eml_add_group": "新規 IMAP トリガー",
671+
"eml_start": "ポーリング開始",
672+
"eml_stop": "ポーリング停止",
673+
"eml_poll_now": "今すぐポーリング",
674+
"eml_poll_done": "今回 {n} 件のメッセージで発火しました。",
675+
"eml_running": "ポーリング中。",
676+
"eml_stopped": "ポーリング停止中。",
677+
"eml_host_label": "ホスト:",
678+
"eml_port_label": "ポート:",
679+
"eml_user_label": "ユーザー:",
680+
"eml_password_label": "パスワード:",
681+
"eml_password_placeholder": "IMAP パスワード / アプリパスワード",
682+
"eml_mailbox_label": "メールボックス:",
683+
"eml_search_label": "検索条件:",
684+
"eml_poll_label": "ポーリング (秒):",
685+
"eml_script_label": "スクリプト:",
686+
"eml_browse": "参照",
687+
"eml_register": "トリガーを登録",
688+
"eml_remove": "選択を削除",
689+
"eml_ssl": "SSL を使用",
690+
"eml_mark_seen": "発火後に既読にする",
691+
"eml_required_fields": "ホスト、ユーザー、パスワード、スクリプトが必要です。",
692+
"eml_col_id": "ID",
693+
"eml_col_host": "ホスト",
694+
"eml_col_user": "ユーザー",
695+
"eml_col_mailbox": "メールボックス",
696+
"eml_col_script": "スクリプト",
697+
"eml_col_fired": "発火回数",
698+
"eml_col_error": "最近のエラー",
699+
667700
# Webhooks tab
668701
"wh_server_group": "HTTP サーバー",
669702
"wh_add_group": "新規 webhook",

je_auto_control/gui/language_wrapper/simplified_chinese.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"tab_hotkeys": "全局热键",
1717
"tab_triggers": "事件触发器",
1818
"tab_webhooks": "Webhook 触发",
19+
"tab_email_triggers": "Email 触发",
1920
"tab_plugins": "插件",
2021
"tab_screen_record": "屏幕录像",
2122
"tab_shell": "Shell 命令",
@@ -654,6 +655,38 @@
654655
"rh_preview_empty": "请选择一条记录预览。",
655656
"rh_preview_no_artifact": "此次执行没有截图。",
656657

658+
# Email triggers tab
659+
"eml_engine_group": "轮询引擎",
660+
"eml_add_group": "新增 IMAP 触发",
661+
"eml_start": "开始轮询",
662+
"eml_stop": "停止轮询",
663+
"eml_poll_now": "立即轮询",
664+
"eml_poll_done": "本次共触发 {n} 封邮件。",
665+
"eml_running": "轮询中。",
666+
"eml_stopped": "轮询已停止。",
667+
"eml_host_label": "主机:",
668+
"eml_port_label": "端口:",
669+
"eml_user_label": "用户:",
670+
"eml_password_label": "密码:",
671+
"eml_password_placeholder": "IMAP 密码 / 应用密码",
672+
"eml_mailbox_label": "邮箱:",
673+
"eml_search_label": "搜索条件:",
674+
"eml_poll_label": "轮询(秒):",
675+
"eml_script_label": "脚本:",
676+
"eml_browse": "浏览",
677+
"eml_register": "注册触发",
678+
"eml_remove": "删除所选",
679+
"eml_ssl": "使用 SSL",
680+
"eml_mark_seen": "触发后标记为已读",
681+
"eml_required_fields": "请填入主机、用户、密码和脚本。",
682+
"eml_col_id": "ID",
683+
"eml_col_host": "主机",
684+
"eml_col_user": "用户",
685+
"eml_col_mailbox": "邮箱",
686+
"eml_col_script": "脚本",
687+
"eml_col_fired": "触发次数",
688+
"eml_col_error": "最近错误",
689+
657690
# Webhooks tab
658691
"wh_server_group": "HTTP 服务器",
659692
"wh_add_group": "新增 webhook",

0 commit comments

Comments
 (0)