poffice-admin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # Poffice Admin Skill
2
+
3
+ Administration and automation suite for the **Poffice** ecosystem (Mailcow, Seafile, Paperless-ngx, SOGo).
4
+
5
+ ## 🚀 Overview
6
+
7
+ The `poffice-admin` skill provides a comprehensive set of Python scripts and documentation to manage a self-hosted office stack. It enables automation of user provisioning, mail management, document generation, and calendar orchestration.
8
+
9
+ ---
10
+
11
+ ## 🛠 Features & Functions
12
+
13
+ ### 1. Mail & User Management (`poffice_mail.py`)
14
+ - **List Domains**: Retrieve all domains managed by the Mailcow instance.
15
+ - **Create Mailbox**: Provision new email accounts with specific quotas and settings.
16
+ - **Send Email**: Send plain text emails via SMTP with TLS support.
17
+
18
+ ### 2. Cloud Storage Management (`poffice_docs.py`)
19
+ - **User Provisioning**: Create, list, and delete Seafile users.
20
+ - **Library Management**: Create and list Seafile libraries (repositories).
21
+ - **File Operations**: Programmatic upload and download of files to/from the cloud.
22
+
23
+ ### 3. Document Archiving (`poffice_paperless.py`)
24
+ - **List Documents**: Query the Paperless-ngx archive for stored documents.
25
+ - **Upload Document**: Direct ingestion of files into the Paperless-ngx processing pipeline.
26
+
27
+ ### 4. Calendar Orchestration (`poffice_calendar.py`)
28
+ - **CRUD Operations**: Add, List, Update, and Delete events via CalDAV.
29
+ - **Meeting Invites**: Generate and send official `.ics` meeting invitations to participants.
30
+ - **SOGo Integration**: Fully compatible with the SOGo groupware suite.
31
+
32
+ ### 5. Document Generation (`poffice_gen.py`)
33
+ - **Word (.docx)**: Create and update Word documents with headings and paragraphs.
34
+ - **Excel (.xlsx)**: Generate spreadsheets from python data structures.
35
+ - **PDF**: Create professional PDF reports programmatically.
36
+
37
+ ### 6. Email Interaction (`poffice_imap.py`)
38
+ - **Read Mail**: List and fetch recent emails from any IMAP folder (e.g., INBOX).
39
+ - **Header Parsing**: Extracts Subject, From, and Date for automation triggers.
40
+
41
+ ### 7. Master Admin Sync (`poffice_master_admin.py`)
42
+ - **Neon Integration**: Interfaces with Neon PostgreSQL to check "Master Admin" status.
43
+ - **Cross-App Sync**: Automatically provisions a user across Mail and Seafile once they are verified as an admin in the central Neon database.
44
+
45
+ ---
46
+
47
+ ## 📂 Repository Structure
48
+
49
+ ```text
50
+ poffice-admin/
51
+ ├── SKILL.md # Core skill description and triggers
52
+ ├── scripts/ # Python automation scripts
53
+ │ ├── poffice_mail.py
54
+ │ ├── poffice_docs.py
55
+ │ ├── poffice_calendar.py
56
+ │ └── ...
57
+ └── references/ # API references and credential templates
58
+ ```
59
+
60
+ ## 📋 Usage
61
+
62
+ Most scripts can be run directly via CLI:
63
+
64
+ ```bash
65
+ # Example: Create a mailbox
66
+ python3 scripts/poffice_mail.py create-mailbox example.com user "John Doe" "password123"
67
+
68
+ # Example: Create a PDF report
69
+ python3 scripts/poffice_gen.py create-pdf report.pdf "Monthly Summary" "All systems operational."
70
+ ```
71
+
72
+ ## 🔐 Configuration
73
+
74
+ Credentials should be managed via environment variables or the `references/credentials.md` template.
75
+ - `MAILCOW_API_KEY`
76
+ - `SEAFILE_ADMIN_EMAIL` / `SEAFILE_ADMIN_PASSWORD`
77
+ - `PAPERLESS_TOKEN`
78
+ - `DATABASE_URL` (Neon PostgreSQL)
package/SKILL.md ADDED
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: poffice-admin
3
+ description: "Administration and automation for the Poffice suite (Mailcow, Seafile, Paperless-ngx). Use for: (1) Creating and managing mail accounts, (2) Sending and receiving emails, (3) Managing calendars and invites (via SOGo), (4) managing documents (Seafile/Paperless), (5) Creating and updating Word, Excel, and PDF documents, (6) Creating and sending calendar events and invites, (7) Automating office workflows."
4
+ ---
5
+
6
+ # Poffice Admin Skill
7
+
8
+ This skill provides the necessary tools and workflows to manage the Poffice infrastructure.
9
+
10
+ ## Core Capabilities
11
+
12
+ 1. **User & Mail Management**: Create mailboxes, manage domains, and send/receive emails.
13
+ 2. **Document Management**: Sync files via Seafile and archive documents via Paperless-ngx.
14
+ 3. **Calendar Management**: Full CRUD management of events (List, Add, Update, Delete) in SOGo (attached to Mailcow) and sending email invites with .ics attachments.
15
+ 4. **Document Creation**: Generate and update Word (.docx), Excel (.xlsx), and PDF documents programmatically.
16
+
17
+ ## Quick Reference Scripts
18
+
19
+ The following scripts are available in the `scripts/` directory:
20
+
21
+ - `poffice_mail.py`: Mailbox creation and email sending.
22
+ - `poffice_docs.py`: Seafile user and repo management.
23
+ - `poffice_paperless.py`: Document ingestion and listing.
24
+ - `poffice_gen.py`: Document creation and update (Word, Excel, PDF).
25
+ - `poffice_calendar.py`: Calendar event creation and email invites.
26
+
27
+ ### Typical Workflows
28
+
29
+ #### Creating a New Office User
30
+ To create a fully provisioned user (Mail + Cloud storage):
31
+ 1. Run `python3 scripts/poffice_mail.py create-mailbox domain.com user_name "Full Name" password`
32
+ 2. Run `python3 scripts/poffice_docs.py create-user user@domain.com password`
33
+
34
+ #### Creating and Archiving a Report
35
+ 1. Use `poffice_gen.py` to create a PDF or Excel report.
36
+ 2. Use `poffice_paperless.py` to upload the generated file for long-term archiving.
37
+ 3. Use `poffice_mail.py` to email the report to the team.
38
+
39
+ #### Scheduling a Meeting and Sending Invites
40
+ 1. Use `poffice_calendar.py add` to put the event on your own calendar.
41
+ 2. Use `poffice_calendar.py invite` to send the official invitation to participants.
42
+ 3. Optionally, use `poffice_gen.py` to create a meeting agenda PDF and attach it to an email using `poffice_mail.py`.
43
+
44
+ #### Managing Existing Events
45
+ 1. Use `poffice_calendar.py list` to see upcoming events.
46
+ 2. Use `poffice_calendar.py delete [UID]` to remove an event.
47
+ 3. Update events using the `update_event` method in `poffice_calendar.py`.
48
+
49
+ #### Sending a System Invite
50
+ Use `poffice_mail.py send-email` to notify users about account creation or meetings.
51
+
52
+ ## Service Access & Credentials
53
+
54
+ See [references/credentials.md](references/credentials.md) for API keys and endpoint information.
55
+
56
+ ## Calendar (SOGo)
57
+ Calendar management is handled through SOGo.
58
+ - **URL**: `https://mail.poffice.online/SOGo`
59
+ - **CalDAV**: `https://mail.poffice.online/SOGo/dav/`
60
+ To send invites programmatically, use standard `ics` generation and send via `poffice_mail.py`.
61
+
62
+ ## Documentation
63
+ - **Mailcow API**: [references/mailcow_api.md](references/mailcow_api.md)
64
+ - **Seafile API**: [references/seafile_api.md](references/seafile_api.md)
65
+ - **Paperless API**: [references/paperless_api.md](references/paperless_api.md)
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "poffice-admin",
3
+ "version": "1.0.0",
4
+ "description": "Administration and automation skill for the Poffice suite (Mailcow, Seafile, Paperless-ngx, SOGo).",
5
+ "main": "SKILL.md",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [
10
+ "poffice",
11
+ "admin",
12
+ "skill",
13
+ "automation",
14
+ "mailcow",
15
+ "seafile",
16
+ "paperless-ngx"
17
+ ],
18
+ "author": "Thierry Teisseire",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/thierryteisseire/poffice-workspace.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/thierryteisseire/poffice-workspace/issues"
26
+ },
27
+ "homepage": "https://github.com/thierryteisseire/poffice-workspace#readme"
28
+ }
@@ -0,0 +1 @@
1
+ # Credentials Template\n\nFill this with your API keys.
@@ -0,0 +1,23 @@
1
+ # Mailcow API Reference (v1)
2
+
3
+ Documentation available at: `https://mail.poffice.online/api/` (if enabled)
4
+
5
+ ### Common Endpoints
6
+
7
+ #### GET /api/v1/get/mailbox/all
8
+ Returns list of all mailboxes.
9
+
10
+ #### POST /api/v1/add/mailbox
11
+ Body:
12
+ ```json
13
+ {
14
+ "address": "user@domain.com",
15
+ "name": "Full Name",
16
+ "domain": "domain.com",
17
+ "local_part": "user",
18
+ "password": "password",
19
+ "password2": "password",
20
+ "quota": 3072,
21
+ "active": 1
22
+ }
23
+ ```
@@ -0,0 +1,149 @@
1
+ import caldav
2
+ from icalendar import Calendar, Event, vText
3
+ from datetime import datetime, timedelta
4
+ import pytz
5
+ import uuid
6
+ import smtplib
7
+ from email.mime.text import MIMEText
8
+ from email.mime.multipart import MIMEMultipart
9
+
10
+ class PofficeCalendar:
11
+ def __init__(self, username, password, base_url="https://mail.poffice.online/SOGo/dav/"):
12
+ self.username = username
13
+ self.password = password
14
+ self.base_url = base_url
15
+
16
+ def _get_calendar(self):
17
+ client = caldav.DAVClient(url=self.base_url, username=self.username, password=self.password)
18
+ principal = client.principal()
19
+ calendars = principal.calendars()
20
+ if not calendars:
21
+ raise Exception("No calendars found")
22
+ return calendars[0]
23
+
24
+ def add_event(self, summary, description, start_time, duration_minutes=60, location=None):
25
+ try:
26
+ calendar = self._get_calendar()
27
+ cal = Calendar()
28
+ cal.add('prodid', '-//Poffice Automation//poffice.online//')
29
+ cal.add('version', '2.0')
30
+
31
+ event = Event()
32
+ event.add('summary', summary)
33
+ event.add('description', description)
34
+ if location:
35
+ event.add('location', location)
36
+ event.add('dtstart', start_time)
37
+ event.add('dtend', start_time + timedelta(minutes=duration_minutes))
38
+ event.add('dtstamp', datetime.now(pytz.utc))
39
+ event.add('uid', str(uuid.uuid4()))
40
+
41
+ cal.add_component(event)
42
+ calendar.save_event(cal.to_ical())
43
+ return {"status": "success", "message": f"Event '{summary}' added"}
44
+ except Exception as e:
45
+ return {"status": "error", "message": str(e)}
46
+
47
+ def list_events(self, start=None, end=None):
48
+ try:
49
+ calendar = self._get_calendar()
50
+ if not start:
51
+ start = datetime.now(pytz.utc)
52
+ if not end:
53
+ end = start + timedelta(days=7)
54
+
55
+ events = calendar.date_search(start, end)
56
+ results = []
57
+ for event in events:
58
+ ical = Calendar.from_ical(event.data)
59
+ for component in ical.walk():
60
+ if component.name == "VEVENT":
61
+ results.append({
62
+ "uid": str(component.get('uid')),
63
+ "summary": str(component.get('summary')),
64
+ "start": component.get('dtstart').dt.isoformat(),
65
+ "end": component.get('dtend').dt.isoformat() if component.get('dtend') else None,
66
+ "description": str(component.get('description'))
67
+ })
68
+ return {"status": "success", "events": results}
69
+ except Exception as e:
70
+ return {"status": "error", "message": str(e)}
71
+
72
+ def delete_event(self, uid):
73
+ try:
74
+ calendar = self._get_calendar()
75
+ event = calendar.event_by_uid(uid)
76
+ event.delete()
77
+ return {"status": "success", "message": f"Event {uid} deleted"}
78
+ except Exception as e:
79
+ return {"status": "error", "message": str(e)}
80
+
81
+ def update_event(self, uid, summary=None, description=None, start_time=None, duration_minutes=None):
82
+ try:
83
+ calendar = self._get_calendar()
84
+ event_obj = calendar.event_by_uid(uid)
85
+ ical = Calendar.from_ical(event_obj.data)
86
+
87
+ for component in ical.walk():
88
+ if component.name == "VEVENT":
89
+ if summary: component.replace('summary', summary)
90
+ if description: component.replace('description', description)
91
+ if start_time:
92
+ component.replace('dtstart', start_time)
93
+ if duration_minutes:
94
+ component.replace('dtend', start_time + timedelta(minutes=duration_minutes))
95
+ elif duration_minutes:
96
+ old_start = component.get('dtstart').dt
97
+ component.replace('dtend', old_start + timedelta(minutes=duration_minutes))
98
+
99
+ event_obj.data = ical.to_ical()
100
+ event_obj.save()
101
+ return {"status": "success", "message": f"Event {uid} updated"}
102
+ except Exception as e:
103
+ return {"status": "error", "message": str(e)}
104
+
105
+ def send_invite(self, smtp_user, smtp_pass, to_email, summary, description, start_time, duration_minutes=60, location=None):
106
+ msg = MIMEMultipart('mixed')
107
+ msg['Reply-To'] = smtp_user
108
+ msg['From'] = smtp_user
109
+ msg['To'] = to_email
110
+ msg['Subject'] = f"Invitation: {summary}"
111
+ cal = Calendar()
112
+ cal.add('prodid', '-//Poffice Automation//poffice.online//'); cal.add('version', '2.0'); cal.add('method', 'REQUEST')
113
+ event = Event()
114
+ event.add('summary', summary); event.add('description', description)
115
+ if location: event.add('location', location)
116
+ event.add('dtstart', start_time); event.add('dtend', start_time + timedelta(minutes=duration_minutes))
117
+ event.add('dtstamp', datetime.now(pytz.utc)); event.add('uid', str(uuid.uuid4())); event.add('priority', 5)
118
+ event.add('attendee', f"MAILTO:{to_email}", extra_params={'RSVP': 'TRUE'})
119
+ event.add('organizer', f"MAILTO:{smtp_user}")
120
+ cal.add_component(event)
121
+ part_text = MIMEText(f"You are invited to: {summary}\n\nDescription: {description}", 'plain')
122
+ msg.attach(part_text)
123
+ part_ics = MIMEText(cal.to_ical().decode("utf-8"), 'calendar; method=REQUEST')
124
+ part_ics.add_header('Content-class', 'urn:content-classes:calendarmessage')
125
+ part_ics.add_header('Filename', 'invite.ics'); part_ics.add_header('Content-Disposition', 'attachment; filename=invite.ics')
126
+ msg.attach(part_ics)
127
+ try:
128
+ server = smtplib.SMTP("mail.poffice.online", 587); server.starttls(); server.login(smtp_user, smtp_pass)
129
+ server.sendmail(smtp_user, to_email, msg.as_string()); server.quit()
130
+ return {"status": "success"}
131
+ except Exception as e: return {"status": "error", "message": str(e)}
132
+
133
+ if __name__ == "__main__":
134
+ import sys, os
135
+ USER = os.getenv("POFFICE_EMAIL", "admin@poffice.online")
136
+ PASS = os.getenv("POFFICE_PASSWORD", "")
137
+ cal_manager = PofficeCalendar(USER, PASS)
138
+ if len(sys.argv) > 1:
139
+ action = sys.argv[1]
140
+ if action == "add":
141
+ start_dt = datetime.strptime(sys.argv[4], "%Y-%m-%d %H:%M").replace(tzinfo=pytz.utc)
142
+ print(cal_manager.add_event(sys.argv[2], sys.argv[3], start_dt))
143
+ elif action == "list":
144
+ print(cal_manager.list_events())
145
+ elif action == "delete":
146
+ print(cal_manager.delete_event(sys.argv[2]))
147
+ elif action == "invite":
148
+ start_dt = datetime.strptime(sys.argv[5], "%Y-%m-%d %H:%M").replace(tzinfo=pytz.utc)
149
+ print(cal_manager.send_invite(USER, PASS, sys.argv[2], sys.argv[3], sys.argv[4], start_dt))
@@ -0,0 +1,121 @@
1
+ import requests
2
+ import json
3
+ import os
4
+
5
+ class SeafileAdmin:
6
+ def __init__(self, username, password, base_url="https://cloud.poffice.online"):
7
+ self.username = username
8
+ self.password = password
9
+ self.base_url = base_url
10
+ self.token = self._get_token()
11
+ self.headers = {
12
+ "Authorization": f"Token {self.token}",
13
+ "Content-Type": "application/json"
14
+ }
15
+
16
+ def _get_token(self):
17
+ response = requests.post(f"{self.base_url}/api2/auth-token/", data={
18
+ "username": self.username,
19
+ "password": self.password
20
+ }, verify=False)
21
+ if response.status_code == 200:
22
+ return response.json()["token"]
23
+ else:
24
+ raise Exception(f"Failed to get Seafile token: {response.text}")
25
+
26
+ def create_user(self, email, password, is_admin=False):
27
+ # Admin endpoint to create user
28
+ data = {
29
+ "email": email,
30
+ "password": password,
31
+ "is_staff": "true" if is_admin else "false"
32
+ }
33
+ # Use v2.1 for better admin control
34
+ response = requests.post(f"{self.base_url}/api/v2.1/admin/users/", headers=self.headers, json=data, verify=False)
35
+ return response.json()
36
+
37
+ def list_users(self):
38
+ # Use v2.1 admin endpoint
39
+ response = requests.get(f"{self.base_url}/api/v2.1/admin/users/", headers=self.headers, verify=False)
40
+ return response.json()
41
+
42
+ def delete_user(self, email):
43
+ # Use v2.1 admin endpoint
44
+ response = requests.delete(f"{self.base_url}/api/v2.1/admin/users/{email}/", headers=self.headers, verify=False)
45
+ return response.json() if response.content else {"status": "ok"}
46
+
47
+ def list_repos(self):
48
+ response = requests.get(f"{self.base_url}/api2/repos/", headers=self.headers, verify=False)
49
+ return response.json()
50
+
51
+ def create_repo(self, name):
52
+ # Note: creates a repo for the current user
53
+ response = requests.post(f"{self.base_url}/api2/repos/", headers=self.headers, json={"name": name}, verify=False)
54
+ return response.json()
55
+
56
+ def list_files(self, repo_id, path="/"):
57
+ response = requests.get(f"{self.base_url}/api2/repos/{repo_id}/dir/?p={path}", headers=self.headers, verify=False)
58
+ return response.json()
59
+
60
+ def upload_file(self, repo_id, file_path, target_path="/"):
61
+ # 1. Get upload link
62
+ response = requests.get(f"{self.base_url}/api2/repos/{repo_id}/upload-link/", headers=self.headers, verify=False)
63
+ upload_url = response.json() # This is the direct string of the URL
64
+
65
+ # 2. Upload file
66
+ filename = os.path.basename(file_path)
67
+ with open(file_path, 'rb') as f:
68
+ files = {
69
+ 'file': (filename, f),
70
+ 'parent_dir': (None, target_path)
71
+ }
72
+ # Upload doesn't use the standard json headers
73
+ upload_response = requests.post(upload_url, headers={"Authorization": f"Token {self.token}"}, files=files, verify=False)
74
+ return upload_response.text
75
+
76
+ def download_file(self, repo_id, file_path, save_as):
77
+ # 1. Get download link
78
+ response = requests.get(f"{self.base_url}/api2/repos/{repo_id}/file/?p={file_path}", headers=self.headers, verify=False)
79
+ download_url = response.json() # Direct string
80
+
81
+ # 2. Download content
82
+ r = requests.get(download_url, verify=False)
83
+ with open(save_as, 'wb') as f:
84
+ f.write(r.content)
85
+ return save_as
86
+
87
+ if __name__ == "__main__":
88
+ import sys
89
+ # Example usage
90
+ USER = os.getenv("SEAFILE_ADMIN_EMAIL", "")
91
+ PASS = os.getenv("SEAFILE_ADMIN_PASSWORD", "")
92
+
93
+ admin = SeafileAdmin(USER, PASS)
94
+
95
+ if len(sys.argv) > 1:
96
+ action = sys.argv[1]
97
+ if action == "list-users":
98
+ print(json.dumps(admin.list_users(), indent=4))
99
+ elif action == "create-user":
100
+ # python3 poffice_docs.py create-user email pass [is_admin]
101
+ is_admin = sys.argv[4].lower() == 'true' if len(sys.argv) > 4 else False
102
+ print(json.dumps(admin.create_user(sys.argv[2], sys.argv[3], is_admin), indent=4))
103
+ elif action == "delete-user":
104
+ # python3 poffice_docs.py delete-user email
105
+ print(json.dumps(admin.delete_user(sys.argv[2]), indent=4))
106
+ elif action == "list-repos":
107
+ print(json.dumps(admin.list_repos(), indent=4))
108
+ elif action == "create-repo":
109
+ # python3 poffice_docs.py create-repo name
110
+ print(json.dumps(admin.create_repo(sys.argv[2]), indent=4))
111
+ elif action == "list-files":
112
+ # python3 poffice_docs.py list-files repo_id [path]
113
+ path = sys.argv[3] if len(sys.argv) > 3 else "/"
114
+ print(json.dumps(admin.list_files(sys.argv[2], path), indent=4))
115
+ elif action == "upload":
116
+ # python3 poffice_docs.py upload repo_id file_path [target_path]
117
+ target = sys.argv[4] if len(sys.argv) > 4 else "/"
118
+ print(admin.upload_file(sys.argv[2], sys.argv[3], target))
119
+ elif action == "download":
120
+ # python3 poffice_docs.py download repo_id file_path save_as
121
+ print(admin.download_file(sys.argv[2], sys.argv[3], sys.argv[4]))
@@ -0,0 +1,70 @@
1
+ import os
2
+ from docx import Document
3
+ from openpyxl import Workbook, load_workbook
4
+ from reportlab.pdfgen import canvas
5
+ from reportlab.lib.pagesizes import letter
6
+
7
+ class PofficeDocumentGenerator:
8
+ @staticmethod
9
+ def create_word(path, title, content):
10
+ doc = Document()
11
+ doc.add_heading(title, 0)
12
+ doc.add_paragraph(content)
13
+ doc.save(path)
14
+ return path
15
+
16
+ @staticmethod
17
+ def update_word(path, new_paragraph):
18
+ doc = Document(path)
19
+ doc.add_paragraph(new_paragraph)
20
+ doc.save(path)
21
+ return path
22
+
23
+ @staticmethod
24
+ def create_excel(path, sheet_name, data):
25
+ """data should be a list of lists: [['Col1', 'Col2'], [1, 2]]"""
26
+ wb = Workbook()
27
+ ws = wb.active
28
+ ws.title = sheet_name
29
+ for row in data:
30
+ ws.append(row)
31
+ wb.save(path)
32
+ return path
33
+
34
+ @staticmethod
35
+ def update_excel(path, data):
36
+ wb = load_workbook(path)
37
+ ws = wb.active
38
+ for row in data:
39
+ ws.append(row)
40
+ wb.save(path)
41
+ return path
42
+
43
+ @staticmethod
44
+ def create_pdf(path, title, content):
45
+ c = canvas.Canvas(path, pagesize=letter)
46
+ width, height = letter
47
+ c.setFont("Helvetica-Bold", 16)
48
+ c.drawString(72, height - 72, title)
49
+ c.setFont("Helvetica", 12)
50
+
51
+ # Simple line splitter for content
52
+ textobject = c.beginText(72, height - 100)
53
+ for line in content.split('\n'):
54
+ textobject.textLine(line)
55
+ c.drawText(textobject)
56
+
57
+ c.showPage()
58
+ c.save()
59
+ return path
60
+
61
+ if __name__ == "__main__":
62
+ import sys
63
+ gen = PofficeDocumentGenerator()
64
+ if len(sys.argv) > 1:
65
+ action = sys.argv[1]
66
+ if action == "create-word":
67
+ # python3 poffice_gen.py create-word test.docx "My Title" "My Content"
68
+ print(gen.create_word(sys.argv[2], sys.argv[3], sys.argv[4]))
69
+ elif action == "create-pdf":
70
+ print(gen.create_pdf(sys.argv[2], sys.argv[3], sys.argv[4]))
@@ -0,0 +1,65 @@
1
+ import imaplib
2
+ import email
3
+ from email.header import decode_header
4
+ import os
5
+
6
+ class PofficeImap:
7
+ def __init__(self, user, password, imap_server="mail.poffice.online", imap_port=993):
8
+ self.user = user
9
+ self.password = password
10
+ self.imap_server = imap_server
11
+ self.imap_port = imap_port
12
+
13
+ def list_emails(self, folder="INBOX", limit=5):
14
+ try:
15
+ # Connect to server
16
+ mail = imaplib.IMAP4_SSL(self.imap_server, self.imap_port)
17
+ mail.login(self.user, self.password)
18
+ mail.select(folder)
19
+
20
+ # Search for all emails
21
+ status, messages = mail.search(None, "ALL")
22
+ if status != "OK":
23
+ return {"status": "error", "message": "Failed to search emails"}
24
+
25
+ # Get the list of email IDs
26
+ mail_ids = messages[0].split()
27
+ results = []
28
+
29
+ # Fetch the last 'limit' emails
30
+ for i in mail_ids[-limit:]:
31
+ status, data = mail.fetch(i, "(RFC822)")
32
+ if status != "OK":
33
+ continue
34
+
35
+ for response_part in data:
36
+ if isinstance(response_part, tuple):
37
+ msg = email.message_from_bytes(response_part[1])
38
+ subject, encoding = decode_header(msg["Subject"])[0]
39
+ if isinstance(subject, bytes):
40
+ subject = subject.decode(encoding if encoding else "utf-8")
41
+
42
+ from_ = msg.get("From")
43
+ results.append({
44
+ "id": i.decode(),
45
+ "from": from_,
46
+ "subject": subject,
47
+ "date": msg.get("Date")
48
+ })
49
+
50
+ mail.close()
51
+ mail.logout()
52
+ return {"status": "success", "emails": results}
53
+ except Exception as e:
54
+ return {"status": "error", "message": str(e)}
55
+
56
+ if __name__ == "__main__":
57
+ import sys
58
+ import json
59
+ if len(sys.argv) > 2:
60
+ user = sys.argv[1]
61
+ password = sys.argv[2]
62
+ imap = PofficeImap(user, password)
63
+ print(json.dumps(imap.list_emails(), indent=4))
64
+ else:
65
+ print("Usage: python3 poffice_imap.py <user> <password>")
@@ -0,0 +1,73 @@
1
+ import requests
2
+ import json
3
+ import os
4
+ import smtplib
5
+ from email.mime.text import MIMEText
6
+ from email.mime.multipart import MIMEMultipart
7
+
8
+ class PofficeMail:
9
+ def __init__(self, api_key, base_url="https://mail.poffice.online"):
10
+ self.api_key = api_key
11
+ self.base_url = base_url
12
+ self.headers = {
13
+ "X-API-Key": api_key,
14
+ "Content-Type": "application/json"
15
+ }
16
+
17
+ def get_domains(self):
18
+ # We use http to the container IP if on host, but here we use the public URL
19
+ # For the script to be robust, we allow overriding the base_url
20
+ response = requests.get(f"{self.base_url}/api/v1/get/domain/all", headers=self.headers, verify=False)
21
+ return response.json()
22
+
23
+ def create_mailbox(self, domain, local_part, name, password, quota=3072):
24
+ data = {
25
+ "address": f"{local_part}@{domain}",
26
+ "name": name,
27
+ "domain": domain,
28
+ "local_part": local_part,
29
+ "password": password,
30
+ "password2": password,
31
+ "quota": quota,
32
+ "active": "1"
33
+ }
34
+ response = requests.post(f"{self.base_url}/api/v1/add/mailbox", headers=self.headers, json=data, verify=False)
35
+ return response.json()
36
+
37
+ def send_email(self, smtp_user, smtp_pass, to_email, subject, body, smtp_server="mail.poffice.online", smtp_port=587):
38
+ msg = MIMEMultipart()
39
+ msg['From'] = smtp_user
40
+ msg['To'] = to_email
41
+ msg['Subject'] = subject
42
+ msg.attach(MIMEText(body, 'plain'))
43
+
44
+ try:
45
+ server = smtplib.SMTP(smtp_server, smtp_port)
46
+ server.starttls()
47
+ server.login(smtp_user, smtp_pass)
48
+ text = msg.as_string()
49
+ server.sendmail(smtp_user, to_email, text)
50
+ server.quit()
51
+ return {"status": "success"}
52
+ except Exception as e:
53
+ return {"status": "error", "message": str(e)}
54
+
55
+ if __name__ == "__main__":
56
+ # Example usage
57
+ import sys
58
+ # For testing, we can provide the API key as en env var
59
+ API_KEY = os.getenv("MAILCOW_API_KEY", "")
60
+ client = PofficeMail(API_KEY)
61
+
62
+ if len(sys.argv) > 1:
63
+ action = sys.argv[1]
64
+ if action == "list-domains":
65
+ print(json.dumps(client.get_domains(), indent=4))
66
+ elif action == "create-mailbox":
67
+ # python3 poffice_mail.py create-mailbox domain user name pass [quota]
68
+ domain = sys.argv[2]
69
+ user = sys.argv[3]
70
+ name = sys.argv[4]
71
+ passwd = sys.argv[5]
72
+ quota = int(sys.argv[6]) if len(sys.argv) > 6 else 3072
73
+ print(json.dumps(client.create_mailbox(domain, user, name, passwd, quota), indent=4))
@@ -0,0 +1,80 @@
1
+ import os
2
+ import psycopg2
3
+ from psycopg2.extras import RealDictCursor
4
+ import json
5
+ import subprocess
6
+
7
+ class PofficeMasterAdmin:
8
+ def __init__(self, db_url):
9
+ self.db_url = db_url
10
+
11
+ def get_user_from_neon(self, email):
12
+ conn = psycopg2.connect(self.db_url)
13
+ cur = conn.cursor(cursor_factory=RealDictCursor)
14
+ cur.execute('SELECT * FROM "User" WHERE email = %s', (email,))
15
+ user = cur.fetchone()
16
+ cur.close()
17
+ conn.close()
18
+ return user
19
+
20
+ def is_master_admin(self, email):
21
+ user = self.get_user_from_neon(email)
22
+ return user and user.get('isAdmin', False)
23
+
24
+ def synchronize_admin_access(self, email, password, name):
25
+ """
26
+ Ensures the user has administrative access or at least a mailbox
27
+ and accounts in all integrated apps.
28
+ """
29
+ if not self.is_master_admin(email):
30
+ return {"status": "error", "message": f"User {email} is not a master admin in Neon."}
31
+
32
+ results = {}
33
+
34
+ # 1. Sync Mailbox
35
+ try:
36
+ # We use the existing poffice_mail.py script
37
+ # Suppress warnings with -W ignore
38
+ cmd = ["python3", "-W", "ignore", ".agents/skills/poffice-admin/scripts/poffice_mail.py", "create-mailbox", "poffice.online", email.split('@')[0], name, password, "512"]
39
+ output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode()
40
+ results["mail"] = json.loads(output)
41
+ except Exception as e:
42
+ results["mail"] = {"status": "error", "message": f"Raw output: {output if 'output' in locals() else 'None'}. Error: {str(e)}"}
43
+
44
+ # 2. Sync Seafile
45
+ try:
46
+ cmd = ["python3", "-W", "ignore", ".agents/skills/poffice-admin/scripts/poffice_docs.py", "create-user", email, password, "true"]
47
+ output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode()
48
+ results["seafile"] = json.loads(output)
49
+ except Exception as e:
50
+ results["seafile"] = {"status": "error", "message": f"Raw output: {output if 'output' in locals() else 'None'}. Error: {str(e)}"}
51
+
52
+ # 3. Sync Paperless (Optional: depending on if paperless has per-user admin)
53
+ # For now we just acknowledge the check
54
+ results["paperless"] = {"status": "not_implemented", "message": "Manual token management required for Paperless-ngx admin access."}
55
+
56
+ return {"status": "success", "results": results}
57
+
58
+ if __name__ == "__main__":
59
+ import sys
60
+ # Load DB URL from .env or provided
61
+ DB_URL = os.getenv("DATABASE_URL", "")
62
+
63
+ manager = PofficeMasterAdmin(DB_URL)
64
+
65
+ if len(sys.argv) > 1:
66
+ action = sys.argv[1]
67
+ if action == "check":
68
+ # python3 poffice_master_admin.py check email
69
+ email = sys.argv[2]
70
+ print(json.dumps({"isAdmin": manager.is_master_admin(email)}, indent=4))
71
+ elif action == "sync":
72
+ # python3 poffice_master_admin.py sync email password name
73
+ email = sys.argv[2]
74
+ password = sys.argv[3]
75
+ name = sys.argv[4]
76
+ print(json.dumps(manager.synchronize_admin_access(email, password, name), indent=4))
77
+ else:
78
+ print("Unknown action")
79
+ else:
80
+ print("Usage: python3 poffice_master_admin.py <check|sync> <email> [password] [name]")
@@ -0,0 +1,43 @@
1
+ import requests
2
+ import json
3
+ import os
4
+
5
+ class PaperlessAdmin:
6
+ def __init__(self, token, base_url="https://docs.poffice.online"):
7
+ self.token = token
8
+ self.base_url = base_url
9
+ self.headers = {
10
+ "Authorization": f"Token {self.token}",
11
+ "Content-Type": "application/json"
12
+ }
13
+
14
+ def list_documents(self):
15
+ response = requests.get(f"{self.base_url}/api/documents/", headers=self.headers, verify=False)
16
+ return response.json()
17
+
18
+ def upload_document(self, file_path, title=None):
19
+ with open(file_path, "rb") as f:
20
+ files = {"document": f}
21
+ data = {}
22
+ if title:
23
+ data["title"] = title
24
+ response = requests.post(f"{self.base_url}/api/documents/post_document/", headers={"Authorization": f"Token {self.token}"}, files=files, data=data, verify=False)
25
+ return response.json()
26
+
27
+ if __name__ == "__main__":
28
+ import sys
29
+ # For Paperless, we need an API token.
30
+ # Paperless tokens are created in the Django admin or via a POST to /api/token/ (if enabled)
31
+ # For now, we assume the user provides it or we use a known one.
32
+ TOKEN = os.getenv("PAPERLESS_TOKEN")
33
+ if not TOKEN:
34
+ print("Error: PAPERLESS_TOKEN env var required")
35
+ sys.exit(1)
36
+
37
+ admin = PaperlessAdmin(TOKEN)
38
+ if len(sys.argv) > 1:
39
+ action = sys.argv[1]
40
+ if action == "list":
41
+ print(json.dumps(admin.list_documents(), indent=4))
42
+ elif action == "upload":
43
+ print(json.dumps(admin.upload_document(sys.argv[2]), indent=4))