feat: Implement AI classification and Web UI, refactor to IMAP

This commit is contained in:
hyungi
2025-12-29 15:50:35 +09:00
commit d3c9cd2c7f
13 changed files with 928 additions and 0 deletions

15
.dockerignore Normal file
View File

@@ -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

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Ignore configuration files containing sensitive information
config.json

29
Dockerfile Normal file
View File

@@ -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"]

142
README.md Normal file
View File

@@ -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.

94
ai_classifier.py Normal file
View File

@@ -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 키로 바꿔주세요.")

175
imap_client.py Normal file
View File

@@ -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()

52
main.py Normal file
View File

@@ -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)

134
processing_logic.py Normal file
View File

@@ -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 연결 정보를 확인하세요.")

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
requests
fastapi
uvicorn[standard]
google-generativeai

45
rules.json Normal file
View File

@@ -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": "검토할 메일"
}
}
}

35
web/index.html Normal file
View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Mail Manager - Rule Editor</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<header>
<h1>Mail Processing Rule Editor</h1>
<p>Edit the rules in JSON format below. Click 'Save Rules' to apply them.</p>
</header>
<main>
<div class="editor-container">
<textarea id="rules-editor" spellcheck="false"></textarea>
</div>
<div class="actions">
<button id="save-button">Save Rules</button>
<button id="run-button">Run Processing Now</button>
</div>
<div id="status-message"></div>
</main>
<footer>
<p>AI Mail Server</p>
</footer>
</div>
<script src="/static/script.js"></script>
</body>
</html>

85
web/script.js Normal file
View File

@@ -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);
});
});
});

116
web/style.css Normal file
View File

@@ -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;
}