|
| 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) |
0 commit comments