From d3c9cd2c7f8873ca74924997a1f8d53fdd317a6b Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 29 Dec 2025 15:50:35 +0900 Subject: [PATCH] feat: Implement AI classification and Web UI, refactor to IMAP --- .dockerignore | 15 ++++ .gitignore | 2 + Dockerfile | 29 ++++++++ README.md | 142 +++++++++++++++++++++++++++++++++++ ai_classifier.py | 94 ++++++++++++++++++++++++ imap_client.py | 175 ++++++++++++++++++++++++++++++++++++++++++++ main.py | 52 +++++++++++++ processing_logic.py | 134 +++++++++++++++++++++++++++++++++ requirements.txt | 4 + rules.json | 45 ++++++++++++ web/index.html | 35 +++++++++ web/script.js | 85 +++++++++++++++++++++ web/style.css | 116 +++++++++++++++++++++++++++++ 13 files changed, 928 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 ai_classifier.py create mode 100644 imap_client.py create mode 100644 main.py create mode 100644 processing_logic.py create mode 100644 requirements.txt create mode 100644 rules.json create mode 100644 web/index.html create mode 100644 web/script.js create mode 100644 web/style.css diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..394ba9b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +# Ignore Python virtual environment +venv/ + +# Ignore Python cache files +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Ignore IDE/editor-specific files +.vscode/ +.idea/ + +# Ignore local development files +.DS_Store diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..787d17c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Ignore configuration files containing sensitive information +config.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e4bbdc6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# 1. Base Image +# Use a lightweight Python base image +FROM python:3.11-slim + +# 2. Set Working Directory +# Set the working directory inside the container +WORKDIR /app + +# 3. Install Dependencies +# Copy the requirements file first to leverage Docker's layer caching. +# This way, dependencies are only re-installed if requirements.txt changes. +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 4. Copy Application Files +# Copy the rest of the application source code, including the 'web' directory +COPY . . + +# 5. Expose Port +# Expose the port the app runs on +EXPOSE 8000 + +# 6. Add a healthcheck +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/ || exit 1 + +# 7. Set Command +# Define the command to run the application. +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d096572 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +# AI-Powered Mail Automation Engine + +This project provides a framework for automatically processing emails from an IMAP server based on a user-defined set of rules. It can categorize, archive, or delete emails, serving as a powerful personal email assistant. + +## Core Concepts + +The engine works by connecting to an IMAP mail server, fetching unread emails, and processing them according to rules you define in a JSON file. This allows for a highly customizable workflow to manage your inbox. + +### AI-Powered Classification +The standout feature is the ability to use Google's Gemini large language model to classify emails based on their content. You define a list of categories, and the AI will determine the most appropriate one for each email. This allows for much more intelligent and nuanced routing than simple keyword matching. + +### Key Components +- `main.py`: The main entry point that runs the FastAPI web server and provides the API. +- `processing_logic.py`: The core module that handles the email processing workflow, including the rule engine. +- `ai_classifier.py`: The module that communicates with the Gemini API to classify emails. +- `imap_client.py`: A class that handles all communication with the IMAP server. +- `config.json`: Stores your sensitive connection details and API keys. +- `rules.json`: **This is where you define your workflow.** You create rules that match email properties (like sender or subject) or AI classification results, and link them to specific actions. +- `web/`: Contains the HTML, CSS, and JavaScript files for the web UI. + +## The 4-Step Workflow + +Based on the rules you set, the script can perform one of the following actions for each email: +1. **DEVONthink (Archive):** Downloads the email as a `.eml` file and saves it to a specified folder. This folder can be indexed by DEVONthink for automatic import. +2. **CATEGORIZE:** Moves the email to a specific mailbox on the server. +3. **REVIEW:** Moves the email to a "Review" mailbox if it doesn't match any other rule, allowing you to handle it manually. +4. **TRASH:** Moves the email to the Trash. + +## Setup and Usage (Local Development) + +### 1. Configure Credentials (`config.json`) + +Update `config.json` with your mail server details and your Gemini API key. + +```json +{ + "username": "your_email_username", + "password": "your_email_password", + "imap": { + "server": "mail.hyungi.net", + "port": 993 + }, + "gemini_api_key": "YOUR_GEMINI_API_KEY" +} +``` +- You can get a Gemini API key from Google AI Studio. + +### 2. Define Your Workflow (`rules.json`) + +This is the most important step. Open `rules.json` and define your email processing logic. + +To use the AI, first define your categories in the `ai_categories` list. Then, create rules that use the `ai_classification_is` condition. + +```json +{ + "ai_categories": ["청구서", "프로젝트 업데이트", "광고", "개인 용무", "기타"], + "rules": [ + { + "rule_name": "AI 분류: 청구서는 DEVONthink로", + "conditions": { + "ai_classification_is": "청구서" + }, + "action": { + "type": "DEVONTHINK", + "parameters": { + "devonthink_inbox_path": "/path/on/nas/for/devonthink" + } + } + }, + { + "rule_name": "키워드: 중요한 프로젝트", + "conditions": { + "subject_contains": "[Urgent Project]" + }, + "action": { + "type": "CATEGORIZE", + "parameters": { + "move_to_mailbox": "INBOX/Urgent" + } + } + } + ], + "default_action": { + "type": "REVIEW", + "parameters": { + "move_to_mailbox": "INBOX/Review" + } + } +} +``` +- **`ai_categories`**: A list of categories you want the AI to use. +- **`conditions`**: Can be `subject_contains`, `from_contains`, or `ai_classification_is`. +- **`action`**: The action to perform (`DEVONTHINK`, `CATEGORIZE`, `TRASH`, `REVIEW`). + +### 3. Installation + +This project uses a virtual environment to manage dependencies. + +**Create the environment (only once):** +```bash +python3 -m venv venv +``` + +**Activate the environment and install packages:** +(Activate the environment every time you open a new terminal) +```bash +source venv/bin/activate +pip install -r requirements.txt +``` + +### 4. Running the Web Server + +With the virtual environment activated, run the FastAPI server: + +```bash +uvicorn main:app --host 0.0.0.0 --port 8000 --reload +``` +- The `--reload` flag automatically restarts the server when you make code changes. +- Access the web UI by opening `http://localhost:8000` in your browser. + +## Docker + +You can also build and run this application as a Docker container. + +### 1. Build the Image +```bash +docker build -t mail-manager . +``` + +### 2. Run the Container +```bash +# Make sure your config.json and rules.json are correctly filled out +docker run --rm -p 8000:8000 -v $(pwd)/config.json:/app/config.json -v $(pwd)/rules.json:/app/rules.json mail-manager +``` +- `-p 8000:8000`: This maps port 8000 on your local machine to port 8000 in the container, making the web UI accessible at `http://localhost:8000`. +- `-v`: Using volumes mounts your local configuration files into the container, making it easy to manage them without rebuilding the image. + +## Roadmap + +- **Advanced Rule Conditions:** Add more complex conditions to the rule engine (e.g., regex matching, checking for attachments). +- **Improved Web UI:** Enhance the web interface for a more user-friendly rule editing experience (e.g., with forms instead of raw JSON). +- **Scheduler:** Implement a scheduler within the application to run the email processing task automatically at regular intervals. diff --git a/ai_classifier.py b/ai_classifier.py new file mode 100644 index 0000000..0a2c3c4 --- /dev/null +++ b/ai_classifier.py @@ -0,0 +1,94 @@ +import google.generativeai as genai +import os + +def classify_email(api_key, email_content, categories): + """ + Classifies the given email content into one of the provided categories using the Gemini API. + + Args: + api_key (str): The Gemini API key. + email_content (str): The content of the email (subject + body). + categories (list): A list of strings representing the possible categories. + + Returns: + str: The chosen category, or None if classification fails. + """ + if not api_key or api_key == "YOUR_GEMINI_API_KEY": + print("! 경고: Gemini API 키가 설정되지 않았습니다. AI 분류를 건너뜁니다.") + return None + + try: + genai.configure(api_key=api_key) + except Exception as e: + print(f"Gemini API 설정 중 오류 발생: {e}") + return None + + # For safety, let's use a model that's less likely to refuse classification + model = genai.GenerativeModel('gemini-1.5-flash') + + # Construct the prompt + prompt = f""" + Analyze the following email content and classify it into one of the following categories. + Your response must be ONLY ONE of the category names provided. Do not add any extra text, explanation, or punctuation. + + Categories: {', '.join(categories)} + + Email Content: + --- + {email_content} + --- + + Category: + """ + + try: + response = model.generate_content(prompt) + # The response text should be one of the categories + result_category = response.text.strip() + + # Validate that the model returned a valid category + if result_category in categories: + print(f"-> AI 분류 결과: '{result_category}'") + return result_category + else: + print(f"! 경고: AI가 유효하지 않은 카테고리 '{result_category}'를 반환했습니다.") + # Fallback or simply return None + return None + + except Exception as e: + print(f"Gemini API 호출 중 오류 발생: {e}") + return None + +if __name__ == '__main__': + # --- 사용 예시 --- + # 1. 아래 YOUR_API_KEY를 실제 키로 변경하세요. + # 2. 터미널에서 `pip install google-generativeai` 를 실행하세요. + # 3. `python ai_classifier.py` 명령으로 테스트하세요. + + test_api_key = os.environ.get("GEMINI_API_KEY", "YOUR_GEMINI_API_KEY") # 실제 키로 교체 필요 + + test_email = """ + Subject: Invoice INV-2024-001 from Office Supplies Co. + + Dear Customer, + + Please find attached your invoice for the recent purchase of office supplies. + The total amount due is $125.50. + + Thank you for your business. + + Sincerely, + Office Supplies Co. + """ + + test_categories = ["청구서", "프로젝트 업데이트", "광고", "개인 용무"] + + if test_api_key != "YOUR_GEMINI_API_KEY": + classified_category = classify_email(test_api_key, test_email, test_categories) + + if classified_category: + print(f"\n최종 분류: {classified_category}") + else: + print("\n분류에 실패했습니다.") + else: + print("\n테스트를 위해 'YOUR_GEMINI_API_KEY'를 실제 API 키로 바꿔주세요.") diff --git a/imap_client.py b/imap_client.py new file mode 100644 index 0000000..8cc5f47 --- /dev/null +++ b/imap_client.py @@ -0,0 +1,175 @@ +import imaplib +import email +from email.header import decode_header +import os + +class ImapMailClient: + def __init__(self, server, port, username, password): + self.server = server + self.port = port + self.username = username + self.password = password + self.mail = None + + def connect(self): + """IMAP 서버에 연결하고 로그인합니다.""" + try: + self.mail = imaplib.IMAP4_SSL(self.server, self.port) + self.mail.login(self.username, self.password) + print("IMAP 서버에 성공적으로 연결했습니다.") + return True + except Exception as e: + print(f"IMAP 연결 오류: {e}") + return False + + def fetch_unread_emails(self, mailbox='inbox'): + """지정된 메일함에서 읽지 않은 모든 이메일의 UID를 가져옵니다.""" + if not self.mail: + print("오류: 먼저 connect()를 호출해야 합니다.") + return [] + + try: + self.mail.select(mailbox) + status, messages = self.mail.search(None, 'UNSEEN') + if status != 'OK': + print(f"'{mailbox}' 메일함에서 UNSEEN 이메일을 검색하지 못했습니다.") + return [] + + email_uids = messages[0].split() + print(f"'{mailbox}'에서 {len(email_uids)}개의 새로운 메일을 발견했습니다.") + return email_uids + except Exception as e: + print(f"읽지 않은 이메일을 가져오는 중 오류 발생: {e}") + return [] + + def fetch_email(self, uid): + """주어진 UID를 가진 이메일의 상세 정보를 가져옵니다 (보낸사람, 제목, 본문).""" + if not self.mail: + return None + + try: + # UID로 메일 가져오기 + status, msg_data = self.mail.fetch(uid, '(RFC822)') + if status != 'OK': + return None + + for response_part in msg_data: + if isinstance(response_part, tuple): + msg = email.message_from_bytes(response_part[1]) + + # 헤더 디코딩 + subject, encoding = decode_header(msg["subject"])[0] + if isinstance(subject, bytes): + subject = subject.decode(encoding if encoding else "utf-8") + + from_ = msg.get("From") + + # 본문 내용 추출 + body = "" + if msg.is_multipart(): + for part in msg.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get("Content-Disposition")) + if content_type == "text/plain" and "attachment" not in content_disposition: + payload = part.get_payload(decode=True) + charset = part.get_content_charset() + body = payload.decode(charset if charset else "utf-8") + break + else: + payload = msg.get_payload(decode=True) + charset = msg.get_content_charset() + body = payload.decode(charset if charset else "utf-8") + + return { + "uid": uid, + "from": from_, + "subject": subject, + "body": body.strip(), + "raw": response_part[1] # .eml 파일 저장을 위한 원본 데이터 + } + return None + except Exception as e: + print(f"UID {uid} 이메일을 가져오는 중 오류 발생: {e}") + return None + + def move_email(self, uid, target_mailbox): + """이메일을 다른 메일함으로 이동합니다.""" + if not self.mail: + return False + try: + # 메일을 대상 메일함으로 복사 + self.mail.copy(uid, target_mailbox) + # 원본 메일을 삭제 플래그 처리 + self.mail.store(uid, '+FLAGS', '\Deleted') + # 메일함에서 영구 삭제 + self.mail.expunge() + print(f"메일(UID: {uid})을 '{target_mailbox}'(으)로 이동했습니다.") + return True + except Exception as e: + print(f"메일 이동 중 오류 발생: {e}") + return False + + def delete_email(self, uid): + """이메일을 삭제합니다 (휴지통으로 이동).""" + # Synology MailPlus에서는 'Deleted' 플래그가 휴지통으로 이동시킵니다. + return self.move_email(uid, 'Trash') + + def download_email(self, email_data, directory): + """이메일 원본 데이터를 .eml 파일로 저장합니다.""" + if not os.path.exists(directory): + os.makedirs(directory) + + # 파일명으로 사용할 수 없는 문자 제거 + safe_subject = "".join([c for c in email_data['subject'] if c.isalpha() or c.isdigit() or c==' ']).rstrip() + filename = f"{email_data['uid'].decode()}_{safe_subject}.eml" + filepath = os.path.join(directory, filename) + + try: + with open(filepath, 'wb') as f: + f.write(email_data['raw']) + print(f"메일을 '{filepath}'에 다운로드했습니다.") + return filepath + except Exception as e: + print(f"메일 다운로드 중 오류 발생: {e}") + return None + + def close(self): + """서버 연결을 종료합니다.""" + if self.mail: + self.mail.close() + self.mail.logout() + print("IMAP 서버 연결을 종료했습니다.") + +if __name__ == '__main__': + # --- 사용 예시 --- + # 1. config.json 파일에 imap 정보를 정확히 입력했는지 확인하세요. + import json + with open('config.json') as f: + config = json.load(f) + + imap_client = ImapMailClient( + server=config['imap']['server'], + port=config['imap']['port'], + username=config['username'], + password=config['password'] + ) + + if imap_client.connect(): + unread_uids = imap_client.fetch_unread_emails() + if unread_uids: + # 첫 번째 안 읽은 메일 테스트 + first_uid = unread_uids[0] + email_details = imap_client.fetch_email(first_uid) + if email_details: + print("\n--- 첫 번째 메일 정보 ---") + print(f" 보낸사람: {email_details['from']}") + print(f" 제목: {email_details['subject']}") + print("------------------------\n") + + # 테스트: 'Test' 메일함으로 이동 + # imap_client.move_email(first_uid, 'Test') + + # 테스트: .eml 파일로 다운로드 + # imap_client.download_email(email_details, 'downloaded_mails') + + imap_client.close() diff --git a/main.py b/main.py new file mode 100644 index 0000000..af54c84 --- /dev/null +++ b/main.py @@ -0,0 +1,52 @@ +from fastapi import FastAPI, Request, BackgroundTasks +from fastapi.staticfiles import StaticFiles +import json +from processing_logic import run_email_processing + +from fastapi.responses import FileResponse + +app = FastAPI() + +# Mount the 'web' directory to serve static files (CSS, JS) +app.mount("/static", StaticFiles(directory="web"), name="static") + +@app.get("/") +def read_root(): + """Serves the main index.html file.""" + return FileResponse('web/index.html') + +@app.get("/api/rules") +def get_rules(): + """Reads and returns the current rules from rules.json.""" + try: + with open('rules.json', 'r', encoding='utf-8') as f: + return json.load(f) + except FileNotFoundError: + return {"error": "rules.json not found"} + except json.JSONDecodeError: + return {"error": "Could not parse rules.json"} + +@app.post("/api/rules") +async def save_rules(request: Request): + """Receives new rules as JSON and overwrites rules.json.""" + try: + new_rules = await request.json() + with open('rules.json', 'w', encoding='utf-8') as f: + json.dump(new_rules, f, indent=2, ensure_ascii=False) + return {"message": "Rules saved successfully."} + except json.JSONDecodeError: + return {"error": "Invalid JSON format received."} + except Exception as e: + return {"error": f"An error occurred: {e}"} + +@app.post("/api/run-processing") +async def trigger_processing(background_tasks: BackgroundTasks): + """Triggers the email processing task in the background.""" + background_tasks.add_task(run_email_processing) + return {"message": "Email processing task has been started in the background."} + + +if __name__ == "__main__": + import uvicorn + # This is for local development. For production, you'd run uvicorn directly. + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/processing_logic.py b/processing_logic.py new file mode 100644 index 0000000..afe77a0 --- /dev/null +++ b/processing_logic.py @@ -0,0 +1,134 @@ +import json +from imap_client import ImapMailClient +from ai_classifier import classify_email + +def load_config(path='config.json'): + """설정 파일을 로드합니다.""" + print(f"'{path}' 파일에서 설정 로드 중...") + with open(path, 'r', encoding='utf-8') as f: + return json.load(f) + +def load_rules(path='rules.json'): + """규칙 파일을 로드합니다.""" + print(f"'{path}' 파일에서 규칙 로드 중...") + with open(path, 'r', encoding='utf-8') as f: + return json.load(f) + +def check_conditions(email_details, conditions): + """이메일이 주어진 조건을 만족하는지 확인합니다.""" + for key, value in conditions.items(): + if key == 'from_contains': + if value.lower() not in email_details.get('from', '').lower(): + return False + elif key == 'subject_contains': + if value.lower() not in email_details.get('subject', '').lower(): + return False + elif key == 'ai_classification_is': + if value != email_details.get('ai_category'): + return False + # 여기에 'to_contains', 'body_contains' 등 다른 조건을 추가할 수 있습니다. + return True + +def process_emails(imap_client, rules_config, gemini_api_key): + """ + 읽지 않은 이메일을 가져와서 규칙에 따라 처리합니다. + """ + unread_uids = imap_client.fetch_unread_emails() + ai_categories = rules_config.get('ai_categories', []) + + for uid in unread_uids: + email_details = imap_client.fetch_email(uid) + if not email_details: + continue + + print(f"\n[처리 시작] 메일 UID: {uid.decode()}, 제목: {email_details['subject']}") + + # AI 분류 실행 (필요한 경우) + # AI 규칙이 하나라도 있을 때만 API를 호출하도록 최적화할 수 있음 + email_details['ai_category'] = None + if ai_categories: + email_content_for_ai = f"Subject: {email_details['subject']}\n\nBody:\n{email_details['body']}" + email_details['ai_category'] = classify_email(gemini_api_key, email_content_for_ai, ai_categories) + + # 규칙 검사 + matched_rule = None + for rule in rules_config.get('rules', []): + if check_conditions(email_details, rule.get('conditions', {})): + matched_rule = rule + break + + action_info = None + if matched_rule: + print(f"-> 규칙 '{matched_rule.get('rule_name', '이름 없음')}'에 해당합니다.") + action_info = matched_rule.get('action') + else: + print("-> 일치하는 규칙이 없어 기본 행동을 실행합니다.") + action_info = rules_config.get('default_action') + + if action_info: + execute_action(imap_client, email_details, action_info) + else: + print("-> 실행할 행동이 정의되지 않았습니다.") + + +def execute_action(client, email, action): + """규칙에 따른 행동을 실행합니다.""" + action_type = action.get('type') + params = action.get('parameters', {}) + uid = email['uid'] + + if action_type == 'DEVONTHINK': + print("-> 행동: DEVONthink로 저장") + inbox_path = params.get('devonthink_inbox_path') + if inbox_path: + client.download_email(email, inbox_path) + # 다운로드 후 메일 삭제 또는 다른 곳으로 이동 등 추가 행동을 원하면 여기에 구현 + client.delete_email(uid) # 예: 다운로드 후 휴지통으로 이동 + else: + print("! 경고: 'devonthink_inbox_path'가 rules.json에 지정되지 않았습니다.") + + elif action_type == 'CATEGORIZE': + target_mailbox = params.get('move_to_mailbox') + # TODO: IMAP으로는 Synology의 '레이블'을 직접 제어하기 어려움. + # 대신 메일함으로 이동하는 것으로 대체. + print(f"-> 행동: '{target_mailbox}' 메일함으로 이동") + if target_mailbox: + client.move_email(uid, target_mailbox) + else: + print("! 경고: 'move_to_mailbox'가 rules.json에 지정되지 않았습니다.") + + elif action_type == 'REVIEW': + target_mailbox = params.get('move_to_mailbox') + print(f"-> 행동: '{target_mailbox}' 메일함으로 이동 (검토 필요)") + if target_mailbox: + client.move_email(uid, target_mailbox) + else: + print("! 경고: 'move_to_mailbox'가 rules.json에 지정되지 않았습니다.") + + elif action_type == 'TRASH': + print("-> 행동: 휴지통으로 이동") + client.delete_email(uid) + + else: + print(f"! 경고: 알 수 없는 행동 타입 '{action_type}' 입니다.") + + +def run_email_processing(): + """메인 실행 함수""" + config = load_config() + rules_config = load_rules() + + imap_client = ImapMailClient( + server=config['imap']['server'], + port=config['imap']['port'], + username=config['username'], + password=config['password'] + ) + + gemini_api_key = config.get('gemini_api_key') + + if imap_client.connect(): + process_emails(imap_client, rules_config, gemini_api_key) + imap_client.close() + else: + print("프로세스를 시작할 수 없습니다. IMAP 연결 정보를 확인하세요.") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8d8a8bd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests +fastapi +uvicorn[standard] +google-generativeai diff --git a/rules.json b/rules.json new file mode 100644 index 0000000..652aae3 --- /dev/null +++ b/rules.json @@ -0,0 +1,45 @@ +{ + "ai_categories": ["청구서", "프로젝트 업데이트", "광고", "개인 용무", "기타"], + "rules": [ + { + "rule_name": "AI 분류: 청구서 보관", + "conditions": { + "ai_classification_is": "청구서" + }, + "action": { + "type": "DEVONTHINK", + "parameters": { + "devonthink_inbox_path": "/path/to/your/devonthink_inbox" + } + } + }, + { + "rule_name": "키워드: Project-A 관련 메일", + "conditions": { + "from_contains": "manager@example.com", + "subject_contains": "[Project-A]" + }, + "action": { + "type": "CATEGORIZE", + "parameters": { + "move_to_mailbox": "프로젝트/Project-A" + } + } + }, + { + "rule_name": "키워드: 광고 메일 삭제", + "conditions": { + "from_contains": "ad@spam.com" + }, + "action": { + "type": "TRASH" + } + } + ], + "default_action": { + "type": "REVIEW", + "parameters": { + "move_to_mailbox": "검토할 메일" + } + } +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..c9b754e --- /dev/null +++ b/web/index.html @@ -0,0 +1,35 @@ + + + + + + AI Mail Manager - Rule Editor + + + + +
+
+

