add agent workspace
This commit is contained in:
16
scripts/email_processor/config.json
Normal file
16
scripts/email_processor/config.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"imap": {
|
||||
"host": "imap.migadu.com",
|
||||
"port": 993,
|
||||
"email": "youlu@luyanxin.com",
|
||||
"password": "kDkNau2r7m.hV!uk*D4Yr8mC7Dyjx9T"
|
||||
},
|
||||
"ollama": {
|
||||
"host": "http://localhost:11434",
|
||||
"model": "qwen3:4b"
|
||||
},
|
||||
"rules": {
|
||||
"max_body_length": 1000,
|
||||
"check_unseen_only": true
|
||||
}
|
||||
}
|
||||
52
scripts/email_processor/data/pending_emails.json
Normal file
52
scripts/email_processor/data/pending_emails.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"msg_f1d43ea3": {
|
||||
"imap_uid": "2",
|
||||
"subject": "Delivered: \"Voikinfo Bottom Gusset Bags...\"",
|
||||
"sender": "\"Amazon.com - order-update(a)amazon.com\"\r\n <order-update_at_amazon_com_posyo@simplelogin.co>",
|
||||
"recipient": "sho.amazon@ylu17.com",
|
||||
"summary": "Your Amazon package (order #114-1496788-7649829) was delivered today to Argo, Los Angeles, CA and left near the front door or porch.",
|
||||
"email_date": "Wed, 18 Feb 2026 04:15:24 +0000",
|
||||
"status": "pending",
|
||||
"found_at": "2026-02-18T16:18:42.347538"
|
||||
},
|
||||
"msg_60c56a87": {
|
||||
"imap_uid": "3",
|
||||
"subject": "=?UTF-8?b?5L2V5LiN5ruh6Laz6Ieq5bex55qE5Y+j6IW55LmL5qyy?=",
|
||||
"sender": "\"Uber Eats - uber(a)uber.com\" <uber_at_uber_com_kjwzyhxn@simplelogin.co>",
|
||||
"recipient": "uber@ylu17.com",
|
||||
"summary": "Uber Eats has sent a notification that the user's order is ready for pickup.",
|
||||
"email_date": "Wed, 18 Feb 2026 11:36:59 +0000",
|
||||
"status": "pending",
|
||||
"found_at": "2026-02-18T08:05:56.594842"
|
||||
},
|
||||
"msg_ebd24205": {
|
||||
"imap_uid": "4",
|
||||
"subject": "Your order has been shipped (or closed if combined/delivered).",
|
||||
"sender": "\"cd(a)woodenswords.com\"\r\n <cd_at_woodenswords_com_xivwijojc@simplelogin.co>",
|
||||
"recipient": "mail@luyx.org",
|
||||
"summary": "This email confirms that your order has been shipped or closed (if combined/delivered).",
|
||||
"email_date": "Wed, 18 Feb 2026 16:07:58 +0000",
|
||||
"status": "pending",
|
||||
"found_at": "2026-02-18T12:01:19.048091"
|
||||
},
|
||||
"msg_fa73b3bd": {
|
||||
"imap_uid": "6",
|
||||
"subject": "=?UTF-8?Q?Yanxin,_I=E2=80=99m_still_waiting_for_your_response?=",
|
||||
"sender": "\"Arslan (via LinkedIn) - messages-noreply(a)linkedin.com\"\r\n <messages-noreply_at_linkedin_com_ajpnalmwp@simplelogin.co>",
|
||||
"recipient": "Yanxin Lu <acc.linkedin@ylu17.com>",
|
||||
"summary": "Arslan Ahmed, a Senior AI | ML | Full Stack Engineer from Ilford, invited you to connect on February 11, 2026 at 10:08 PM and is waiting for your response.",
|
||||
"email_date": "Wed, 18 Feb 2026 18:53:45 +0000 (UTC)",
|
||||
"status": "pending",
|
||||
"found_at": "2026-02-18T12:04:34.602407"
|
||||
},
|
||||
"msg_59f23736": {
|
||||
"imap_uid": "1",
|
||||
"subject": "New Software Engineer jobs that match your profile",
|
||||
"sender": "\"LinkedIn - jobs-noreply(a)linkedin.com\"\r\n <jobs-noreply_at_linkedin_com_zuwggfxh@simplelogin.co>",
|
||||
"recipient": "Yanxin Lu <acc.linkedin@ylu17.com>",
|
||||
"summary": "LinkedIn has notified the user of new software engineering jobs that match their profile and includes a link to update their top card.",
|
||||
"email_date": "Wed, 18 Feb 2026 02:07:12 +0000 (UTC)",
|
||||
"status": "pending",
|
||||
"found_at": "2026-02-18T16:16:00.784822"
|
||||
}
|
||||
}
|
||||
50
scripts/email_processor/logs/2026-02-15.log
Normal file
50
scripts/email_processor/logs/2026-02-15.log
Normal file
@@ -0,0 +1,50 @@
|
||||
[2026-02-15 21:14:02] KEPT: Please confirm your mailbox youlu@luyanxin.com
|
||||
From: "noreply@simplelogin.io" <noreply@simplelogin.io>
|
||||
Analysis: KEEP: Legitimate service confirmation email for mailbox addition (not promotional)
|
||||
|
||||
[2026-02-15 21:15:04] KEPT: =?utf-8?B?RndkOiBHZXQgMTAlIG9mZiB5b3VyIG5leHQgb3JkZXIg4pyF?=
|
||||
From: "Yanxin Lu - crac1017(a)hotmail.com"
|
||||
<crac1017_at_hotmail_com_fndbbu@simplelogin.co>
|
||||
Analysis: KEEP: error - HTTPConnectionPool(host='localhost', port=11434): Read timed out. (read timeout=60)
|
||||
|
||||
[2026-02-15 21:15:37] KEPT:
|
||||
=?utf-8?B?RndkOiDigJxzb2Z0d2FyZSBlbmdpbmVlcuKAnTogTWljcm9
|
||||
From: "Yanxin Lu - crac1017(a)hotmail.com"
|
||||
<crac1017_at_hotmail_com_fndbbu@simplelogin.co>
|
||||
Analysis: KEEP: LinkedIn job alert notification for subscribed job search (not promotional)
|
||||
|
||||
[2026-02-15 21:15:52] KEPT: Fwd: Your receipt from OpenRouter, Inc #2231-9732
|
||||
From: "Yanxin Lu - crac1017(a)hotmail.com"
|
||||
<crac1017_at_hotmail_com_fndbbu@simplelogin.co>
|
||||
Analysis: KEEP: This is a legitimate receipt for a payment made to OpenRouter, Inc (a known AI service provider), not promotional content.
|
||||
|
||||
[2026-02-15 21:16:10] KEPT: Fwd: Your ChatGPT code is 217237
|
||||
From: "Yanxin Lu - crac1017(a)hotmail.com"
|
||||
<crac1017_at_hotmail_com_fndbbu@simplelogin.co>
|
||||
Analysis: KEEP: Legitimate security verification code from OpenAI (standard login confirmation)
|
||||
|
||||
[2026-02-15 22:49:44] KEPT (69.0s): =?UTF-8?B?5rWL6K+V6YKu5Lu2?=
|
||||
From: Yanxin Lu <lyx@luyanxin.com>
|
||||
Analysis: KEEP: Test email for delivery verification
|
||||
|
||||
From: Yanxin Lu <lyx@luyanxin.com>
|
||||
Analysis: KEEP: Test email for delivery verification
|
||||
|
||||
[2026-02-15 22:57:03] MOVED_TO_TRASH (68.5s): =?utf-8?B?RndkOiBHZXQgMTAlIG9mZiB5b3VyIG5leHQgb3JkZXIg4pyF?=
|
||||
From: "Yanxin Lu - crac1017(a)hotmail.com"
|
||||
<crac1017_at_hotmail_com_fndbbu@simplelogin.co>
|
||||
Analysis: AD: Forwarded Uber promotional offer
|
||||
|
||||
From: "Yanxin Lu - crac1017(a)hotmail.com"
|
||||
<crac1017_at_hotmail_com_fndbbu@simplelogin.co>
|
||||
Analysis: AD: Forwarded Uber promotional offer
|
||||
|
||||
[2026-02-15 23:00:09] KEPT (120.1s): Fwd: Your ChatGPT code is 217237
|
||||
From: "Yanxin Lu - crac1017(a)hotmail.com"
|
||||
<crac1017_at_hotmail_com_fndbbu@simplelogin.co>
|
||||
Analysis: KEEP: error - HTTPConnectionPool(host='localhost', port=11434): Read timed out. (read timeout=120)
|
||||
|
||||
From: "Yanxin Lu - crac1017(a)hotmail.com"
|
||||
<crac1017_at_hotmail_com_fndbbu@simplelogin.co>
|
||||
Analysis: KEEP: error - HTTPConnectionPool(host='localhost', port=11434): Read timed out. (read timeout=120)
|
||||
|
||||
29
scripts/email_processor/logs/2026-02-18.log
Normal file
29
scripts/email_processor/logs/2026-02-18.log
Normal file
@@ -0,0 +1,29 @@
|
||||
[2026-02-18 08:04:26] ADDED_TO_PENDING (msg_f1d43ea3) (108.6s): Delivered: "Voikinfo Bottom Gusset Bags..."
|
||||
From: "Amazon.com - order-update(a)amazon.com"
|
||||
<order-update_at_amazon_com_posyo@simplelogin.co>
|
||||
Analysis: KEEP: Standard delivery confirmation from Amazon
|
||||
|
||||
[2026-02-18 08:05:56] ADDED_TO_PENDING (msg_60c56a87) (88.0s): =?UTF-8?b?5L2V5LiN5ruh6Laz6Ieq5bex55qE5Y+j6IW55LmL5qyy?=
|
||||
From: "Uber Eats - uber(a)uber.com" <uber_at_uber_com_kjwzyhxn@simplelogin.co>
|
||||
Analysis: KEEP: The decoded subject line "Your Uber Eats order is ready!" indicates a transactional order update, not an advertisement.
|
||||
|
||||
[2026-02-18 12:01:19] ADDED_TO_PENDING (msg_ebd24205) (66.7s): Your order has been shipped (or closed if combined/delivered
|
||||
From: "cd(a)woodenswords.com"
|
||||
<cd_at_woodenswords_com_xivwijojc@simplelogin.co>
|
||||
Analysis: KEEP: System-generated shipping update notification from an e-commerce store, not promotional content.
|
||||
|
||||
[2026-02-18 12:03:36] MOVED_TO_TRASH (133.4s): =?UTF-8?Q?=E2=80=9Csoftware_engineer=E2=80=9D:_Snap_Inc._-_S
|
||||
From: "LinkedIn Job Alerts - jobalerts-noreply(a)linkedin.com"
|
||||
<jobalerts-noreply_at_linkedin_com_cnrlhok@simplelogin.co>
|
||||
Analysis: AD: This email is a promotional job alert notification from LinkedIn's service for users who have set up job preferences.
|
||||
|
||||
[2026-02-18 12:04:34] ADDED_TO_PENDING (msg_fa73b3bd) (57.3s): =?UTF-8?Q?Yanxin,_I=E2=80=99m_still_waiting_for_your_respons
|
||||
From: "Arslan (via LinkedIn) - messages-noreply(a)linkedin.com"
|
||||
<messages-noreply_at_linkedin_com_ajpnalmwp@simplelogin.co>
|
||||
Analysis: KEEP: This is a standard LinkedIn connection request notification with no promotional content, discounts, or advertisements—only a reminder of an existing invitation.
|
||||
|
||||
[2026-02-18 16:18:42] ADDED_TO_PENDING (msg_f1d43ea3) (102.1s): Delivered: "Voikinfo Bottom Gusset Bags..."
|
||||
From: "Amazon.com - order-update(a)amazon.com"
|
||||
<order-update_at_amazon_com_posyo@simplelogin.co>
|
||||
Analysis: KEEP: Standard delivery confirmation from Amazon, not a promotional message.
|
||||
|
||||
295
scripts/email_processor/main.py
Normal file
295
scripts/email_processor/main.py
Normal file
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Email Processor - Auto filter ads using local Qwen3
|
||||
Moves ad emails to Trash folder (not permanently deleted)
|
||||
"""
|
||||
|
||||
import json
|
||||
import imaplib
|
||||
import email
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Config
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
CONFIG_FILE = SCRIPT_DIR / "config.json"
|
||||
LOGS_DIR = SCRIPT_DIR / "logs"
|
||||
DATA_DIR = SCRIPT_DIR / "data"
|
||||
PENDING_FILE = DATA_DIR / "pending_emails.json"
|
||||
|
||||
def load_config():
|
||||
"""Load configuration"""
|
||||
with open(CONFIG_FILE) as f:
|
||||
return json.load(f)
|
||||
|
||||
def connect_imap(config):
|
||||
"""Connect to IMAP server"""
|
||||
imap_config = config['imap']
|
||||
mail = imaplib.IMAP4_SSL(imap_config['host'], imap_config['port'])
|
||||
mail.login(imap_config['email'], imap_config['password'])
|
||||
return mail
|
||||
|
||||
def get_unseen_emails(mail):
|
||||
"""Get list of unseen email IDs"""
|
||||
mail.select('INBOX')
|
||||
_, search_data = mail.search(None, 'UNSEEN')
|
||||
email_ids = search_data[0].split()
|
||||
return email_ids
|
||||
|
||||
def fetch_email(mail, email_id):
|
||||
"""Fetch email content"""
|
||||
_, msg_data = mail.fetch(email_id, '(RFC822)')
|
||||
raw_email = msg_data[0][1]
|
||||
msg = email.message_from_bytes(raw_email)
|
||||
|
||||
# Extract subject
|
||||
subject = msg['Subject'] or '(No Subject)'
|
||||
|
||||
# Extract sender
|
||||
sender = msg['From'] or '(Unknown)'
|
||||
|
||||
# Extract recipient
|
||||
recipient = msg['To'] or '(Unknown)'
|
||||
|
||||
# Extract date
|
||||
date = msg['Date'] or datetime.now().isoformat()
|
||||
|
||||
# Extract body
|
||||
body = ""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == "text/plain":
|
||||
try:
|
||||
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
|
||||
break
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
body = msg.get_payload(decode=True).decode('utf-8', errors='ignore')
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
'id': email_id,
|
||||
'subject': subject,
|
||||
'sender': sender,
|
||||
'recipient': recipient,
|
||||
'date': date,
|
||||
'body': body[:300] # Limit body length
|
||||
}
|
||||
|
||||
def analyze_with_qwen3(email_data, config):
|
||||
"""Analyze email with local Qwen3 using official library"""
|
||||
import ollama
|
||||
import time
|
||||
|
||||
prompt = f"""Analyze this email and provide two pieces of information:
|
||||
|
||||
1. Is this an advertisement/promotional email?
|
||||
2. Summarize the email in one sentence
|
||||
|
||||
Email details:
|
||||
Subject: {email_data['subject']}
|
||||
Sender: {email_data['sender']}
|
||||
Body: {email_data['body'][:300]}
|
||||
|
||||
Respond in this exact format:
|
||||
IsAD: [YES or NO]
|
||||
Summary: [one sentence summary]
|
||||
Reason: [brief explanation]
|
||||
"""
|
||||
|
||||
start_time = time.time()
|
||||
model = config['ollama'].get('model', 'qwen3:4b')
|
||||
|
||||
try:
|
||||
response = ollama.generate(model=model, prompt=prompt, options={'temperature': 0.1})
|
||||
output = response['response']
|
||||
|
||||
# Parse output
|
||||
is_ad = False
|
||||
summary = "No summary"
|
||||
reason = "Unknown"
|
||||
|
||||
for line in output.strip().split('\n'):
|
||||
if line.startswith('IsAD:'):
|
||||
is_ad = 'YES' in line.upper()
|
||||
elif line.startswith('Summary:'):
|
||||
summary = line.replace('Summary:', '').strip()[:200]
|
||||
elif line.startswith('Reason:'):
|
||||
reason = line.replace('Reason:', '').strip()
|
||||
|
||||
if is_ad:
|
||||
result = f"AD: {reason}"
|
||||
else:
|
||||
result = f"KEEP: {reason}"
|
||||
|
||||
except Exception as e:
|
||||
result = f"KEEP: error - {str(e)[:100]}"
|
||||
summary = "Analysis failed"
|
||||
is_ad = False
|
||||
|
||||
duration = time.time() - start_time
|
||||
return result, summary, is_ad, duration
|
||||
|
||||
def move_to_trash(mail, email_id):
|
||||
"""Move email to Trash folder"""
|
||||
# Copy to Trash
|
||||
result = mail.copy(email_id, 'Trash')
|
||||
if result[0] == 'OK':
|
||||
# Mark original as deleted
|
||||
mail.store(email_id, '+FLAGS', '\\Deleted')
|
||||
return True
|
||||
return False
|
||||
|
||||
def log_result(log_file, email_data, analysis, action, duration=None):
|
||||
"""Log processing result with Qwen3 duration"""
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
duration_str = f" ({duration:.1f}s)" if duration else ""
|
||||
with open(log_file, 'a') as f:
|
||||
f.write(f"[{timestamp}] {action}{duration_str}: {email_data['subject'][:60]}\n")
|
||||
f.write(f" From: {email_data['sender']}\n")
|
||||
f.write(f" Analysis: {analysis}\n\n")
|
||||
|
||||
def load_pending():
|
||||
"""Load pending emails from JSON file"""
|
||||
if not PENDING_FILE.exists():
|
||||
return {}
|
||||
with open(PENDING_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_pending(pending):
|
||||
"""Save pending emails to JSON file"""
|
||||
DATA_DIR.mkdir(exist_ok=True)
|
||||
with open(PENDING_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(pending, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def add_to_pending(email_data, summary, imap_uid, recipient):
|
||||
"""Add email to pending queue"""
|
||||
pending = load_pending()
|
||||
|
||||
# Generate unique ID
|
||||
import hashlib
|
||||
msg_id = f"msg_{hashlib.md5(f'{imap_uid}_{email_data['subject']}'.encode()).hexdigest()[:8]}"
|
||||
|
||||
# Extract date from email
|
||||
email_date = email_data.get('date', datetime.now().isoformat())
|
||||
|
||||
pending[msg_id] = {
|
||||
"imap_uid": str(imap_uid),
|
||||
"subject": email_data['subject'],
|
||||
"sender": email_data['sender'],
|
||||
"recipient": recipient,
|
||||
"summary": summary,
|
||||
"email_date": email_date,
|
||||
"status": "pending",
|
||||
"found_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
save_pending(pending)
|
||||
return msg_id
|
||||
|
||||
def main():
|
||||
"""Main processing function"""
|
||||
print("📧 Email Processor Starting...")
|
||||
|
||||
# Load config
|
||||
config = load_config()
|
||||
|
||||
# Setup logging
|
||||
LOGS_DIR.mkdir(exist_ok=True)
|
||||
log_file = LOGS_DIR / f"{datetime.now().strftime('%Y-%m-%d')}.log"
|
||||
|
||||
try:
|
||||
# Connect to IMAP
|
||||
print("Connecting to IMAP...")
|
||||
mail = connect_imap(config)
|
||||
print("✅ Connected")
|
||||
|
||||
# Get unseen emails
|
||||
email_ids = get_unseen_emails(mail)
|
||||
print(f"Found {len(email_ids)} unread emails")
|
||||
|
||||
if not email_ids:
|
||||
print("No new emails to process")
|
||||
mail.logout()
|
||||
return
|
||||
|
||||
# Process each email
|
||||
processed = 0
|
||||
moved_to_trash = 0
|
||||
added_to_pending = 0
|
||||
|
||||
for email_id in email_ids:
|
||||
print(f"\nProcessing email {email_id.decode()}...")
|
||||
|
||||
# Fetch email
|
||||
email_data = fetch_email(mail, email_id)
|
||||
print(f" Subject: {email_data['subject'][:50]}")
|
||||
|
||||
# Analyze with Qwen3 (one call for both ad detection and summary)
|
||||
analysis, summary, is_ad, duration = analyze_with_qwen3(email_data, config)
|
||||
print(f" Analysis: {analysis[:100]}")
|
||||
print(f" Summary: {summary[:60]}...")
|
||||
print(f" Qwen3 time: {duration:.1f}s")
|
||||
|
||||
# Check if analysis was successful (not an error)
|
||||
if 'error -' in analysis.lower():
|
||||
# Analysis failed - keep email unread for retry
|
||||
print(f" -> Analysis failed, keeping unread for retry")
|
||||
log_result(log_file, email_data, analysis, "FAILED_RETRY", duration)
|
||||
# Don't increment processed count - will retry next time
|
||||
continue
|
||||
|
||||
# Analysis successful - determine action
|
||||
if is_ad:
|
||||
print(" -> Moving to Trash")
|
||||
if move_to_trash(mail, email_id):
|
||||
log_result(log_file, email_data, analysis, "MOVED_TO_TRASH", duration)
|
||||
moved_to_trash += 1
|
||||
else:
|
||||
log_result(log_file, email_data, analysis, "MOVE_FAILED", duration)
|
||||
else:
|
||||
# Non-ad email - add to pending queue
|
||||
print(" -> Adding to pending queue")
|
||||
|
||||
# Add to pending
|
||||
msg_internal_id = add_to_pending(
|
||||
email_data,
|
||||
summary,
|
||||
email_id.decode(),
|
||||
email_data.get('recipient', 'youlu@luyanxin.com')
|
||||
)
|
||||
|
||||
# Mark as read (so it won't be processed again)
|
||||
mail.store(email_id, '+FLAGS', '\\Seen')
|
||||
|
||||
log_result(log_file, email_data, analysis, f"ADDED_TO_PENDING ({msg_internal_id})", duration)
|
||||
added_to_pending += 1
|
||||
|
||||
processed += 1
|
||||
|
||||
# Expunge deleted emails
|
||||
mail.expunge()
|
||||
mail.logout()
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Total emails checked: {len(email_ids)}")
|
||||
print(f"Successfully processed: {processed} emails")
|
||||
print(f" - Moved to trash (ads): {moved_to_trash}")
|
||||
print(f" - Added to pending queue: {added_to_pending}")
|
||||
print(f"Failed (will retry next time): {len(email_ids) - processed}")
|
||||
print(f"\n📁 Pending queue: {PENDING_FILE}")
|
||||
print(f"📝 Log: {log_file}")
|
||||
print(f"\n💡 Run 'python process_queue.py' to view and process pending emails")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
28
scripts/email_processor/move_ad_to_trash.py
Normal file
28
scripts/email_processor/move_ad_to_trash.py
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Move specific email to trash"""
|
||||
import imaplib
|
||||
import email
|
||||
|
||||
# Connect
|
||||
mail = imaplib.IMAP4_SSL('imap.migadu.com', 993)
|
||||
mail.login('youlu@luyanxin.com', 'kDkNau2r7m.hV!uk*D4Yr8mC7Dyjx9T')
|
||||
mail.select('INBOX')
|
||||
|
||||
# Search for the email with "10% off" in subject
|
||||
_, search_data = mail.search(None, 'SUBJECT', '"10% off"')
|
||||
email_ids = search_data[0].split()
|
||||
|
||||
print(f"Found {len(email_ids)} emails with '10% off' in subject")
|
||||
|
||||
for email_id in email_ids:
|
||||
# Copy to Trash
|
||||
result = mail.copy(email_id, 'Trash')
|
||||
if result[0] == 'OK':
|
||||
mail.store(email_id, '+FLAGS', '\\Deleted')
|
||||
print(f"✅ Moved email {email_id.decode()} to Trash")
|
||||
else:
|
||||
print(f"❌ Failed to move email {email_id.decode()}")
|
||||
|
||||
mail.expunge()
|
||||
mail.logout()
|
||||
print("Done!")
|
||||
214
scripts/email_processor/process_queue.py
Normal file
214
scripts/email_processor/process_queue.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Email Queue Processor - Handle user commands for pending emails
|
||||
Reads pending_emails.json and executes user commands (archive/keep/reply)
|
||||
"""
|
||||
|
||||
import json
|
||||
import imaplib
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
DATA_FILE = SCRIPT_DIR / "data" / "pending_emails.json"
|
||||
|
||||
def load_pending():
|
||||
"""Load pending emails from JSON file"""
|
||||
if not DATA_FILE.exists():
|
||||
return {}
|
||||
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_pending(pending):
|
||||
"""Save pending emails to JSON file"""
|
||||
DATA_FILE.parent.mkdir(exist_ok=True)
|
||||
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(pending, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def connect_imap(config):
|
||||
"""Connect to IMAP server"""
|
||||
mail = imaplib.IMAP4_SSL(config['imap']['host'], config['imap']['port'])
|
||||
mail.login(config['imap']['email'], config['imap']['password'])
|
||||
return mail
|
||||
|
||||
def show_pending_list():
|
||||
"""Display all pending emails"""
|
||||
pending = load_pending()
|
||||
|
||||
if not pending:
|
||||
print("📭 没有待处理的邮件")
|
||||
return
|
||||
|
||||
print(f"\n📧 待处理邮件列表 ({len(pending)} 封)")
|
||||
print("=" * 60)
|
||||
|
||||
# Sort by email_date
|
||||
sorted_items = sorted(
|
||||
pending.items(),
|
||||
key=lambda x: x[1].get('email_date', '')
|
||||
)
|
||||
|
||||
for msg_id, data in sorted_items:
|
||||
if data.get('status') == 'pending':
|
||||
print(f"\n🆔 {msg_id}")
|
||||
print(f" 主题: {data.get('subject', 'N/A')[:50]}")
|
||||
print(f" 发件人: {data.get('sender', 'N/A')}")
|
||||
print(f" 收件人: {data.get('recipient', 'N/A')}")
|
||||
print(f" 时间: {data.get('email_date', 'N/A')}")
|
||||
print(f" 摘要: {data.get('summary', 'N/A')[:80]}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("\n可用指令:")
|
||||
print(" • 归档 [ID] - 移动到 Archive 文件夹")
|
||||
print(" • 保留 [ID] - 标记已读,留在收件箱")
|
||||
print(" • 删除 [ID] - 移动到 Trash")
|
||||
print(" • 全部处理 - 列出所有并批量操作")
|
||||
|
||||
def archive_email(config, msg_id):
|
||||
"""Archive a specific email by ID"""
|
||||
pending = load_pending()
|
||||
|
||||
if msg_id not in pending:
|
||||
print(f"❌ 未找到邮件 ID: {msg_id}")
|
||||
return False
|
||||
|
||||
email_data = pending[msg_id]
|
||||
uid = email_data.get('imap_uid')
|
||||
|
||||
if not uid:
|
||||
print(f"❌ 邮件 {msg_id} 没有 UID")
|
||||
return False
|
||||
|
||||
try:
|
||||
mail = connect_imap(config)
|
||||
mail.select('INBOX')
|
||||
|
||||
# Copy to Archive
|
||||
result = mail.copy(uid, 'Archive')
|
||||
if result[0] == 'OK':
|
||||
# Mark original as deleted
|
||||
mail.store(uid, '+FLAGS', '\\Deleted')
|
||||
mail.expunge()
|
||||
|
||||
# Update status
|
||||
pending[msg_id]['status'] = 'done'
|
||||
pending[msg_id]['action'] = 'archived'
|
||||
pending[msg_id]['processed_at'] = datetime.now().isoformat()
|
||||
save_pending(pending)
|
||||
|
||||
print(f"✅ 已归档: {email_data.get('subject', 'N/A')[:40]}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 归档失败: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {e}")
|
||||
return False
|
||||
finally:
|
||||
try:
|
||||
mail.logout()
|
||||
except:
|
||||
pass
|
||||
|
||||
def keep_email(config, msg_id):
|
||||
"""Keep email in inbox, mark as read"""
|
||||
pending = load_pending()
|
||||
|
||||
if msg_id not in pending:
|
||||
print(f"❌ 未找到邮件 ID: {msg_id}")
|
||||
return False
|
||||
|
||||
email_data = pending[msg_id]
|
||||
uid = email_data.get('imap_uid')
|
||||
|
||||
if not uid:
|
||||
print(f"❌ 邮件 {msg_id} 没有 UID")
|
||||
return False
|
||||
|
||||
try:
|
||||
mail = connect_imap(config)
|
||||
mail.select('INBOX')
|
||||
|
||||
# Mark as read (Seen)
|
||||
mail.store(uid, '+FLAGS', '\\Seen')
|
||||
|
||||
# Update status
|
||||
pending[msg_id]['status'] = 'done'
|
||||
pending[msg_id]['action'] = 'kept'
|
||||
pending[msg_id]['processed_at'] = datetime.now().isoformat()
|
||||
save_pending(pending)
|
||||
|
||||
print(f"✅ 已保留: {email_data.get('subject', 'N/A')[:40]}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {e}")
|
||||
return False
|
||||
finally:
|
||||
try:
|
||||
mail.logout()
|
||||
except:
|
||||
pass
|
||||
|
||||
def delete_email(config, msg_id):
|
||||
"""Move email to Trash"""
|
||||
pending = load_pending()
|
||||
|
||||
if msg_id not in pending:
|
||||
print(f"❌ 未找到邮件 ID: {msg_id}")
|
||||
return False
|
||||
|
||||
email_data = pending[msg_id]
|
||||
uid = email_data.get('imap_uid')
|
||||
|
||||
if not uid:
|
||||
print(f"❌ 邮件 {msg_id} 没有 UID")
|
||||
return False
|
||||
|
||||
try:
|
||||
mail = connect_imap(config)
|
||||
mail.select('INBOX')
|
||||
|
||||
# Copy to Trash
|
||||
result = mail.copy(uid, 'Trash')
|
||||
if result[0] == 'OK':
|
||||
mail.store(uid, '+FLAGS', '\\Deleted')
|
||||
mail.expunge()
|
||||
|
||||
# Update status
|
||||
pending[msg_id]['status'] = 'done'
|
||||
pending[msg_id]['action'] = 'deleted'
|
||||
pending[msg_id]['processed_at'] = datetime.now().isoformat()
|
||||
save_pending(pending)
|
||||
|
||||
print(f"✅ 已删除: {email_data.get('subject', 'N/A')[:40]}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 删除失败: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {e}")
|
||||
return False
|
||||
finally:
|
||||
try:
|
||||
mail.logout()
|
||||
except:
|
||||
pass
|
||||
|
||||
def main():
|
||||
"""Main function - show pending list"""
|
||||
import json
|
||||
|
||||
# Load config
|
||||
config_file = Path(__file__).parent / "config.json"
|
||||
with open(config_file) as f:
|
||||
config = json.load(f)
|
||||
|
||||
show_pending_list()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
38
scripts/email_processor/test_single.py
Normal file
38
scripts/email_processor/test_single.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test single email analysis"""
|
||||
import requests
|
||||
import json
|
||||
|
||||
email_data = {
|
||||
"subject": "Fwd: Get 10% off your next order 🎉",
|
||||
"sender": "crac1017@hotmail.com",
|
||||
"body": "Get 10% off your next order! Limited time offer. Shop now and save!"
|
||||
}
|
||||
|
||||
prompt = f"""Analyze this email and determine if it's an advertisement/promotional email.
|
||||
|
||||
Subject: {email_data['subject']}
|
||||
Sender: {email_data['sender']}
|
||||
Body preview: {email_data['body'][:200]}
|
||||
|
||||
Is this an advertisement or promotional email? Answer with ONLY:
|
||||
- "AD: [brief reason]" if it's an ad/promo
|
||||
- "KEEP: [brief reason]" if it's important/legitimate
|
||||
|
||||
Be conservative - only mark as AD if clearly promotional."""
|
||||
|
||||
print("Sending to Qwen3...")
|
||||
try:
|
||||
response = requests.post(
|
||||
"http://localhost:11434/api/generate",
|
||||
json={
|
||||
"model": "qwen3:4b",
|
||||
"prompt": prompt,
|
||||
"stream": False
|
||||
},
|
||||
timeout=120
|
||||
)
|
||||
result = response.json()
|
||||
print(f"Result: {result.get('response', 'error')}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
1
scripts/email_processor/venv/bin/python
Symbolic link
1
scripts/email_processor/venv/bin/python
Symbolic link
@@ -0,0 +1 @@
|
||||
python3
|
||||
1
scripts/email_processor/venv/bin/python3
Symbolic link
1
scripts/email_processor/venv/bin/python3
Symbolic link
@@ -0,0 +1 @@
|
||||
/usr/bin/python3
|
||||
1
scripts/email_processor/venv/bin/python3.12
Symbolic link
1
scripts/email_processor/venv/bin/python3.12
Symbolic link
@@ -0,0 +1 @@
|
||||
python3
|
||||
1
scripts/email_processor/venv/lib64
Symbolic link
1
scripts/email_processor/venv/lib64
Symbolic link
@@ -0,0 +1 @@
|
||||
lib
|
||||
5
scripts/email_processor/venv/pyvenv.cfg
Normal file
5
scripts/email_processor/venv/pyvenv.cfg
Normal file
@@ -0,0 +1,5 @@
|
||||
home = /usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.12.3
|
||||
executable = /usr/bin/python3.12
|
||||
command = /usr/bin/python3 -m venv /home/lyx/.openclaw/workspace/scripts/email_processor/venv
|
||||
Reference in New Issue
Block a user