From f49ee2ab957d2ca4c384765f3436da65f05a6dae Mon Sep 17 00:00:00 2001 From: Windmill Bot Date: Wed, 25 Feb 2026 10:31:36 +0900 Subject: [PATCH] Add mail_filter flow (manual export from Windmill API) --- workflows/f/mail/mail_filter__flow/flow.yaml | 290 +++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 workflows/f/mail/mail_filter__flow/flow.yaml diff --git a/workflows/f/mail/mail_filter__flow/flow.yaml b/workflows/f/mail/mail_filter__flow/flow.yaml new file mode 100644 index 0000000..2be2c53 --- /dev/null +++ b/workflows/f/mail/mail_filter__flow/flow.yaml @@ -0,0 +1,290 @@ +summary: メールフィルタリング +description: IMAPで新着メールを受信し、送信者ルール確認→LLM判定→LINE通知を行う。Keinasystemと連携。Gmail→Hotmail→Xserverの順で段階的に有効化する。 +value: + modules: + - id: a + value: + lock: '# py: 3.12 + + anyio==4.12.1 + + certifi==2026.1.4 + + h11==0.16.0 + + httpcore==1.0.9 + + httpx==0.28.1 + + idna==3.11 + + typing-extensions==4.15.0 + + wmill==1.642.0' + type: rawscript + content: "import imaplib\nimport email\nimport email.header\nimport json\nimport\ + \ re\nimport ssl\nimport urllib.request\nimport urllib.parse\nfrom datetime\ + \ import datetime, timezone, timedelta\nfrom email.utils import parsedate_to_datetime\n\ + import wmill\n\nJST = timezone(timedelta(hours=9))\n\n# ============================================================\n\ + # アカウント設定\n# 新しいアカウントを追加する際は enabled: True にする\n# ============================================================\n\ + ACCOUNTS = [\n {\n \"name\": \"gmail\",\n \"account_code\"\ + : \"gmail\",\n \"host\": \"imap.gmail.com\",\n \"port\": 993,\n\ + \ \"user_var\": \"u/admin/GMAIL_IMAP_USER\",\n \"pass_var\"\ + : \"u/admin/GMAIL_IMAP_PASSWORD\",\n \"last_uid_var\": \"u/admin/MAIL_FILTER_GMAIL_LAST_UID\"\ + ,\n \"mailbox\": \"[Gmail]/All Mail\",\n \"enabled\": True,\n\ + \ },\n {\n \"name\": \"gmail_service\",\n \"account_code\"\ + : \"gmail_service\",\n \"host\": \"imap.gmail.com\",\n \"port\"\ + : 993,\n \"user_var\": \"u/admin/GMAIL2_IMAP_USER\",\n \"pass_var\"\ + : \"u/admin/GMAIL2_IMAP_PASSWORD\",\n \"last_uid_var\": \"u/admin/MAIL_FILTER_GMAIL2_LAST_UID\"\ + ,\n \"mailbox\": \"[Gmail]/All Mail\",\n \"enabled\": True,\n\ + \ },\n # Hotmail テスト後に有効化\n # {\n # \"name\": \"hotmail\"\ + ,\n # \"account_code\": \"hotmail\",\n # \"host\": \"outlook.office365.com\"\ + ,\n # \"port\": 993,\n # \"user_var\": \"u/admin/HOTMAIL_IMAP_USER\"\ + ,\n # \"pass_var\": \"u/admin/HOTMAIL_IMAP_PASSWORD\",\n # \"\ + last_uid_var\": \"u/admin/MAIL_FILTER_HOTMAIL_LAST_UID\",\n # \"enabled\"\ + : False,\n # },\n # Xserver (keinafarm.com) 6アカウント\n {\n \"\ + name\": \"xserver_akiracraftwork\",\n \"account_code\": \"xserver\"\ + ,\n \"host\": \"sv579.xserver.jp\",\n \"port\": 993,\n \ + \ \"user_var\": \"u/admin/XSERVER1_IMAP_USER\",\n \"pass_var\": \"\ + u/admin/XSERVER1_IMAP_PASSWORD\",\n \"last_uid_var\": \"u/admin/MAIL_FILTER_XSERVER1_LAST_UID\"\ + ,\n \"enabled\": True,\n },\n {\n \"name\": \"xserver_service\"\ + ,\n \"account_code\": \"xserver\",\n \"host\": \"sv579.xserver.jp\"\ + ,\n \"port\": 993,\n \"user_var\": \"u/admin/XSERVER2_IMAP_USER\"\ + ,\n \"pass_var\": \"u/admin/XSERVER2_IMAP_PASSWORD\",\n \"last_uid_var\"\ + : \"u/admin/MAIL_FILTER_XSERVER2_LAST_UID\",\n \"enabled\": True,\n\ + \ },\n {\n \"name\": \"xserver_midori\",\n \"account_code\"\ + : \"xserver\",\n \"host\": \"sv579.xserver.jp\",\n \"port\"\ + : 993,\n \"user_var\": \"u/admin/XSERVER3_IMAP_USER\",\n \"\ + pass_var\": \"u/admin/XSERVER3_IMAP_PASSWORD\",\n \"last_uid_var\"\ + : \"u/admin/MAIL_FILTER_XSERVER3_LAST_UID\",\n \"enabled\": True,\n\ + \ },\n {\n \"name\": \"xserver_kouseiren\",\n \"account_code\"\ + : \"xserver\",\n \"host\": \"sv579.xserver.jp\",\n \"port\"\ + : 993,\n \"user_var\": \"u/admin/XSERVER4_IMAP_USER\",\n \"\ + pass_var\": \"u/admin/XSERVER4_IMAP_PASSWORD\",\n \"last_uid_var\"\ + : \"u/admin/MAIL_FILTER_XSERVER4_LAST_UID\",\n \"enabled\": True,\n\ + \ },\n {\n \"name\": \"xserver_post\",\n \"account_code\"\ + : \"xserver\",\n \"host\": \"sv579.xserver.jp\",\n \"port\"\ + : 993,\n \"user_var\": \"u/admin/XSERVER5_IMAP_USER\",\n \"\ + pass_var\": \"u/admin/XSERVER5_IMAP_PASSWORD\",\n \"last_uid_var\"\ + : \"u/admin/MAIL_FILTER_XSERVER5_LAST_UID\",\n \"enabled\": True,\n\ + \ },\n {\n \"name\": \"xserver_sales\",\n \"account_code\"\ + : \"xserver\",\n \"host\": \"sv579.xserver.jp\",\n \"port\"\ + : 993,\n \"user_var\": \"u/admin/XSERVER6_IMAP_USER\",\n \"\ + pass_var\": \"u/admin/XSERVER6_IMAP_PASSWORD\",\n \"last_uid_var\"\ + : \"u/admin/MAIL_FILTER_XSERVER6_LAST_UID\",\n \"enabled\": True,\n\ + \ },\n]\n\n\ndef main():\n # 共通変数取得\n api_key = wmill.get_variable(\"\ + u/admin/KEINASYSTEM_API_KEY\")\n api_url = wmill.get_variable(\"u/admin/KEINASYSTEM_API_URL\"\ + ).rstrip(\"/\")\n gemini_key = wmill.get_variable(\"u/admin/GEMINI_API_KEY\"\ + )\n line_token = wmill.get_variable(\"u/admin/LINE_CHANNEL_ACCESS_TOKEN\"\ + )\n line_to = wmill.get_variable(\"u/admin/LINE_TO\")\n\n total_processed\ + \ = 0\n total_notified = 0\n\n for account in ACCOUNTS:\n if\ + \ not account[\"enabled\"]:\n continue\n print(f\"[{account['name']}]\ + \ 処理開始\")\n try:\n processed, notified = process_account(\n\ + \ account, api_key, api_url, gemini_key, line_token, line_to\n\ + \ )\n total_processed += processed\n total_notified\ + \ += notified\n print(f\"[{account['name']}] 処理完了: {processed}件処理,\ + \ {notified}件通知\")\n except Exception as e:\n print(f\"\ + [{account['name']}] エラー: {e}\")\n # 1アカウントが失敗しても他のアカウントは継続\n\n\ + \ return {\n \"total_processed\": total_processed,\n \"total_notified\"\ + : total_notified,\n }\n\n\ndef process_account(account, api_key, api_url,\ + \ gemini_key, line_token, line_to):\n user = wmill.get_variable(account[\"\ + user_var\"])\n password = wmill.get_variable(account[\"pass_var\"])\n\n\ + \ # 前回の最終UID取得\n try:\n last_uid_str = wmill.get_variable(account[\"\ + last_uid_var\"])\n last_uid = int(last_uid_str) if last_uid_str and\ + \ last_uid_str != \"0\" else None\n except Exception:\n last_uid\ + \ = None\n\n # IMAP接続\n ssl_ctx = ssl.create_default_context()\n \ + \ mail = imaplib.IMAP4_SSL(account[\"host\"], account[\"port\"], ssl_context=ssl_ctx)\n\ + \ mail.login(user, password)\n mailbox = account.get(\"mailbox\", \"\ + INBOX\")\n imap_mailbox = resolve_mailbox(mail, mailbox)\n\n try:\n\ + \ if last_uid is None:\n # 初回実行: 現在の最大UIDを記録して終了(既存メールは処理しない)\n\ + \ _, data = mail.uid(\"SEARCH\", None, \"ALL\")\n all_uids\ + \ = data[0].split() if data[0] else []\n max_uid = int(all_uids[-1])\ + \ if all_uids else 0\n wmill.set_variable(account[\"last_uid_var\"\ + ], str(max_uid))\n print(f\"[{account['name']}] 初回実行: 最大UID={max_uid}\ + \ を記録、既存メールはスキップ\")\n return 0, 0\n\n # last_uid より大きい UID\ + \ を検索\n search_criterion = f\"UID {last_uid + 1}:*\"\n _, data\ + \ = mail.uid(\"SEARCH\", None, search_criterion)\n raw_uids = data[0].split()\ + \ if data[0] else []\n new_uids = [u for u in raw_uids if int(u) >\ + \ last_uid]\n\n if not new_uids:\n print(f\"[{account['name']}]\ + \ 新着メールなし\")\n return 0, 0\n\n print(f\"[{account['name']}]\ + \ 新着{len(new_uids)}件\")\n\n processed = 0\n notified = 0\n \ + \ max_processed_uid = last_uid\n\n for uid_bytes in new_uids:\n\ + \ uid = int(uid_bytes)\n try:\n result\ + \ = process_message(\n mail, uid, account,\n \ + \ api_key, api_url, gemini_key, line_token, line_to\n \ + \ )\n processed += 1\n if result == \"notified\"\ + :\n notified += 1\n max_processed_uid =\ + \ max(max_processed_uid, uid)\n except Exception as e:\n \ + \ print(f\"[{account['name']}] UID={uid} 処理エラー: {e}\")\n \ + \ # 個別メッセージのエラーは継続、UIDは進めない\n\n # 処理済み最大UIDを保存(正常完了時のみ)\n \ + \ if max_processed_uid > last_uid:\n wmill.set_variable(account[\"\ + last_uid_var\"], str(max_processed_uid))\n\n return processed, notified\n\ + \ finally:\n mail.logout()\n\n\ndef process_message(mail, uid, account,\ + \ api_key, api_url, gemini_key, line_token, line_to):\n \"\"\"メッセージを1通処理。戻り値:\ + \ 'skipped' / 'not_important' / 'notified'\"\"\"\n account_code = account[\"\ + account_code\"]\n forwarding_map = account.get(\"forwarding_map\", {})\n\ + \n # メール取得\n _, data = mail.uid(\"FETCH\", str(uid), \"(RFC822)\")\n\ + \ if not data or not data[0]:\n return \"skipped\"\n\n raw_email\ + \ = data[0][1]\n msg = email.message_from_bytes(raw_email)\n\n # ヘッダー解析\n\ + \ message_id = msg.get(\"Message-ID\", \"\").strip()\n if not message_id:\n\ + \ message_id = f\"{account_code}-uid-{uid}\"\n\n sender_raw = msg.get(\"\ + From\", \"\")\n sender_email_addr = extract_email_address(sender_raw)\n\ + \ sender_domain = sender_email_addr.split(\"@\")[-1] if \"@\" in sender_email_addr\ + \ else \"\"\n\n subject = decode_header_value(msg.get(\"Subject\", \"(件名なし)\"\ + ))\n\n date_str = msg.get(\"Date\", \"\")\n try:\n received_at\ + \ = parsedate_to_datetime(date_str).isoformat()\n except Exception:\n \ + \ received_at = datetime.now(JST).isoformat()\n\n body_preview =\ + \ extract_body_preview(msg, max_chars=500)\n\n # 転送検出: To:ヘッダーのドメインが forwarding_map\ + \ に存在する場合は account_code を上書き\n if forwarding_map:\n to_raw = msg.get(\"\ + To\", \"\")\n if to_raw:\n to_addr = extract_email_address(to_raw)\n\ + \ to_domain = to_addr.split(\"@\")[-1] if \"@\" in to_addr else\ + \ \"\"\n if to_domain in forwarding_map:\n account_code\ + \ = forwarding_map[to_domain]\n print(f\" [転送検出] To:{to_addr}\ + \ → account: {account_code}\")\n\n print(f\" From: {sender_email_addr},\ + \ Subject: {subject[:50]}\")\n\n # --- ステップ1: 送信者ルール確認 ---\n rule_result\ + \ = call_api_get(api_key, api_url, \"/api/mail/sender-rule/\", {\n \ + \ \"email\": sender_email_addr,\n \"domain\": sender_domain,\n })\n\ + \n if rule_result.get(\"matched\"):\n rule = rule_result[\"rule\"\ + ]\n\n if rule == \"never_notify\":\n print(f\" → never_notify\ + \ ルール一致、スキップ\")\n return \"skipped\"\n\n elif rule == \"\ + always_notify\":\n print(f\" → always_notify ルール一致、即通知\")\n \ + \ result = post_email(api_key, api_url, {\n \"account\"\ + : account_code,\n \"message_id\": message_id,\n \ + \ \"sender_email\": sender_email_addr,\n \"sender_domain\"\ + : sender_domain,\n \"subject\": subject,\n \"\ + body_preview\": body_preview,\n \"received_at\": received_at,\n\ + \ \"llm_verdict\": \"important\",\n })\n \ + \ if result.get(\"feedback_url\"):\n send_line_notification(line_token,\ + \ line_to, account_code, sender_email_addr, subject, result[\"feedback_url\"\ + ])\n return \"notified\"\n return \"skipped\"\n\n\ + \ # --- ステップ2: LLM判定 ---\n context = call_api_get(api_key, api_url,\ + \ \"/api/mail/sender-context/\", {\n \"email\": sender_email_addr,\n\ + \ \"domain\": sender_domain,\n })\n verdict = judge_with_llm(gemini_key,\ + \ sender_email_addr, subject, body_preview, context)\n print(f\" → LLM判定:\ + \ {verdict}\")\n\n # --- ステップ3: Keinasystemに記録 ---\n result = post_email(api_key,\ + \ api_url, {\n \"account\": account_code,\n \"message_id\":\ + \ message_id,\n \"sender_email\": sender_email_addr,\n \"sender_domain\"\ + : sender_domain,\n \"subject\": subject,\n \"body_preview\"\ + : body_preview,\n \"received_at\": received_at,\n \"llm_verdict\"\ + : verdict,\n })\n\n if verdict == \"important\" and result.get(\"feedback_url\"\ + ):\n send_line_notification(line_token, line_to, account_code, sender_email_addr,\ + \ subject, result[\"feedback_url\"])\n return \"notified\"\n\n return\ + \ \"not_important\"\n\n\n# ============================================================\n\ + # メールボックス解決\n# ============================================================\n\ + \ndef resolve_mailbox(mail, mailbox):\n \"\"\"メールボックスを選択し SELECT する。\n\ + \ INBOX はそのまま、それ以外は指定名 -> \\\\All 属性でフォールバック。\n \"\"\"\n if mailbox\ + \ == \"INBOX\":\n typ, data = mail.select(\"INBOX\")\n if typ\ + \ != 'OK':\n raise Exception(f\"SELECT INBOX failed: {data}\")\n\ + \ return \"INBOX\"\n\n # まず指定名で試行\n imap_name = '\"' + mailbox\ + \ + '\"'\n typ, data = mail.select(imap_name)\n if typ == 'OK':\n \ + \ return imap_name\n\n # 失敗した場合: \\\\All 属性を持つメールボックスを自動検出\n print(f\"\ + \ [INFO] {mailbox} not found, searching for \\\\\\\\All mailbox...\")\n \ + \ typ2, mboxes = mail.list()\n if typ2 == 'OK':\n for mb in mboxes:\n\ + \ if not mb:\n continue\n mb_str = mb.decode()\ + \ if isinstance(mb, bytes) else mb\n if '\\\\\\\\All' in mb_str\ + \ or '\\\\All' in mb_str:\n # \"(attrs) \\\".\\\" \\\"name\\\ + \"\" 形式から名前を抽出\n parts = mb_str.rsplit('\"', 2)\n \ + \ if len(parts) >= 2 and parts[-2]:\n found = parts[-2]\n\ + \ else:\n found = mb_str.split()[-1].strip('\"\ + ')\n print(f\" [INFO] Found All Mail mailbox: {found}\")\n\ + \ imap_found = '\"' + found + '\"'\n typ3, data3\ + \ = mail.select(imap_found)\n if typ3 == 'OK':\n \ + \ return imap_found\n raise Exception(f\"Could not select any\ + \ All Mail mailbox (tried: {mailbox})\")\n\n\n# ============================================================\n\ + # APIヘルパー\n# ============================================================\n\ + \ndef _make_ssl_ctx():\n ctx = ssl.create_default_context()\n ctx.check_hostname\ + \ = False\n ctx.verify_mode = ssl.CERT_NONE\n return ctx\n\n\ndef call_api_get(api_key,\ + \ api_url, path, params):\n qs = urllib.parse.urlencode(params)\n url\ + \ = f\"{api_url}{path}?{qs}\"\n req = urllib.request.Request(url, headers={\"\ + X-API-Key\": api_key})\n with urllib.request.urlopen(req, context=_make_ssl_ctx(),\ + \ timeout=10) as resp:\n return json.loads(resp.read().decode(\"utf-8\"\ + ))\n\n\ndef post_email(api_key, api_url, data):\n url = f\"{api_url}/api/mail/emails/\"\ + \n payload = json.dumps(data).encode(\"utf-8\")\n req = urllib.request.Request(\n\ + \ url,\n data=payload,\n headers={\"X-API-Key\": api_key,\ + \ \"Content-Type\": \"application/json\"},\n method=\"POST\",\n \ + \ )\n try:\n with urllib.request.urlopen(req, context=_make_ssl_ctx(),\ + \ timeout=10) as resp:\n return json.loads(resp.read().decode(\"\ + utf-8\"))\n except urllib.error.HTTPError as e:\n body = e.read().decode(\"\ + utf-8\")\n if e.code == 400 and \"message_id\" in body:\n \ + \ # 重複message_idは正常(再実行時の冦殁)\n print(f\" 重複メール、スキップ\")\n \ + \ return {}\n raise\n\n\nACCOUNT_LABELS = {\n \"gmail\"\ + : \"Gmail (メイン)\",\n \"gmail_service\": \"Gmail (サービス用)\",\n \"hotmail\"\ + : \"Hotmail\",\n \"xserver\": \"Xserver\",\n}\n\ndef send_line_notification(line_token,\ + \ line_to, account_code, sender_email_addr, subject, feedback_url):\n account_label\ + \ = ACCOUNT_LABELS.get(account_code, account_code)\n message = (\n \ + \ f\"\U0001F4E7 重要なメールが届きました\\n\\n\"\n f\"宛先: {account_label}\\\ + n\"\n f\"差出人: {sender_email_addr}\\n\"\n f\"件名: {subject}\\\ + n\\n\"\n f\"フィードバック:\\n{feedback_url}\"\n )\n payload = json.dumps({\n\ + \ \"to\": line_to,\n \"messages\": [{\"type\": \"text\", \"\ + text\": message}],\n }).encode(\"utf-8\")\n req = urllib.request.Request(\n\ + \ \"https://api.line.me/v2/bot/message/push\",\n data=payload,\n\ + \ headers={\n \"Authorization\": f\"Bearer {line_token}\"\ + ,\n \"Content-Type\": \"application/json\",\n },\n \ + \ method=\"POST\",\n )\n with urllib.request.urlopen(req, timeout=30)\ + \ as resp:\n resp.read()\n\n\n# ============================================================\n\ + # LLM判定(Gemini API)\n# ============================================================\n\ + \ndef judge_with_llm(gemini_key, sender_email_addr, subject, body_preview,\ + \ context):\n \"\"\"農家にとって重要なメールか判定。'important' または 'not_important' を返す。\"\ + \"\"\n\n context_text = \"\"\n total = context.get(\"total_notified\"\ + , 0)\n if total > 0:\n context_text = (\n f\"\\n\\n[この送信者の過去データ]\ + \ \"\n f\"通知済み{total}件: \"\n f\"重要{context.get('important',\ + \ 0)}件 / \"\n f\"普通{context.get('not_important', 0)}件 / \"\n \ + \ f\"通知不要{context.get('never_notify', 0)}件 / \"\n f\"\ + 未評価{context.get('no_feedback', 0)}件\"\n )\n\n user_message = (\n\ + \ f\"送信者: {sender_email_addr}\\n\"\n f\"件名: {subject}\\n\"\n\ + \ f\"本文冠頭:\\n{body_preview}\"\n f\"{context_text}\\n\\n\"\n\ + \ f\"このメールは農家にとって重要ですか?\\n\"\n f\"1: 重要(要確認)\\n\"\n f\"\ + 2: 重要でない(営業・通知等)\\n\"\n f\"数字1文字のみで答えてください。\"\n )\n\n payload\ + \ = json.dumps({\n \"system_instruction\": {\n \"parts\"\ + : [{\"text\": \"あなたは農家のメールフィルタリングアシスタントです。メールが重要かどうかを判定してください。\"}]\n \ + \ },\n \"contents\": [{\n \"role\": \"user\",\n \ + \ \"parts\": [{\"text\": user_message}]\n }],\n \"generationConfig\"\ + : {\n \"maxOutputTokens\": 10,\n \"temperature\": 0\n\ + \ }\n }).encode(\"utf-8\")\n\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={gemini_key}\"\ + \n req = urllib.request.Request(\n url,\n data=payload,\n\ + \ headers={\"Content-Type\": \"application/json\"},\n method=\"\ + POST\",\n )\n with urllib.request.urlopen(req, timeout=30) as resp:\n\ + \ result = json.loads(resp.read().decode(\"utf-8\"))\n answer\ + \ = result[\"candidates\"][0][\"content\"][\"parts\"][0][\"text\"].strip()\n\ + \n return \"important\" if answer.startswith(\"1\") else \"not_important\"\ + \n\n\n# ============================================================\n# メール解析ヘルパー\n\ + # ============================================================\n\ndef extract_email_address(raw):\n\ + \ \"\"\"'Name ' または 'email@example.com' からアドレスを抽出\"\"\ + \"\n match = re.search(r'<([^>]+)>', raw)\n if match:\n return\ + \ match.group(1).strip().lower()\n return raw.strip().lower()\n\n\ndef\ + \ decode_header_value(value):\n \"\"\"MIMEエンコードされたヘッダー値をデコード\"\"\"\n \ + \ if not value:\n return \"\"\n parts = email.header.decode_header(value)\n\ + \ decoded = []\n for part, charset in parts:\n if isinstance(part,\ + \ bytes):\n decoded.append(part.decode(charset or \"utf-8\", errors=\"\ + replace\"))\n else:\n decoded.append(part)\n return \"\ + \".join(decoded)\n\n\ndef extract_body_preview(msg, max_chars=500):\n \"\ + \"\"メール本文の冠頭を抽出(テキスト優先、HTMLフォールバック)\"\"\"\n text_content = \"\"\n html_content\ + \ = \"\"\n\n if msg.is_multipart():\n for part in msg.walk():\n\ + \ ctype = part.get_content_type()\n if ctype == \"text/plain\"\ + \ and not text_content:\n charset = part.get_content_charset()\ + \ or \"utf-8\"\n try:\n text_content = part.get_payload(decode=True).decode(charset,\ + \ errors=\"replace\")\n except Exception:\n \ + \ pass\n elif ctype == \"text/html\" and not html_content:\n\ + \ charset = part.get_content_charset() or \"utf-8\"\n \ + \ try:\n html_content = part.get_payload(decode=True).decode(charset,\ + \ errors=\"replace\")\n except Exception:\n \ + \ pass\n else:\n charset = msg.get_content_charset() or \"utf-8\"\ + \n try:\n content = msg.get_payload(decode=True).decode(charset,\ + \ errors=\"replace\")\n if msg.get_content_type() == \"text/html\"\ + :\n html_content = content\n else:\n \ + \ text_content = content\n except Exception:\n pass\n\ + \n if text_content:\n # フッター・区切り線を除去\n text = re.sub(r'\\\ + n[-_=]{10,}\\n.*', '', text_content, flags=re.DOTALL)\n text = re.sub(r'\\\ + s+', ' ', text).strip()\n return text[:max_chars]\n\n if html_content:\n\ + \ # HTMLタグを除去\n text = re.sub(r'<[^>]+>', ' ', html_content)\n\ + \ text = re.sub(r'\\s+', ' ', text).strip()\n return text[:max_chars]\n\ + \n return \"\"\n" + language: python3 + input_transforms: {} + summary: メール取得・判定・通知 +schema: + $schema: https://json-schema.org/draft/2020-12/schema + type: object + order: [] + properties: {} + required: []