Mail Processing Rule Editor

+

Edit the rules in JSON format below. Click 'Save Rules' to apply them.

+
+ +
+
+ +
+
+ + +
+
+
+ +
+

AI Mail Server

+
+
+ + + + diff --git a/web/script.js b/web/script.js new file mode 100644 index 0000000..c4b04d6 --- /dev/null +++ b/web/script.js @@ -0,0 +1,85 @@ +document.addEventListener('DOMContentLoaded', () => { + const editor = document.getElementById('rules-editor'); + const saveButton = document.getElementById('save-button'); + const runButton = document.getElementById('run-button'); + const statusMessage = document.getElementById('status-message'); + + // Function to display status messages + const showStatus = (message, isError = false) => { + statusMessage.textContent = message; + statusMessage.className = isError ? 'error' : 'success'; + }; + + // 1. Fetch initial rules on page load + fetch('/api/rules') + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.error) { + showStatus(`Error loading rules: ${data.error}`, true); + } else { + // Pretty-print the JSON with 2 spaces + editor.value = JSON.stringify(data, null, 2); + } + }) + .catch(error => { + showStatus('Failed to fetch rules. Is the server running?', true); + console.error('Fetch error:', error); + }); + + // 2. Add event listener for the Save button + saveButton.addEventListener('click', () => { + let rulesContent; + try { + // We parse it first to ensure it's valid JSON before sending + rulesContent = JSON.parse(editor.value); + } catch (error) { + showStatus('Invalid JSON format. Please correct it before saving.', true); + return; + } + + fetch('/api/rules', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(rulesContent), + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + showStatus(`Error saving rules: ${data.error}`, true); + } else { + showStatus(data.message || 'Rules saved successfully!', false); + } + }) + .catch(error => { + showStatus('Failed to save rules. An unknown error occurred.', true); + console.error('Save error:', error); + }); + }); + + // 3. Add event listener for the Run button + runButton.addEventListener('click', () => { + showStatus('Requesting to start email processing...', false); + fetch('/api/run-processing', { + method: 'POST', + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + showStatus(`Error starting process: ${data.error}`, true); + } else { + showStatus(data.message || 'Processing started in the background.', false); + } + }) + .catch(error => { + showStatus('Failed to start processing. An unknown error occurred.', true); + console.error('Run error:', error); + }); + }); +}); diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..32d65ef --- /dev/null +++ b/web/style.css @@ -0,0 +1,116 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background-color: #f4f7f9; + color: #333; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.container { + width: 80%; + max-width: 900px; + background-color: #fff; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + padding: 2rem; +} + +header { + text-align: center; + border-bottom: 1px solid #e0e0e0; + padding-bottom: 1rem; + margin-bottom: 1.5rem; +} + +h1 { + color: #2c3e50; + margin: 0; +} + +p { + color: #7f8c8d; +} + +.editor-container { + border: 1px solid #ccc; + border-radius: 4px; + overflow: hidden; +} + +#rules-editor { + width: 100%; + height: 400px; + border: none; + padding: 1rem; + font-family: "SF Mono", "Fira Code", "Courier New", monospace; + font-size: 14px; + line-height: 1.5; + resize: vertical; + box-sizing: border-box; +} + +.actions { + display: flex; + justify-content: flex-end; + gap: 1rem; + margin-top: 1.5rem; +} + +button { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +#save-button { + background-color: #3498db; + color: white; +} + +#save-button:hover { + background-color: #2980b9; +} + +#run-button { + background-color: #2ecc71; + color: white; +} + +#run-button:hover { + background-color: #27ae60; +} + +#status-message { + margin-top: 1rem; + padding: 0.75rem; + border-radius: 4px; + text-align: center; + font-weight: 500; + display: none; /* Hidden by default */ +} + +#status-message.success { + background-color: #e8f5e9; + color: #2e7d32; + display: block; +} + +#status-message.error { + background-color: #ffebee; + color: #c62828; + display: block; +} + +footer { + text-align: center; + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid #e0e0e0; + color: #bdc3c7; +}