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 +78 -0
- package/SKILL.md +65 -0
- package/package.json +28 -0
- package/references/credentials.md +1 -0
- package/references/mailcow_api.md +23 -0
- package/scripts/poffice_calendar.py +149 -0
- package/scripts/poffice_docs.py +121 -0
- package/scripts/poffice_gen.py +70 -0
- package/scripts/poffice_imap.py +65 -0
- package/scripts/poffice_mail.py +73 -0
- package/scripts/poffice_master_admin.py +80 -0
- package/scripts/poffice_paperless.py +43 -0
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))
|