Initial parlobyg-email skill - IMAP/SMTP client for info@parlobyg.dk

This commit is contained in:
Parlomachine 2026-03-23 19:50:11 +00:00
commit 602019f039
3 changed files with 430 additions and 0 deletions

71
SKILL.md Normal file
View 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)

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