Initial parlobyg-email skill - IMAP/SMTP client for info@parlobyg.dk
This commit is contained in:
commit
602019f039
3 changed files with 430 additions and 0 deletions
71
SKILL.md
Normal file
71
SKILL.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
name: parlobyg-email
|
||||
description: Email access for Parlobyg (info@parlobyg.dk). IMAP/SMTP via one.com. Use for reading, sending, and organizing customer emails. Triggers on email tasks for Parlobyg.
|
||||
---
|
||||
|
||||
# Parlobyg Email Skill
|
||||
|
||||
IMAP/SMTP client for info@parlobyg.dk — the main customer email for Parlobyg construction company.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Check inbox
|
||||
python3 scripts/email_client.py inbox --limit 10
|
||||
|
||||
# Check unread
|
||||
python3 scripts/email_client.py unread
|
||||
|
||||
# Read specific email
|
||||
python3 scripts/email_client.py read --id 123
|
||||
|
||||
# Search emails
|
||||
python3 scripts/email_client.py search --query "from:kunde@example.dk"
|
||||
|
||||
# List folders
|
||||
python3 scripts/email_client.py folders
|
||||
|
||||
# Send email
|
||||
python3 scripts/email_client.py send --to "kunde@example.dk" --subject "Re: Tilbud" --body "Tak for din henvendelse..."
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
- **Email:** info@parlobyg.dk
|
||||
- **Provider:** one.com
|
||||
- **IMAP:** imap.one.com:993 (SSL)
|
||||
- **SMTP:** send.one.com:465 (SSL)
|
||||
- **Password:** Q223Ln57TyyC4
|
||||
|
||||
## Folder Structure
|
||||
|
||||
~1967 folders organized as:
|
||||
```
|
||||
Kunder og byggesager/
|
||||
├── Kunder/
|
||||
│ ├── [Customer Name]/
|
||||
│ │ ├── [Project 1]/
|
||||
│ │ └── [Project 2]/
|
||||
```
|
||||
|
||||
## Related Mailboxes
|
||||
|
||||
- **support@parlobyg.dk** — Michelle Ermler (project coordinator, 61 10 99 81)
|
||||
- **wordpress@parlobyg.dk** — Contact form submissions (forwards to support@)
|
||||
|
||||
## Integration with Ordrestyring
|
||||
|
||||
Cross-reference email senders with Ordrestyring customers:
|
||||
```bash
|
||||
# Search customer in Ordrestyring by email
|
||||
python3 ~/.openclaw/skills/ordrestyring/scripts/graphql_query.py customers --limit 50
|
||||
```
|
||||
|
||||
## Cron Tasks
|
||||
|
||||
- **Morning triage (5am CET):** Scan inbox, classify, draft replies
|
||||
- **Evening summary (17:30 CET):** Send digest to Mikkel
|
||||
|
||||
## References
|
||||
|
||||
- [Email Patterns](references/email-patterns.md)
|
||||
77
references/email-patterns.md
Normal file
77
references/email-patterns.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Email Patterns for Parlobyg
|
||||
|
||||
## Common Email Types
|
||||
|
||||
### 1. Customer Inquiries
|
||||
- New project requests
|
||||
- Quote requests
|
||||
- Questions about ongoing work
|
||||
|
||||
**Classification signals:**
|
||||
- Subject contains: "tilbud", "forespørgsel", "pris", "kan I..."
|
||||
- From unknown/new sender
|
||||
- CC to support@parlobyg.dk
|
||||
|
||||
### 2. Project Updates
|
||||
- Schedule changes
|
||||
- Material deliveries
|
||||
- Site visit coordination
|
||||
|
||||
**Classification signals:**
|
||||
- From known customer (match Ordrestyring)
|
||||
- References case number
|
||||
- Contains dates/times
|
||||
|
||||
### 3. Supplier Communications
|
||||
- Material quotes
|
||||
- Delivery confirmations
|
||||
- Invoice notifications
|
||||
|
||||
**Classification signals:**
|
||||
- From: @stark.dk, @davidsen.dk, @xl-byg.dk, etc.
|
||||
- Subject contains: "ordre", "levering", "faktura"
|
||||
|
||||
### 4. WordPress Form Submissions
|
||||
- Contact form entries from parlobyg.dk
|
||||
|
||||
**Classification signals:**
|
||||
- From: wordpress@parlobyg.dk
|
||||
- To: support@parlobyg.dk
|
||||
- Subject: "Ny besked fra..."
|
||||
|
||||
## Auto-Response Templates
|
||||
|
||||
### Acknowledgment (Danish)
|
||||
```
|
||||
Tak for din henvendelse!
|
||||
|
||||
Vi har modtaget din besked og vender tilbage hurtigst muligt — typisk inden for 1-2 hverdage.
|
||||
|
||||
Med venlig hilsen,
|
||||
Parlobyg
|
||||
Tlf: 61 10 99 81
|
||||
```
|
||||
|
||||
### Quote Follow-up
|
||||
```
|
||||
Hej [NAVN],
|
||||
|
||||
Tak for din interesse i Parlobyg.
|
||||
|
||||
Jeg har set på din forespørgsel om [PROJEKT] og vil gerne aftale et tidspunkt til besigtigelse.
|
||||
|
||||
Hvornår passer det dig?
|
||||
|
||||
Med venlig hilsen,
|
||||
Mikkel Parlo
|
||||
Parlobyg
|
||||
```
|
||||
|
||||
## Folder Organization
|
||||
|
||||
Emails should be filed to:
|
||||
```
|
||||
Kunder og byggesager/Kunder/[Kundenavn]/[Projektnavn]/
|
||||
```
|
||||
|
||||
Match customer name with Ordrestyring customer record.
|
||||
282
scripts/email_client.py
Executable file
282
scripts/email_client.py
Executable file
|
|
@ -0,0 +1,282 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Parlobyg Email Client (info@parlobyg.dk)
|
||||
|
||||
Usage:
|
||||
python3 email_client.py inbox --limit 10
|
||||
python3 email_client.py unread
|
||||
python3 email_client.py read --id 123
|
||||
python3 email_client.py search --query "from:example@mail.dk"
|
||||
python3 email_client.py folders
|
||||
python3 email_client.py send --to "to@example.dk" --subject "Subject" --body "Body"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import imaplib
|
||||
import smtplib
|
||||
import ssl
|
||||
import email
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.header import decode_header
|
||||
from datetime import datetime
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Configuration
|
||||
IMAP_HOST = "imap.one.com"
|
||||
IMAP_PORT = 993
|
||||
SMTP_HOST = "send.one.com"
|
||||
SMTP_PORT = 465
|
||||
EMAIL_USER = "info@parlobyg.dk"
|
||||
EMAIL_PASS = "Q223Ln57TyyC4"
|
||||
|
||||
|
||||
def decode_mime_header(header):
|
||||
"""Decode MIME encoded header."""
|
||||
if header is None:
|
||||
return ""
|
||||
decoded_parts = decode_header(header)
|
||||
result = []
|
||||
for part, encoding in decoded_parts:
|
||||
if isinstance(part, bytes):
|
||||
result.append(part.decode(encoding or 'utf-8', errors='replace'))
|
||||
else:
|
||||
result.append(part)
|
||||
return ' '.join(result)
|
||||
|
||||
|
||||
def get_imap_connection():
|
||||
"""Create IMAP connection."""
|
||||
context = ssl.create_default_context()
|
||||
mail = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT, ssl_context=context)
|
||||
mail.login(EMAIL_USER, EMAIL_PASS)
|
||||
return mail
|
||||
|
||||
|
||||
def list_inbox(limit=10):
|
||||
"""List recent inbox messages."""
|
||||
mail = get_imap_connection()
|
||||
mail.select("INBOX")
|
||||
|
||||
status, messages = mail.search(None, "ALL")
|
||||
if status != "OK":
|
||||
return {"error": "Failed to search inbox"}
|
||||
|
||||
msg_ids = messages[0].split()
|
||||
msg_ids = msg_ids[-limit:] if len(msg_ids) > limit else msg_ids
|
||||
msg_ids.reverse() # Most recent first
|
||||
|
||||
results = []
|
||||
for msg_id in msg_ids:
|
||||
status, msg_data = mail.fetch(msg_id, "(RFC822.HEADER)")
|
||||
if status == "OK":
|
||||
msg = email.message_from_bytes(msg_data[0][1])
|
||||
results.append({
|
||||
"id": msg_id.decode(),
|
||||
"from": decode_mime_header(msg["From"]),
|
||||
"subject": decode_mime_header(msg["Subject"]),
|
||||
"date": msg["Date"]
|
||||
})
|
||||
|
||||
mail.logout()
|
||||
return {"count": len(results), "messages": results}
|
||||
|
||||
|
||||
def list_unread():
|
||||
"""List unread messages."""
|
||||
mail = get_imap_connection()
|
||||
mail.select("INBOX")
|
||||
|
||||
status, messages = mail.search(None, "UNSEEN")
|
||||
if status != "OK":
|
||||
return {"error": "Failed to search unread"}
|
||||
|
||||
msg_ids = messages[0].split() if messages[0] else []
|
||||
|
||||
results = []
|
||||
for msg_id in msg_ids:
|
||||
status, msg_data = mail.fetch(msg_id, "(RFC822.HEADER)")
|
||||
if status == "OK":
|
||||
msg = email.message_from_bytes(msg_data[0][1])
|
||||
results.append({
|
||||
"id": msg_id.decode(),
|
||||
"from": decode_mime_header(msg["From"]),
|
||||
"subject": decode_mime_header(msg["Subject"]),
|
||||
"date": msg["Date"]
|
||||
})
|
||||
|
||||
mail.logout()
|
||||
return {"unread_count": len(results), "messages": results}
|
||||
|
||||
|
||||
def read_email(msg_id):
|
||||
"""Read a specific email by ID."""
|
||||
mail = get_imap_connection()
|
||||
mail.select("INBOX")
|
||||
|
||||
status, msg_data = mail.fetch(str(msg_id).encode(), "(RFC822)")
|
||||
if status != "OK":
|
||||
return {"error": f"Failed to fetch email {msg_id}"}
|
||||
|
||||
msg = email.message_from_bytes(msg_data[0][1])
|
||||
|
||||
# Extract body
|
||||
body = ""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == "text/plain":
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
body = payload.decode(part.get_content_charset() or 'utf-8', errors='replace')
|
||||
break
|
||||
else:
|
||||
payload = msg.get_payload(decode=True)
|
||||
if payload:
|
||||
body = payload.decode(msg.get_content_charset() or 'utf-8', errors='replace')
|
||||
|
||||
mail.logout()
|
||||
return {
|
||||
"id": msg_id,
|
||||
"from": decode_mime_header(msg["From"]),
|
||||
"to": decode_mime_header(msg["To"]),
|
||||
"subject": decode_mime_header(msg["Subject"]),
|
||||
"date": msg["Date"],
|
||||
"body": body[:5000] # Truncate long bodies
|
||||
}
|
||||
|
||||
|
||||
def search_emails(query, limit=20):
|
||||
"""Search emails with IMAP search criteria."""
|
||||
mail = get_imap_connection()
|
||||
mail.select("INBOX")
|
||||
|
||||
# Parse simple query format
|
||||
search_criteria = "ALL"
|
||||
if query.startswith("from:"):
|
||||
search_criteria = f'FROM "{query[5:]}"'
|
||||
elif query.startswith("subject:"):
|
||||
search_criteria = f'SUBJECT "{query[8:]}"'
|
||||
elif query.startswith("to:"):
|
||||
search_criteria = f'TO "{query[3:]}"'
|
||||
else:
|
||||
search_criteria = f'BODY "{query}"'
|
||||
|
||||
status, messages = mail.search(None, search_criteria)
|
||||
if status != "OK":
|
||||
return {"error": "Search failed"}
|
||||
|
||||
msg_ids = messages[0].split() if messages[0] else []
|
||||
msg_ids = msg_ids[-limit:] if len(msg_ids) > limit else msg_ids
|
||||
msg_ids.reverse()
|
||||
|
||||
results = []
|
||||
for msg_id in msg_ids:
|
||||
status, msg_data = mail.fetch(msg_id, "(RFC822.HEADER)")
|
||||
if status == "OK":
|
||||
msg = email.message_from_bytes(msg_data[0][1])
|
||||
results.append({
|
||||
"id": msg_id.decode(),
|
||||
"from": decode_mime_header(msg["From"]),
|
||||
"subject": decode_mime_header(msg["Subject"]),
|
||||
"date": msg["Date"]
|
||||
})
|
||||
|
||||
mail.logout()
|
||||
return {"query": query, "count": len(results), "messages": results}
|
||||
|
||||
|
||||
def list_folders():
|
||||
"""List all folders."""
|
||||
mail = get_imap_connection()
|
||||
|
||||
status, folders = mail.list()
|
||||
if status != "OK":
|
||||
return {"error": "Failed to list folders"}
|
||||
|
||||
folder_names = []
|
||||
for folder in folders:
|
||||
# Parse folder response
|
||||
parts = folder.decode().split(' "/" ')
|
||||
if len(parts) >= 2:
|
||||
folder_names.append(parts[1].strip('"'))
|
||||
|
||||
mail.logout()
|
||||
return {"count": len(folder_names), "folders": folder_names[:100]} # Limit output
|
||||
|
||||
|
||||
def send_email(to, subject, body, cc=None):
|
||||
"""Send an email."""
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = EMAIL_USER
|
||||
msg["To"] = to
|
||||
msg["Subject"] = subject
|
||||
if cc:
|
||||
msg["Cc"] = cc
|
||||
|
||||
msg.attach(MIMEText(body, "plain", "utf-8"))
|
||||
|
||||
try:
|
||||
context = ssl.create_default_context()
|
||||
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, context=context) as server:
|
||||
server.login(EMAIL_USER, EMAIL_PASS)
|
||||
recipients = [to] + ([cc] if cc else [])
|
||||
server.sendmail(EMAIL_USER, recipients, msg.as_string())
|
||||
return {"status": "sent", "to": to, "subject": subject}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Parlobyg Email Client")
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# Inbox
|
||||
inbox_parser = subparsers.add_parser("inbox", help="List recent inbox messages")
|
||||
inbox_parser.add_argument("--limit", type=int, default=10)
|
||||
|
||||
# Unread
|
||||
subparsers.add_parser("unread", help="List unread messages")
|
||||
|
||||
# Read
|
||||
read_parser = subparsers.add_parser("read", help="Read a specific email")
|
||||
read_parser.add_argument("--id", required=True, help="Email ID")
|
||||
|
||||
# Search
|
||||
search_parser = subparsers.add_parser("search", help="Search emails")
|
||||
search_parser.add_argument("--query", required=True, help="Search query (from:, subject:, to:, or text)")
|
||||
search_parser.add_argument("--limit", type=int, default=20)
|
||||
|
||||
# Folders
|
||||
subparsers.add_parser("folders", help="List folders")
|
||||
|
||||
# Send
|
||||
send_parser = subparsers.add_parser("send", help="Send an email")
|
||||
send_parser.add_argument("--to", required=True, help="Recipient")
|
||||
send_parser.add_argument("--subject", required=True, help="Subject")
|
||||
send_parser.add_argument("--body", required=True, help="Body text")
|
||||
send_parser.add_argument("--cc", help="CC recipient")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "inbox":
|
||||
result = list_inbox(args.limit)
|
||||
elif args.command == "unread":
|
||||
result = list_unread()
|
||||
elif args.command == "read":
|
||||
result = read_email(args.id)
|
||||
elif args.command == "search":
|
||||
result = search_emails(args.query, args.limit)
|
||||
elif args.command == "folders":
|
||||
result = list_folders()
|
||||
elif args.command == "send":
|
||||
result = send_email(args.to, args.subject, args.body, args.cc)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue