commit 602019f0396d5b4ecf74bca9bc6908f3fc59d65f Author: Parlomachine Date: Mon Mar 23 19:50:11 2026 +0000 Initial parlobyg-email skill - IMAP/SMTP client for info@parlobyg.dk diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..a29924c --- /dev/null +++ b/SKILL.md @@ -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) diff --git a/references/email-patterns.md b/references/email-patterns.md new file mode 100644 index 0000000..2d1d93c --- /dev/null +++ b/references/email-patterns.md @@ -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. diff --git a/scripts/email_client.py b/scripts/email_client.py new file mode 100755 index 0000000..c9d8392 --- /dev/null +++ b/scripts/email_client.py @@ -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()