mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-03-09 22:00:14 +01:00
Add CyberMail Email Delivery integration
- New emailDelivery Django app with full platform API integration - Account connection, domain management, SMTP credentials, relay config - Auto-configure SPF/DKIM/DMARC DNS records via PowerDNS - Postfix SMTP relay through CyberMail (configureRelayHost/removeRelayHost) - Real-time delivery logs, stats, and per-domain analytics - Single-page AngularJS dashboard with marketing landing page - Promotional banners on 6 email-related pages with dismiss cookie - Manual SQL table creation in upgrade.py for existing installs - Documentation: setup guide, technical reference, user guide
This commit is contained in:
@@ -76,6 +76,7 @@ INSTALLED_APPS = [
|
||||
'IncBackups',
|
||||
'aiScanner',
|
||||
'webmail',
|
||||
'emailDelivery',
|
||||
# 'WebTerminal'
|
||||
]
|
||||
|
||||
|
||||
@@ -46,5 +46,6 @@ urlpatterns = [
|
||||
path('IncrementalBackups/', include('IncBackups.urls')),
|
||||
path('aiscanner/', include('aiScanner.urls')),
|
||||
path('webmail/', include('webmail.urls')),
|
||||
path('emailDelivery/', include('emailDelivery.urls')),
|
||||
# path('Terminal/', include('WebTerminal.urls')),
|
||||
]
|
||||
|
||||
@@ -1937,8 +1937,12 @@
|
||||
<a href="{% url 'Rspamd' %}" class="menu-item">
|
||||
<span>RSPAMD</span>
|
||||
</a>
|
||||
<a href="{% url 'emailDeliveryHome' %}" class="menu-item">
|
||||
<span>Email Delivery</span>
|
||||
<span class="badge">NEW</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<a href="#" class="menu-item" onclick="toggleSubmenu('manage-services-submenu', this); return false;">
|
||||
<div class="icon-wrapper">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
|
||||
266
docs/CYBERMAIL_SETUP_GUIDE.md
Normal file
266
docs/CYBERMAIL_SETUP_GUIDE.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# CyberMail Email Delivery — Setup & Administration Guide
|
||||
|
||||
**Feature**: CyberMail Email Delivery Integration
|
||||
**CyberPanel Version**: 2.4.5+
|
||||
**Platform**: https://platform.cyberpersons.com
|
||||
**Last Updated**: 2026-03-06
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
CyberMail Email Delivery is a built-in CyberPanel feature that routes outgoing emails through CyberMail's optimized delivery infrastructure. It solves common email deliverability problems — emails landing in spam, IP blacklisting, missing DNS records — by providing dedicated sending servers, automatic DNS configuration, and real-time delivery analytics.
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- **15,000 emails/month free** on the Free plan
|
||||
- **Automatic DNS setup** — SPF, DKIM, and DMARC records configured in one click
|
||||
- **SMTP relay** — route all server email through CyberMail with one toggle
|
||||
- **Real-time analytics** — delivery logs, bounce tracking, reputation monitoring
|
||||
- **Multi-region delivery** — 4 delivery nodes with 99.9% uptime SLA
|
||||
- **98%+ inbox rate** across major providers (Gmail, Outlook, Yahoo)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. CyberPanel 2.4.5 or later installed
|
||||
2. Active internet connection from the server
|
||||
3. PowerDNS running (for automatic DNS configuration)
|
||||
4. Postfix installed (for SMTP relay feature)
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
The CyberMail module is included in CyberPanel 2.4.5+. No separate installation needed.
|
||||
|
||||
### Verify the Module
|
||||
|
||||
```bash
|
||||
# Check the app exists
|
||||
ls /usr/local/CyberCP/emailDelivery/
|
||||
|
||||
# Check it's in INSTALLED_APPS
|
||||
grep -n "emailDelivery" /usr/local/CyberCP/CyberCP/settings.py
|
||||
|
||||
# Run migrations if needed
|
||||
cd /usr/local/CyberCP
|
||||
python manage.py migrate emailDelivery
|
||||
```
|
||||
|
||||
### Database Tables
|
||||
|
||||
The module creates two tables:
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `cybermail_accounts` | Stores per-admin CyberMail account connections |
|
||||
| `cybermail_domains` | Tracks sending domains and their verification status |
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Step 1: Access CyberMail
|
||||
|
||||
Navigate to: **https://your-server:8090/emailDelivery/**
|
||||
|
||||
You'll see the CyberMail marketing page with plan information and a "Get Started Free" button.
|
||||
|
||||
### Step 2: Connect Your Account
|
||||
|
||||
1. Click **"Get Started Free"**
|
||||
2. Enter your email address (defaults to your CyberPanel admin email)
|
||||
3. Create a password for your CyberMail account
|
||||
4. Click **Connect**
|
||||
|
||||
This registers your account on the CyberMail platform and obtains an API key that's stored locally for future API calls.
|
||||
|
||||
> **Note**: If you already have a CyberMail account on the platform, use the same email and password. The system will link your existing account.
|
||||
|
||||
### Step 3: Add Sending Domains
|
||||
|
||||
1. After connecting, you'll see the dashboard
|
||||
2. Go to the **Domains** tab
|
||||
3. Click **"Add Domain"**
|
||||
4. Enter your domain name (e.g., `example.com`)
|
||||
5. Click **Add**
|
||||
|
||||
The system will:
|
||||
- Register the domain on the CyberMail platform
|
||||
- Automatically create SPF, DKIM, and DMARC DNS records in PowerDNS
|
||||
- Report how many DNS records were configured
|
||||
|
||||
### Step 4: Verify Domain
|
||||
|
||||
1. Click **"Verify"** next to your domain
|
||||
2. The system checks SPF, DKIM, and DMARC records
|
||||
3. Green checkmarks appear for each verified record
|
||||
4. Status changes to "Verified" when all records pass
|
||||
|
||||
> **DNS Propagation**: If verification fails immediately after adding, wait 5-10 minutes for DNS propagation and try again.
|
||||
|
||||
### Step 5: Enable SMTP Relay (Optional)
|
||||
|
||||
The SMTP relay routes ALL outgoing email from your server through CyberMail:
|
||||
|
||||
1. Go to the **Relay** tab
|
||||
2. Click **"Enable Relay"**
|
||||
3. The system will:
|
||||
- Create (or rotate) SMTP credentials on the platform
|
||||
- Configure Postfix with the relay host (`mail.cyberpersons.com:587`)
|
||||
- Set up SASL authentication
|
||||
- Enable TLS encryption
|
||||
|
||||
**What gets configured in Postfix** (`/etc/postfix/main.cf`):
|
||||
```
|
||||
relayhost = [mail.cyberpersons.com]:587
|
||||
smtp_sasl_auth_enable = yes
|
||||
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
|
||||
smtp_sasl_security_options = noanonymous
|
||||
smtp_tls_security_level = encrypt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Overview
|
||||
|
||||
After connecting, the dashboard provides five tabs:
|
||||
|
||||
### Domains Tab
|
||||
- Lists all sending domains with verification status
|
||||
- SPF, DKIM, DMARC status badges (green = verified)
|
||||
- Actions: Verify, Auto-Configure DNS, Remove
|
||||
- DNS auto-configuration works when the domain exists in PowerDNS
|
||||
|
||||
### SMTP Tab
|
||||
- Manage SMTP credentials for sending
|
||||
- Create new credentials with descriptions
|
||||
- Rotate passwords (one-time display)
|
||||
- Delete unused credentials
|
||||
|
||||
### Relay Tab
|
||||
- Shows current relay status (Enabled/Disabled)
|
||||
- Displays relay host and port
|
||||
- Enable/Disable toggle
|
||||
- Relay info: `mail.cyberpersons.com:587` with STARTTLS
|
||||
|
||||
### Logs Tab
|
||||
- Paginated delivery logs
|
||||
- Filter by: Status (delivered/bounced/failed), Days (1-30)
|
||||
- Shows: Date, From, To, Subject, Status
|
||||
- Color-coded status badges
|
||||
|
||||
### Stats Tab
|
||||
- Aggregate stats: Total Sent, Delivered, Bounced, Failed, Delivery Rate
|
||||
- Per-domain breakdown table
|
||||
- Useful for identifying domains with deliverability issues
|
||||
|
||||
---
|
||||
|
||||
## Plans & Pricing
|
||||
|
||||
| Plan | Price | Emails/Month | Features |
|
||||
|------|-------|-------------|----------|
|
||||
| **Free** | $0 | 15,000 | Shared infrastructure, basic analytics |
|
||||
| **Starter** | $15/mo | 100,000 | Priority support, advanced analytics |
|
||||
| **Professional** | $90/mo | 500,000 | Dedicated IPs, custom DKIM, webhooks |
|
||||
| **Enterprise** | $299/mo | 2,000,000 | SLA guarantee, account manager, custom limits |
|
||||
|
||||
Upgrade at: https://platform.cyberpersons.com
|
||||
|
||||
---
|
||||
|
||||
## Promotional Banners
|
||||
|
||||
CyberMail banners appear on email-related pages to inform users about the delivery service:
|
||||
|
||||
- Mail Functions (`/mailServer/`)
|
||||
- Create Email Account
|
||||
- DKIM Manager
|
||||
- Webmail
|
||||
- Email Premium
|
||||
- Email Marketing
|
||||
|
||||
Banners are dismissible with a 7-day cookie-based suppression. They show:
|
||||
- "Stop Landing in Spam" headline
|
||||
- Brief feature description
|
||||
- "Get Started Free" CTA linking to `/emailDelivery/`
|
||||
|
||||
---
|
||||
|
||||
## Disconnecting
|
||||
|
||||
1. Go to **https://your-server:8090/emailDelivery/**
|
||||
2. Click the **"Disconnect"** button
|
||||
3. Confirm the action
|
||||
|
||||
Disconnecting will:
|
||||
- Disable SMTP relay if active (remove Postfix relay config)
|
||||
- Clear the stored API key
|
||||
- Remove local domain records
|
||||
- Reset SMTP credential references
|
||||
|
||||
> **Note**: Your CyberMail platform account is NOT deleted. You can reconnect later with the same credentials.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Account not connected" error
|
||||
The admin session doesn't have an active CyberMail connection. Click "Get Started Free" to connect.
|
||||
|
||||
### DNS records not auto-configured
|
||||
- The domain must exist in PowerDNS on this server
|
||||
- Check if the domain was created via CyberPanel's DNS management
|
||||
- If using external DNS, add records manually using the DNS records shown on the platform
|
||||
|
||||
### Domain verification failing
|
||||
- Wait 5-10 minutes after DNS changes for propagation
|
||||
- Verify records exist: `dig TXT example.com +short`
|
||||
- Check for conflicting SPF records (only one SPF record allowed per domain)
|
||||
|
||||
### SMTP relay not working
|
||||
- Check Postfix status: `systemctl status postfix`
|
||||
- Verify relay config: `grep relayhost /etc/postfix/main.cf`
|
||||
- Check SASL credentials: `cat /etc/postfix/sasl_passwd`
|
||||
- Test connectivity: `telnet mail.cyberpersons.com 587`
|
||||
- Check mail queue: `mailq`
|
||||
- View Postfix logs: `tail -f /var/log/mail.log`
|
||||
|
||||
### Relay shows "Failed to configure"
|
||||
- Ensure `/usr/local/CyberCP/plogical/mailUtilities.py` has the `configureRelayHost` method
|
||||
- Check file permissions on `/etc/postfix/sasl_passwd`
|
||||
- Verify Postfix is installed and running
|
||||
|
||||
### Emails still going to spam
|
||||
1. Verify all DNS records (SPF, DKIM, DMARC) are green
|
||||
2. Check your domain's reputation at https://www.mail-tester.com
|
||||
3. Ensure you're not sending to purchased/scraped lists
|
||||
4. Consider upgrading to a plan with dedicated IPs
|
||||
|
||||
---
|
||||
|
||||
## File Reference
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `emailDelivery/emailDeliveryManager.py` | Core business logic, platform API calls |
|
||||
| `emailDelivery/models.py` | Database models (CyberMailAccount, CyberMailDomain) |
|
||||
| `emailDelivery/views.py` | Django view functions (thin wrappers) |
|
||||
| `emailDelivery/urls.py` | URL routing (18 endpoints) |
|
||||
| `emailDelivery/static/emailDelivery/emailDelivery.js` | AngularJS controller |
|
||||
| `emailDelivery/templates/emailDelivery/index.html` | Single-page template (marketing + dashboard) |
|
||||
| `plogical/mailUtilities.py` | Postfix relay configuration (configureRelayHost/removeRelayHost) |
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
- API keys are stored per-admin in the local database, never in config files
|
||||
- All platform API calls use HTTPS with Bearer token authentication
|
||||
- SMTP credentials use SASL over TLS (STARTTLS on port 587)
|
||||
- SASL password file is chmod 600 (root-only readable)
|
||||
- Session-based authentication with CSRF protection on all endpoints
|
||||
- Passwords are never stored locally — only on the platform
|
||||
393
docs/CYBERMAIL_TECHNICAL_REFERENCE.md
Normal file
393
docs/CYBERMAIL_TECHNICAL_REFERENCE.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# CyberMail Email Delivery — Technical Reference
|
||||
|
||||
**Module**: `emailDelivery`
|
||||
**Platform API Base**: `https://platform.cyberpersons.com/email/cp/`
|
||||
**Last Updated**: 2026-03-06
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
CyberPanel UI (AngularJS)
|
||||
|
|
||||
v
|
||||
Django Views (emailDelivery/views.py)
|
||||
|
|
||||
v
|
||||
EmailDeliveryManager (emailDelivery/emailDeliveryManager.py)
|
||||
|
|
||||
├──> CyberMail Platform API (HTTPS POST, Bearer auth)
|
||||
├──> PowerDNS (via dnsUtilities.DNS.createDNSRecord)
|
||||
└──> Postfix (via mailUtilities.py subprocess as root)
|
||||
```
|
||||
|
||||
### Design Patterns
|
||||
|
||||
- **Manager Class Pattern**: All business logic in `EmailDeliveryManager`, views are thin wrappers
|
||||
- **Per-User API Keys**: Each CyberPanel admin gets their own platform API key (no server-level key)
|
||||
- **AngularJS SPA**: Single template with conditional rendering based on `isConnected` state
|
||||
- **Subprocess for Root Operations**: Postfix relay config runs via `ProcessUtilities.outputExecutioner()` which executes as root
|
||||
|
||||
---
|
||||
|
||||
## Database Models
|
||||
|
||||
### CyberMailAccount (`cybermail_accounts`)
|
||||
|
||||
```python
|
||||
class CyberMailAccount(models.Model):
|
||||
admin = OneToOneField(Administrator, CASCADE) # 1:1 with admin
|
||||
platform_account_id = IntegerField(null=True) # Platform's account ID
|
||||
api_key = CharField(max_length=255) # Per-user Bearer token
|
||||
email = CharField(max_length=255) # Platform email
|
||||
plan_name = CharField(default='Free') # Display name
|
||||
plan_slug = CharField(default='free') # free/starter/professional/enterprise
|
||||
emails_per_month = IntegerField(default=15000) # Plan limit
|
||||
is_connected = BooleanField(default=False) # Active connection
|
||||
relay_enabled = BooleanField(default=False) # Postfix relay active
|
||||
smtp_credential_id = IntegerField(null=True) # Active relay credential
|
||||
smtp_username = CharField(max_length=255) # Relay SMTP username
|
||||
smtp_host = CharField(default='mail.cyberpersons.com')
|
||||
smtp_port = IntegerField(default=587)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
updated_at = DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
### CyberMailDomain (`cybermail_domains`)
|
||||
|
||||
```python
|
||||
class CyberMailDomain(models.Model):
|
||||
account = ForeignKey(CyberMailAccount, CASCADE)
|
||||
domain = CharField(max_length=255)
|
||||
platform_domain_id = IntegerField(null=True)
|
||||
status = CharField(default='pending') # pending/verified
|
||||
spf_verified = BooleanField(default=False)
|
||||
dkim_verified = BooleanField(default=False)
|
||||
dmarc_verified = BooleanField(default=False)
|
||||
dns_configured = BooleanField(default=False) # Auto-configured in PowerDNS
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints (CyberPanel Internal)
|
||||
|
||||
All endpoints are POST, require authenticated CyberPanel session, and return JSON.
|
||||
|
||||
### Account Management
|
||||
|
||||
| Endpoint | Method | Purpose | Request Body |
|
||||
|----------|--------|---------|-------------|
|
||||
| `/emailDelivery/` | GET | Render page | — |
|
||||
| `/emailDelivery/connect/` | POST | Register/connect account | `{email, password}` |
|
||||
| `/emailDelivery/status/` | POST | Get account status + sync domains | — |
|
||||
| `/emailDelivery/disconnect/` | POST | Disconnect account, cleanup | — |
|
||||
|
||||
### Domain Management
|
||||
|
||||
| Endpoint | Method | Purpose | Request Body |
|
||||
|----------|--------|---------|-------------|
|
||||
| `/emailDelivery/domains/add/` | POST | Add sending domain | `{domain}` |
|
||||
| `/emailDelivery/domains/list/` | POST | List domains with status | — |
|
||||
| `/emailDelivery/domains/verify/` | POST | Verify DNS records | `{domain}` |
|
||||
| `/emailDelivery/domains/dns-records/` | POST | Get required DNS records | `{domain}` |
|
||||
| `/emailDelivery/domains/auto-configure-dns/` | POST | Auto-add DNS to PowerDNS | `{domain}` |
|
||||
| `/emailDelivery/domains/remove/` | POST | Remove sending domain | `{domain}` |
|
||||
|
||||
### SMTP Credentials
|
||||
|
||||
| Endpoint | Method | Purpose | Request Body |
|
||||
|----------|--------|---------|-------------|
|
||||
| `/emailDelivery/smtp/create/` | POST | Create SMTP credential | `{description}` |
|
||||
| `/emailDelivery/smtp/list/` | POST | List all credentials | — |
|
||||
| `/emailDelivery/smtp/rotate/` | POST | Rotate credential password | `{credential_id}` |
|
||||
| `/emailDelivery/smtp/delete/` | POST | Delete credential | `{credential_id}` |
|
||||
|
||||
### Relay
|
||||
|
||||
| Endpoint | Method | Purpose | Request Body |
|
||||
|----------|--------|---------|-------------|
|
||||
| `/emailDelivery/relay/enable/` | POST | Enable Postfix SMTP relay | — |
|
||||
| `/emailDelivery/relay/disable/` | POST | Disable Postfix SMTP relay | — |
|
||||
|
||||
### Analytics
|
||||
|
||||
| Endpoint | Method | Purpose | Request Body |
|
||||
|----------|--------|---------|-------------|
|
||||
| `/emailDelivery/stats/` | POST | Aggregate sending stats | — |
|
||||
| `/emailDelivery/stats/domains/` | POST | Per-domain stats | — |
|
||||
| `/emailDelivery/logs/` | POST | Paginated delivery logs | `{page, per_page, status, from_domain, days}` |
|
||||
|
||||
### Health
|
||||
|
||||
| Endpoint | Method | Purpose | Request Body |
|
||||
|----------|--------|---------|-------------|
|
||||
| `/emailDelivery/health/` | POST | Platform health check | — |
|
||||
|
||||
---
|
||||
|
||||
## Platform API Mapping
|
||||
|
||||
Each CyberPanel endpoint maps to a platform API call:
|
||||
|
||||
| CyberPanel Method | Platform Endpoint | Auth | Notes |
|
||||
|-------------------|-------------------|------|-------|
|
||||
| `connect()` | `api/register/` | None (public) | Returns `api_key` for future calls |
|
||||
| `getStatus()` | `api/account/` + `api/domains/list/` | Bearer | Syncs plan + domains |
|
||||
| `addDomain()` | `api/domains/add/` + `api/domains/dns-records/` | Bearer | Auto-configures DNS |
|
||||
| `listDomains()` | `api/domains/list/` | Bearer | Syncs verification status |
|
||||
| `verifyDomain()` | `api/domains/verify/` | Bearer | Returns spf/dkim/dmarc booleans |
|
||||
| `getDnsRecords()` | `api/domains/dns-records/` | Bearer | Returns required records |
|
||||
| `removeDomain()` | `api/domains/remove/` | Bearer | — |
|
||||
| `createSmtpCredential()` | `api/smtp/create/` | Bearer | Returns one-time password |
|
||||
| `listSmtpCredentials()` | `api/smtp/list/` | Bearer | Normalizes `id` → `credential_id` |
|
||||
| `rotateSmtpPassword()` | `api/smtp/rotate/` | Bearer | Normalizes `new_password` → `password` |
|
||||
| `deleteSmtpCredential()` | `api/smtp/delete/` | Bearer | Clears relay if active credential |
|
||||
| `enableRelay()` | `api/smtp/create/` or `api/smtp/rotate/` | Bearer | Then configures Postfix |
|
||||
| `getStats()` | `api/stats/` | Bearer | — |
|
||||
| `getDomainStats()` | `api/stats/domains/` | Bearer | Converts dict → array |
|
||||
| `getLogs()` | `api/logs/` | Bearer | Maps field names for JS |
|
||||
| `checkStatus()` | `api/health/` | None | — |
|
||||
|
||||
### API Response Normalization
|
||||
|
||||
The manager normalizes platform responses for frontend compatibility:
|
||||
|
||||
| Platform Field | CyberPanel Field | Method |
|
||||
|---------------|-----------------|--------|
|
||||
| `id` (credential) | `credential_id` | `listSmtpCredentials()` |
|
||||
| `new_password` | `password` | `rotateSmtpPassword()` |
|
||||
| `queued_at` | `date` | `getLogs()` |
|
||||
| `from_email` | `from` | `getLogs()` |
|
||||
| `to_email` | `to` | `getLogs()` |
|
||||
| `domains` (dict) | `domains` (array) | `getDomainStats()` |
|
||||
|
||||
---
|
||||
|
||||
## Connection Flow
|
||||
|
||||
```
|
||||
1. User clicks "Get Started Free"
|
||||
2. JS sends POST /emailDelivery/connect/ {email, password}
|
||||
3. Manager calls platform POST api/register/ (no auth)
|
||||
4. Platform returns {success, data: {api_key, account_id, plan_name, ...}}
|
||||
5. Manager creates/updates CyberMailAccount with api_key
|
||||
6. All subsequent calls use Authorization: Bearer <api_key>
|
||||
```
|
||||
|
||||
### Reconnection Behavior
|
||||
|
||||
When connecting with an existing `CyberMailAccount` record:
|
||||
- Updates email, api_key, platform_account_id, plan info
|
||||
- Resets: `smtp_credential_id=None`, `smtp_username=''`, `relay_enabled=False`
|
||||
- Deletes all local `CyberMailDomain` records (stale data)
|
||||
|
||||
### Disconnection Behavior
|
||||
|
||||
- Disables Postfix relay if active
|
||||
- Clears: `is_connected`, `relay_enabled`, `api_key`, `smtp_credential_id`, `smtp_username`, `platform_account_id`
|
||||
- Deletes all local domain records
|
||||
- Does NOT delete the platform account
|
||||
|
||||
---
|
||||
|
||||
## DNS Auto-Configuration
|
||||
|
||||
### Flow
|
||||
|
||||
```
|
||||
1. User adds domain via addDomain()
|
||||
2. Manager calls platform api/domains/add/
|
||||
3. Manager calls _autoConfigureDnsForDomain()
|
||||
4. → Finds domain zone in PowerDNS (dns.models.Domains)
|
||||
5. → Calls platform api/domains/dns-records/ to get required records
|
||||
6. → For each record: DNS.createDNSRecord(zone, host, type, value, priority, ttl)
|
||||
7. → Marks CyberMailDomain.dns_configured = True
|
||||
8. User clicks "Verify" → calls platform api/domains/verify/
|
||||
9. Platform checks DNS → returns spf/dkim/dmarc booleans
|
||||
```
|
||||
|
||||
### DNS Record Types Created
|
||||
|
||||
| Record | Type | Example Value |
|
||||
|--------|------|---------------|
|
||||
| SPF | TXT | `v=spf1 include:spf.cyberpersons.com ~all` |
|
||||
| DKIM | TXT | `v=DKIM1; k=rsa; p=MIIBIjANBg...` |
|
||||
| DMARC | TXT | `v=DMARC1; p=quarantine; rua=mailto:dmarc@...` |
|
||||
|
||||
### When Auto-Configuration Fails
|
||||
|
||||
- Domain not in PowerDNS → returns message to add records manually
|
||||
- Platform API unreachable → returns connection error
|
||||
- Individual record creation fails → logged, continues with remaining records
|
||||
|
||||
---
|
||||
|
||||
## SMTP Relay Configuration
|
||||
|
||||
### Enable Relay Flow
|
||||
|
||||
```
|
||||
1. Manager checks account.smtp_credential_id
|
||||
2. If no credential:
|
||||
→ POST api/smtp/create/ {email, description: "CyberPanel Relay"}
|
||||
→ Stores credential_id, username, gets one-time password
|
||||
3. If credential exists:
|
||||
→ POST api/smtp/rotate/ {email, credential_id}
|
||||
→ Gets new_password
|
||||
4. Calls subprocess: python mailUtilities.py configureRelayHost
|
||||
--smtpHost mail.cyberpersons.com --smtpPort 587
|
||||
--smtpUser <username> --smtpPassword <password>
|
||||
5. mailUtilities.py (runs as root):
|
||||
→ Writes /etc/postfix/main.cf relay lines
|
||||
→ Writes /etc/postfix/sasl_passwd
|
||||
→ chmod 600, postmap, systemctl reload postfix
|
||||
6. Sets account.relay_enabled = True
|
||||
```
|
||||
|
||||
### Postfix Configuration Applied
|
||||
|
||||
```ini
|
||||
# Added to /etc/postfix/main.cf
|
||||
relayhost = [mail.cyberpersons.com]:587
|
||||
smtp_sasl_auth_enable = yes
|
||||
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
|
||||
smtp_sasl_security_options = noanonymous
|
||||
smtp_tls_security_level = encrypt
|
||||
```
|
||||
|
||||
```
|
||||
# /etc/postfix/sasl_passwd
|
||||
[mail.cyberpersons.com]:587 username:password
|
||||
```
|
||||
|
||||
### Disable Relay
|
||||
|
||||
```
|
||||
1. Calls subprocess: python mailUtilities.py removeRelayHost
|
||||
2. mailUtilities.py removes relay lines from main.cf
|
||||
3. Restores smtp_tls_security_level = may
|
||||
4. Deletes sasl_passwd and .db files
|
||||
5. Reloads Postfix
|
||||
6. Sets account.relay_enabled = False
|
||||
```
|
||||
|
||||
### Output Parsing
|
||||
|
||||
The subprocess prints `1,None` on success. The manager checks for `'1,None' in output` (not `startswith`) because Python SyntaxWarnings may appear before the success output.
|
||||
|
||||
---
|
||||
|
||||
## Frontend Architecture
|
||||
|
||||
### Template: `emailDelivery/templates/emailDelivery/index.html`
|
||||
|
||||
- Extends `baseTemplate/index.html`
|
||||
- Uses AngularJS 1.6.5 with `{$ $}` interpolation (not `{{ }}`)
|
||||
- CSS classes use `ed-` prefix
|
||||
- Two views controlled by Django `{% if isConnected %}`:
|
||||
- Marketing landing page (not connected)
|
||||
- Dashboard with tabs (connected)
|
||||
|
||||
### Controller: `emailDeliveryCtrl`
|
||||
|
||||
Located in `emailDelivery/static/emailDelivery/emailDelivery.js`
|
||||
|
||||
Key state variables:
|
||||
```javascript
|
||||
$scope.isConnected // Boolean — dashboard vs marketing view
|
||||
$scope.activeTab // 'domains' | 'smtp' | 'relay' | 'logs' | 'stats'
|
||||
$scope.account // Account object from getStatus
|
||||
$scope.domains // Array of domain objects
|
||||
$scope.smtpCredentials // Array of SMTP credential objects
|
||||
$scope.stats // Aggregate stats object
|
||||
$scope.domainStats // Array of per-domain stats
|
||||
$scope.logs // Array of log entries
|
||||
$scope.logFilters // {status, from_domain, days}
|
||||
$scope.logsPage // Current log page number
|
||||
$scope.logsTotalPages // Total log pages
|
||||
```
|
||||
|
||||
### Modal Handling
|
||||
|
||||
Bootstrap 3 modals are placed OUTSIDE the `ng-controller` div (AngularJS scope limitation). Modal forms use jQuery + `onclick` handlers that call standalone functions (`cmConnect()`, `cmAddDomain()`, etc.) which make AJAX calls with `$.ajax()` and CSRF tokens.
|
||||
|
||||
### CSRF Token
|
||||
|
||||
All AJAX calls include the CSRF token from cookies:
|
||||
```javascript
|
||||
function getCookie(name) {
|
||||
var value = "; " + document.cookie;
|
||||
var parts = value.split("; " + name + "=");
|
||||
if (parts.length == 2) return parts.pop().split(";").shift();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### API Errors
|
||||
- Connection timeout: 30-second timeout on all platform API calls
|
||||
- Connection errors: caught and returned as `{success: false, error: "Could not connect..."}`
|
||||
- Missing API key: `{success: false, error: "No API key found. Please reconnect your account."}`
|
||||
|
||||
### Logging
|
||||
All errors logged via `CyberCPLogFileWriter.writeToFile()` with format:
|
||||
```
|
||||
[EmailDeliveryManager.<method>] Error: <message>
|
||||
```
|
||||
|
||||
Log file: `/home/cyberpanel/error-logs.txt`
|
||||
|
||||
---
|
||||
|
||||
## Banner System
|
||||
|
||||
Promotional banners appear on 6 email-related pages:
|
||||
|
||||
| Page | Template Path |
|
||||
|------|--------------|
|
||||
| Mail Functions | `mailServer/templates/mailServer/index.html` |
|
||||
| Create Email | `mailServer/templates/mailServer/createEmailAccount.html` |
|
||||
| DKIM Manager | `mailServer/templates/mailServer/dkimManager.html` |
|
||||
| Webmail | `webmail/templates/webmail/index.html` |
|
||||
| Email Premium | `emailPremium/templates/emailPremium/emailPage.html` |
|
||||
| Email Marketing | `emailMarketing/templates/emailMarketing/emailMarketing.html` |
|
||||
|
||||
### Banner Behavior
|
||||
- Hidden by default (`display:none`)
|
||||
- Shown via JS if `cybermail_dismiss=1` cookie is NOT present
|
||||
- Dismiss button sets cookie with 7-day expiry (`max-age=604800`)
|
||||
- Links to `/emailDelivery/`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
emailDelivery/
|
||||
├── __init__.py
|
||||
├── apps.py # Django app config
|
||||
├── models.py # CyberMailAccount, CyberMailDomain
|
||||
├── views.py # Thin view wrappers (18 endpoints)
|
||||
├── urls.py # URL patterns
|
||||
├── emailDeliveryManager.py # Core business logic (~743 lines)
|
||||
├── migrations/
|
||||
│ └── __init__.py
|
||||
├── static/
|
||||
│ └── emailDelivery/
|
||||
│ └── emailDelivery.js # AngularJS controller
|
||||
└── templates/
|
||||
└── emailDelivery/
|
||||
└── index.html # SPA template (marketing + dashboard)
|
||||
```
|
||||
|
||||
### Related Files
|
||||
|
||||
| File | Modifications |
|
||||
|------|--------------|
|
||||
| `CyberCP/settings.py` | Added `'emailDelivery'` to `INSTALLED_APPS` |
|
||||
| `CyberCP/urls.py` | Added `path('emailDelivery/', include('emailDelivery.urls'))` |
|
||||
| `plogical/mailUtilities.py` | Added `configureRelayHost()` and `removeRelayHost()` static methods + argparse args |
|
||||
188
docs/CYBERMAIL_USER_GUIDE.md
Normal file
188
docs/CYBERMAIL_USER_GUIDE.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# CyberMail Email Delivery — User Guide
|
||||
|
||||
**For**: CyberPanel users and resellers
|
||||
**Last Updated**: 2026-03-06
|
||||
|
||||
---
|
||||
|
||||
## What is CyberMail?
|
||||
|
||||
CyberMail is CyberPanel's built-in email delivery service. It routes your outgoing emails through optimized servers so they land in the inbox instead of spam. Every CyberPanel installation includes CyberMail with a free tier of 15,000 emails per month.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Open CyberMail
|
||||
|
||||
Log into CyberPanel and navigate to: **Email > Email Delivery** or go directly to `https://your-server:8090/emailDelivery/`
|
||||
|
||||
### 2. Create Your Account
|
||||
|
||||
- Click **"Get Started Free"**
|
||||
- Enter your email address
|
||||
- Choose a password
|
||||
- Click **Connect**
|
||||
|
||||
You'll immediately get access to the dashboard with the Free plan (15,000 emails/month).
|
||||
|
||||
### 3. Add Your Domain
|
||||
|
||||
- Go to the **Domains** tab
|
||||
- Click **"Add Domain"**
|
||||
- Type your domain name (e.g., `mydomain.com`)
|
||||
- Click **Add**
|
||||
|
||||
CyberMail will automatically set up the DNS records needed for email delivery (SPF, DKIM, DMARC). If your domain's DNS is managed by CyberPanel's PowerDNS, this happens instantly.
|
||||
|
||||
### 4. Verify Your Domain
|
||||
|
||||
- Click **"Verify"** next to your domain
|
||||
- Check that SPF, DKIM, and DMARC all show green checkmarks
|
||||
- If any are red, wait a few minutes for DNS propagation and verify again
|
||||
|
||||
### 5. Start Sending
|
||||
|
||||
Once your domain is verified, emails sent from that domain will benefit from CyberMail's delivery optimization. For maximum deliverability, enable the SMTP relay.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Domain Management
|
||||
|
||||
Add multiple sending domains to your CyberMail account. Each domain gets:
|
||||
|
||||
- **SPF record** — tells receivers your emails are authorized
|
||||
- **DKIM signing** — cryptographically signs your emails to prevent tampering
|
||||
- **DMARC policy** — instructs receivers how to handle unauthenticated emails
|
||||
|
||||
**Status indicators:**
|
||||
- Gray badge = Not verified
|
||||
- Green badge = Verified and active
|
||||
|
||||
**Actions:**
|
||||
- **Verify** — recheck DNS records
|
||||
- **Auto DNS** — reconfigure DNS records in PowerDNS (if records were deleted)
|
||||
- **Remove** — remove the domain from CyberMail
|
||||
|
||||
### SMTP Credentials
|
||||
|
||||
Create credentials for sending emails through CyberMail's SMTP servers:
|
||||
|
||||
- **Create** — generates a username and one-time password
|
||||
- **Rotate** — generates a new password (old one stops working)
|
||||
- **Delete** — permanently removes the credential
|
||||
|
||||
> **Important**: The password is shown only once when created or rotated. Copy it immediately.
|
||||
|
||||
**SMTP Settings for manual configuration:**
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Host | `mail.cyberpersons.com` |
|
||||
| Port | `587` |
|
||||
| Security | STARTTLS |
|
||||
| Authentication | Login (SASL) |
|
||||
| Username | Shown after creation |
|
||||
| Password | Shown once after creation |
|
||||
|
||||
### SMTP Relay
|
||||
|
||||
The easiest way to use CyberMail — route ALL outgoing email from your server automatically:
|
||||
|
||||
- **Enable** — one click to configure everything
|
||||
- **Disable** — one click to revert to direct sending
|
||||
|
||||
When enabled, every email your server sends (from all websites, all email accounts) goes through CyberMail. No application-level changes needed.
|
||||
|
||||
### Delivery Logs
|
||||
|
||||
Monitor every email sent through CyberMail:
|
||||
|
||||
- **Filter by status**: All, Delivered, Bounced, Failed, Deferred
|
||||
- **Filter by time**: Last 1, 3, 7, 14, or 30 days
|
||||
- **View details**: Date, sender, recipient, subject, delivery status
|
||||
|
||||
### Statistics
|
||||
|
||||
Track your email performance:
|
||||
|
||||
- **Total Sent** — emails sent this billing period
|
||||
- **Delivered** — successfully delivered to recipient
|
||||
- **Bounced** — rejected by recipient server
|
||||
- **Failed** — permanent delivery failures
|
||||
- **Delivery Rate** — percentage of successful deliveries
|
||||
- **Per-Domain Breakdown** — stats for each sending domain
|
||||
|
||||
### Usage Tracking
|
||||
|
||||
The dashboard shows a progress bar of your monthly email usage:
|
||||
- Green = under 80% usage
|
||||
- Yellow/warning = approaching limit
|
||||
- An upgrade banner appears when you're near your plan limit
|
||||
|
||||
---
|
||||
|
||||
## Plans
|
||||
|
||||
| | Free | Starter | Professional | Enterprise |
|
||||
|---|---|---|---|---|
|
||||
| **Price** | $0/mo | $15/mo | $90/mo | $299/mo |
|
||||
| **Emails** | 15,000 | 100,000 | 500,000 | 2,000,000 |
|
||||
| **Infrastructure** | Shared | Shared | Dedicated IPs | Dedicated IPs |
|
||||
| **Analytics** | Basic | Advanced | Advanced | Advanced |
|
||||
| **Support** | Community | Priority | Priority | Dedicated Manager |
|
||||
| **Custom DKIM** | No | No | Yes | Yes |
|
||||
| **Webhooks** | No | No | Yes | Yes |
|
||||
| **SLA** | — | — | — | 99.9% |
|
||||
|
||||
To upgrade, visit the CyberMail platform at https://platform.cyberpersons.com
|
||||
|
||||
---
|
||||
|
||||
## Disconnecting Your Account
|
||||
|
||||
If you need to disconnect CyberMail:
|
||||
|
||||
1. Go to the CyberMail dashboard
|
||||
2. Click the **"Disconnect"** button
|
||||
3. Confirm the action
|
||||
|
||||
**What happens:**
|
||||
- SMTP relay is disabled (if it was enabled)
|
||||
- Postfix returns to direct sending
|
||||
- Local data (domains, credentials) is cleared
|
||||
- Your platform account is preserved — you can reconnect anytime
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Will enabling relay affect my existing email accounts?**
|
||||
A: Yes, ALL outgoing email from the server will route through CyberMail. This includes emails from websites (contact forms, notifications) and email accounts (Postfix). Incoming email is not affected.
|
||||
|
||||
**Q: Can I use CyberMail without the relay?**
|
||||
A: Yes. You can use SMTP credentials directly in your applications (WordPress SMTP plugins, custom scripts, etc.) without enabling the server-wide relay.
|
||||
|
||||
**Q: What happens if I exceed my plan limit?**
|
||||
A: Check with the platform for current overage policies. The dashboard shows your usage so you can monitor and upgrade before hitting limits.
|
||||
|
||||
**Q: Can I use CyberMail for bulk marketing emails?**
|
||||
A: CyberMail is designed for transactional and legitimate business email. Bulk marketing to purchased lists is not permitted. Use it for newsletters to opted-in subscribers, transactional emails, and business communications.
|
||||
|
||||
**Q: My domain DNS is not managed by CyberPanel. Can I still use CyberMail?**
|
||||
A: Yes. After adding the domain, click "DNS Records" to see the required records. Add them manually at your DNS provider (Cloudflare, Route53, etc.).
|
||||
|
||||
**Q: I disconnected and reconnected. Why are my old domains gone?**
|
||||
A: Reconnecting clears stale local data. Simply add your domains again — if they're still registered on the platform, they'll link back.
|
||||
|
||||
**Q: Is there a banner on other pages?**
|
||||
A: Yes, a promotional banner appears on email-related pages (Webmail, Mail Functions, etc.). It can be dismissed by clicking the X button and won't reappear for 7 days.
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **CyberPanel Issues**: https://github.com/usmannasir/cyberpanel/issues
|
||||
- **CyberMail Platform**: https://platform.cyberpersons.com
|
||||
- **Email Deliverability Help**: Check your domain at https://www.mail-tester.com
|
||||
0
emailDelivery/__init__.py
Normal file
0
emailDelivery/__init__.py
Normal file
6
emailDelivery/apps.py
Normal file
6
emailDelivery/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class EmaildeliveryConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'emailDelivery'
|
||||
742
emailDelivery/emailDeliveryManager.py
Normal file
742
emailDelivery/emailDeliveryManager.py
Normal file
@@ -0,0 +1,742 @@
|
||||
import json
|
||||
import requests
|
||||
from django.shortcuts import render
|
||||
from django.http import JsonResponse
|
||||
from loginSystem.models import Administrator
|
||||
from plogical.acl import ACLManager
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
from .models import CyberMailAccount, CyberMailDomain
|
||||
|
||||
|
||||
class EmailDeliveryManager:
|
||||
|
||||
PLATFORM_URL = 'https://platform.cyberpersons.com/email/cp/'
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging
|
||||
|
||||
def _apiCall(self, endpoint, data=None, apiKey=None):
|
||||
"""POST to platform API. If apiKey provided, sends Bearer auth."""
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
if apiKey:
|
||||
headers['Authorization'] = 'Bearer %s' % apiKey
|
||||
url = self.PLATFORM_URL + endpoint
|
||||
try:
|
||||
resp = requests.post(url, json=data or {}, headers=headers, timeout=30)
|
||||
return resp.json()
|
||||
except requests.exceptions.Timeout:
|
||||
return {'success': False, 'error': 'Platform API request timed out.'}
|
||||
except requests.exceptions.ConnectionError:
|
||||
return {'success': False, 'error': 'Could not connect to CyberMail platform.'}
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def _accountApiCall(self, account, endpoint, data=None):
|
||||
"""API call using a CyberMailAccount's stored per-user key."""
|
||||
if not account.api_key:
|
||||
return {'success': False, 'error': 'No API key found. Please reconnect your account.'}
|
||||
return self._apiCall(endpoint, data, apiKey=account.api_key)
|
||||
|
||||
def home(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
isConnected = False
|
||||
try:
|
||||
account = CyberMailAccount.objects.get(admin=admin)
|
||||
isConnected = account.is_connected
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
pass
|
||||
|
||||
context = {
|
||||
'isConnected': isConnected,
|
||||
'adminEmail': admin.email,
|
||||
'adminName': admin.firstName if hasattr(admin, 'firstName') else admin.userName,
|
||||
}
|
||||
|
||||
return render(request, 'emailDelivery/index.html', context)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.home] Error: %s' % str(e))
|
||||
return render(request, 'emailDelivery/index.html', {
|
||||
'error': str(e),
|
||||
'isConnected': False,
|
||||
'adminEmail': '',
|
||||
'adminName': '',
|
||||
})
|
||||
|
||||
def getStatus(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
|
||||
result = self._accountApiCall(account, 'api/account/', {'email': account.email})
|
||||
if not result.get('success', False):
|
||||
return JsonResponse({'success': False, 'error': result.get('error', 'Failed to get account status')})
|
||||
|
||||
accountData = result.get('data', {})
|
||||
|
||||
# Platform returns plan info nested under data.plan
|
||||
planInfo = accountData.get('plan', {})
|
||||
if planInfo.get('name'):
|
||||
account.plan_name = planInfo['name']
|
||||
account.plan_slug = planInfo.get('slug', account.plan_slug)
|
||||
account.emails_per_month = planInfo.get('emails_per_month', account.emails_per_month)
|
||||
account.save()
|
||||
|
||||
# Sync domains from platform to local DB
|
||||
try:
|
||||
domainResult = self._accountApiCall(account, 'api/domains/list/', {'email': account.email})
|
||||
if domainResult.get('success', False):
|
||||
platformDomains = domainResult.get('data', {}).get('domains', [])
|
||||
for pd in platformDomains:
|
||||
try:
|
||||
cmDomain = CyberMailDomain.objects.get(account=account, domain=pd['domain'])
|
||||
cmDomain.status = pd.get('status', cmDomain.status)
|
||||
cmDomain.spf_verified = pd.get('spf_verified', False)
|
||||
cmDomain.dkim_verified = pd.get('dkim_verified', False)
|
||||
cmDomain.dmarc_verified = pd.get('dmarc_verified', False)
|
||||
cmDomain.save()
|
||||
except CyberMailDomain.DoesNotExist:
|
||||
CyberMailDomain.objects.create(
|
||||
account=account,
|
||||
domain=pd['domain'],
|
||||
platform_domain_id=pd.get('id'),
|
||||
status=pd.get('status', 'pending'),
|
||||
spf_verified=pd.get('spf_verified', False),
|
||||
dkim_verified=pd.get('dkim_verified', False),
|
||||
dmarc_verified=pd.get('dmarc_verified', False),
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.getStatus] Domain sync error: %s' % str(e))
|
||||
|
||||
domains = list(CyberMailDomain.objects.filter(account=account).values(
|
||||
'id', 'domain', 'platform_domain_id', 'status',
|
||||
'spf_verified', 'dkim_verified', 'dmarc_verified', 'dns_configured'
|
||||
))
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'account': {
|
||||
'email': account.email,
|
||||
'plan_name': account.plan_name,
|
||||
'plan_slug': account.plan_slug,
|
||||
'emails_per_month': account.emails_per_month,
|
||||
'relay_enabled': account.relay_enabled,
|
||||
'smtp_host': account.smtp_host,
|
||||
'smtp_port': account.smtp_port,
|
||||
},
|
||||
'domains': domains,
|
||||
'stats': {
|
||||
'emails_sent': accountData.get('emails_sent_this_month', 0),
|
||||
'reputation_score': accountData.get('reputation_score', 0),
|
||||
},
|
||||
})
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.getStatus] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def connect(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
data = json.loads(request.body)
|
||||
password = data.get('password', '')
|
||||
email = data.get('email', admin.email)
|
||||
|
||||
if not password:
|
||||
return JsonResponse({'success': False, 'error': 'Password is required'})
|
||||
|
||||
fullName = admin.firstName if hasattr(admin, 'firstName') and admin.firstName else admin.userName
|
||||
|
||||
# Public endpoint — no API key needed for registration
|
||||
result = self._apiCall('api/register/', {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'full_name': fullName,
|
||||
})
|
||||
|
||||
if not result.get('success', False):
|
||||
return JsonResponse({'success': False, 'error': result.get('error', 'Registration failed')})
|
||||
|
||||
accountData = result.get('data', {})
|
||||
apiKey = accountData.get('api_key', '')
|
||||
|
||||
account, created = CyberMailAccount.objects.get_or_create(
|
||||
admin=admin,
|
||||
defaults={
|
||||
'email': email,
|
||||
'api_key': apiKey,
|
||||
'platform_account_id': accountData.get('account_id'),
|
||||
'plan_name': accountData.get('plan_name', 'Free'),
|
||||
'plan_slug': accountData.get('plan_slug', 'free'),
|
||||
'emails_per_month': accountData.get('emails_per_month', 15000),
|
||||
'is_connected': True,
|
||||
}
|
||||
)
|
||||
|
||||
if not created:
|
||||
account.email = email
|
||||
account.api_key = apiKey
|
||||
account.platform_account_id = accountData.get('account_id')
|
||||
account.plan_name = accountData.get('plan_name', 'Free')
|
||||
account.plan_slug = accountData.get('plan_slug', 'free')
|
||||
account.emails_per_month = accountData.get('emails_per_month', 15000)
|
||||
account.is_connected = True
|
||||
# Reset relay fields from previous account
|
||||
account.smtp_credential_id = None
|
||||
account.smtp_username = ''
|
||||
account.relay_enabled = False
|
||||
account.save()
|
||||
# Clear stale domains from previous account
|
||||
CyberMailDomain.objects.filter(account=account).delete()
|
||||
|
||||
return JsonResponse({'success': True, 'message': 'Connected to CyberMail successfully'})
|
||||
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.connect] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def addDomain(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
data = json.loads(request.body)
|
||||
domainName = data.get('domain', '')
|
||||
|
||||
if not domainName:
|
||||
return JsonResponse({'success': False, 'error': 'Domain name is required'})
|
||||
|
||||
result = self._accountApiCall(account, 'api/domains/add/', {
|
||||
'email': account.email,
|
||||
'domain': domainName,
|
||||
})
|
||||
|
||||
if not result.get('success', False):
|
||||
return JsonResponse({'success': False, 'error': result.get('error', 'Failed to add domain')})
|
||||
|
||||
domainData = result.get('data', {})
|
||||
|
||||
cmDomain, created = CyberMailDomain.objects.get_or_create(
|
||||
account=account,
|
||||
domain=domainName,
|
||||
defaults={
|
||||
'platform_domain_id': domainData.get('id') or domainData.get('domain_id'),
|
||||
'status': domainData.get('status', 'pending'),
|
||||
}
|
||||
)
|
||||
if not created:
|
||||
cmDomain.platform_domain_id = domainData.get('id') or domainData.get('domain_id')
|
||||
cmDomain.status = domainData.get('status', 'pending')
|
||||
cmDomain.save()
|
||||
|
||||
# Auto-configure DNS if domain exists in PowerDNS
|
||||
dnsResult = self._autoConfigureDnsForDomain(account, domainName)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': 'Domain added successfully',
|
||||
'dns_configured': dnsResult.get('success', False),
|
||||
'dns_message': dnsResult.get('message', ''),
|
||||
})
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.addDomain] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def _autoConfigureDnsForDomain(self, account, domainName):
|
||||
try:
|
||||
from dns.models import Domains as dnsDomains
|
||||
from plogical.dnsUtilities import DNS
|
||||
|
||||
try:
|
||||
zone = dnsDomains.objects.get(name=domainName)
|
||||
except dnsDomains.DoesNotExist:
|
||||
return {'success': False, 'message': 'Domain not found in PowerDNS. Please add DNS records manually.'}
|
||||
|
||||
recordsResult = self._accountApiCall(account, 'api/domains/dns-records/', {
|
||||
'email': account.email,
|
||||
'domain': domainName,
|
||||
})
|
||||
|
||||
if not recordsResult.get('success', False):
|
||||
return {'success': False, 'message': 'Could not fetch DNS records from platform.'}
|
||||
|
||||
records = recordsResult.get('data', {}).get('records', [])
|
||||
added = 0
|
||||
for rec in records:
|
||||
try:
|
||||
# Platform returns 'host' for the DNS hostname, 'type' for record type, 'value' for content
|
||||
recordHost = rec.get('host', '')
|
||||
recordType = rec.get('type', '')
|
||||
recordValue = rec.get('value', '')
|
||||
|
||||
if not recordHost or not recordType or not recordValue:
|
||||
continue
|
||||
|
||||
DNS.createDNSRecord(
|
||||
zone,
|
||||
recordHost,
|
||||
recordType,
|
||||
recordValue,
|
||||
rec.get('priority', 0),
|
||||
rec.get('ttl', 3600)
|
||||
)
|
||||
added += 1
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager._autoConfigureDnsForDomain] Record error: %s' % str(e))
|
||||
|
||||
try:
|
||||
cmDomain = CyberMailDomain.objects.get(account=account, domain=domainName)
|
||||
cmDomain.dns_configured = True
|
||||
cmDomain.save()
|
||||
except CyberMailDomain.DoesNotExist:
|
||||
pass
|
||||
|
||||
return {'success': True, 'message': '%d DNS records configured automatically.' % added}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager._autoConfigureDnsForDomain] Error: %s' % str(e))
|
||||
return {'success': False, 'message': str(e)}
|
||||
|
||||
def verifyDomain(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
data = json.loads(request.body)
|
||||
domainName = data.get('domain', '')
|
||||
|
||||
result = self._accountApiCall(account, 'api/domains/verify/', {
|
||||
'email': account.email,
|
||||
'domain': domainName,
|
||||
})
|
||||
|
||||
if not result.get('success', False):
|
||||
return JsonResponse({'success': False, 'error': result.get('error', 'Verification failed')})
|
||||
|
||||
verifyData = result.get('data', {})
|
||||
|
||||
# Platform returns: spf, dkim, dmarc, all_verified, verification_token
|
||||
allVerified = verifyData.get('all_verified', False)
|
||||
|
||||
try:
|
||||
cmDomain = CyberMailDomain.objects.get(account=account, domain=domainName)
|
||||
cmDomain.status = 'verified' if allVerified else 'pending'
|
||||
cmDomain.spf_verified = verifyData.get('spf', False)
|
||||
cmDomain.dkim_verified = verifyData.get('dkim', False)
|
||||
cmDomain.dmarc_verified = verifyData.get('dmarc', False)
|
||||
cmDomain.save()
|
||||
except CyberMailDomain.DoesNotExist:
|
||||
pass
|
||||
|
||||
return JsonResponse({'success': True, 'data': {
|
||||
'status': 'verified' if allVerified else 'pending',
|
||||
'spf_verified': verifyData.get('spf', False),
|
||||
'dkim_verified': verifyData.get('dkim', False),
|
||||
'dmarc_verified': verifyData.get('dmarc', False),
|
||||
'all_verified': allVerified,
|
||||
}})
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.verifyDomain] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def listDomains(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
|
||||
result = self._accountApiCall(account, 'api/domains/list/', {'email': account.email})
|
||||
if not result.get('success', False):
|
||||
return JsonResponse({'success': False, 'error': result.get('error', 'Failed to list domains')})
|
||||
|
||||
platformDomains = result.get('data', {}).get('domains', [])
|
||||
|
||||
for pd in platformDomains:
|
||||
try:
|
||||
cmDomain = CyberMailDomain.objects.get(account=account, domain=pd['domain'])
|
||||
cmDomain.status = pd.get('status', cmDomain.status)
|
||||
cmDomain.spf_verified = pd.get('spf_verified', False)
|
||||
cmDomain.dkim_verified = pd.get('dkim_verified', False)
|
||||
cmDomain.dmarc_verified = pd.get('dmarc_verified', False)
|
||||
cmDomain.save()
|
||||
except CyberMailDomain.DoesNotExist:
|
||||
CyberMailDomain.objects.create(
|
||||
account=account,
|
||||
domain=pd['domain'],
|
||||
platform_domain_id=pd.get('id'),
|
||||
status=pd.get('status', 'pending'),
|
||||
spf_verified=pd.get('spf_verified', False),
|
||||
dkim_verified=pd.get('dkim_verified', False),
|
||||
dmarc_verified=pd.get('dmarc_verified', False),
|
||||
)
|
||||
|
||||
domains = list(CyberMailDomain.objects.filter(account=account).values(
|
||||
'id', 'domain', 'platform_domain_id', 'status',
|
||||
'spf_verified', 'dkim_verified', 'dmarc_verified', 'dns_configured'
|
||||
))
|
||||
|
||||
return JsonResponse({'success': True, 'domains': domains})
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.listDomains] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def getDnsRecords(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
data = json.loads(request.body)
|
||||
domainName = data.get('domain', '')
|
||||
|
||||
result = self._accountApiCall(account, 'api/domains/dns-records/', {
|
||||
'email': account.email,
|
||||
'domain': domainName,
|
||||
})
|
||||
|
||||
return JsonResponse(result)
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.getDnsRecords] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def removeDomain(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
data = json.loads(request.body)
|
||||
domainName = data.get('domain', '')
|
||||
|
||||
result = self._accountApiCall(account, 'api/domains/remove/', {
|
||||
'email': account.email,
|
||||
'domain': domainName,
|
||||
})
|
||||
|
||||
if not result.get('success', False):
|
||||
return JsonResponse({'success': False, 'error': result.get('error', 'Failed to remove domain')})
|
||||
|
||||
CyberMailDomain.objects.filter(account=account, domain=domainName).delete()
|
||||
|
||||
return JsonResponse({'success': True, 'message': 'Domain removed successfully'})
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.removeDomain] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def autoConfigureDns(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
data = json.loads(request.body)
|
||||
domainName = data.get('domain', '')
|
||||
|
||||
result = self._autoConfigureDnsForDomain(account, domainName)
|
||||
|
||||
return JsonResponse({
|
||||
'success': result.get('success', False),
|
||||
'message': result.get('message', ''),
|
||||
})
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.autoConfigureDns] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def createSmtpCredential(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
data = json.loads(request.body)
|
||||
|
||||
result = self._accountApiCall(account, 'api/smtp/create/', {
|
||||
'email': account.email,
|
||||
'description': data.get('description', ''),
|
||||
})
|
||||
|
||||
return JsonResponse(result)
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.createSmtpCredential] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def listSmtpCredentials(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
|
||||
result = self._accountApiCall(account, 'api/smtp/list/', {'email': account.email})
|
||||
|
||||
# Normalize: platform returns 'id' per credential, JS expects 'credential_id'
|
||||
if result.get('success') and result.get('data', {}).get('credentials'):
|
||||
for cred in result['data']['credentials']:
|
||||
if 'id' in cred and 'credential_id' not in cred:
|
||||
cred['credential_id'] = cred['id']
|
||||
|
||||
return JsonResponse(result)
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.listSmtpCredentials] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def rotateSmtpPassword(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
data = json.loads(request.body)
|
||||
|
||||
result = self._accountApiCall(account, 'api/smtp/rotate/', {
|
||||
'email': account.email,
|
||||
'credential_id': data.get('credential_id'),
|
||||
})
|
||||
|
||||
# Normalize: platform returns 'new_password', JS expects 'password'
|
||||
if result.get('success') and result.get('data', {}).get('new_password'):
|
||||
result['data']['password'] = result['data'].pop('new_password')
|
||||
|
||||
return JsonResponse(result)
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.rotateSmtpPassword] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def deleteSmtpCredential(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
data = json.loads(request.body)
|
||||
|
||||
result = self._accountApiCall(account, 'api/smtp/delete/', {
|
||||
'email': account.email,
|
||||
'credential_id': data.get('credential_id'),
|
||||
})
|
||||
|
||||
if result.get('success', False):
|
||||
if account.smtp_credential_id == data.get('credential_id'):
|
||||
account.smtp_credential_id = None
|
||||
account.smtp_username = ''
|
||||
account.save()
|
||||
|
||||
return JsonResponse(result)
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.deleteSmtpCredential] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def enableRelay(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
|
||||
# Create SMTP credential if none exists
|
||||
if not account.smtp_credential_id:
|
||||
result = self._accountApiCall(account, 'api/smtp/create/', {
|
||||
'email': account.email,
|
||||
'description': 'CyberPanel Relay',
|
||||
})
|
||||
if not result.get('success', False):
|
||||
return JsonResponse({'success': False, 'error': result.get('error', 'Failed to create SMTP credential')})
|
||||
|
||||
credData = result.get('data', {})
|
||||
account.smtp_credential_id = credData.get('credential_id')
|
||||
account.smtp_username = credData.get('username', '')
|
||||
account.save()
|
||||
|
||||
smtpPassword = credData.get('password', '')
|
||||
else:
|
||||
# Rotate to get a fresh password
|
||||
result = self._accountApiCall(account, 'api/smtp/rotate/', {
|
||||
'email': account.email,
|
||||
'credential_id': account.smtp_credential_id,
|
||||
})
|
||||
if not result.get('success', False):
|
||||
return JsonResponse({'success': False, 'error': result.get('error', 'Failed to get SMTP password')})
|
||||
|
||||
smtpPassword = result.get('data', {}).get('new_password', '')
|
||||
|
||||
# Configure Postfix relay via mailUtilities subprocess
|
||||
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/mailUtilities.py"
|
||||
execPath += " configureRelayHost --smtpHost %s --smtpPort %s --smtpUser '%s' --smtpPassword '%s'" % (
|
||||
account.smtp_host, account.smtp_port, account.smtp_username, smtpPassword
|
||||
)
|
||||
output = ProcessUtilities.outputExecutioner(execPath)
|
||||
|
||||
if output and '1,None' in output:
|
||||
account.relay_enabled = True
|
||||
account.save()
|
||||
return JsonResponse({'success': True, 'message': 'SMTP relay enabled successfully'})
|
||||
else:
|
||||
return JsonResponse({'success': False, 'error': 'Failed to configure Postfix relay: %s' % str(output)})
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.enableRelay] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def disableRelay(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
|
||||
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/mailUtilities.py removeRelayHost"
|
||||
output = ProcessUtilities.outputExecutioner(execPath)
|
||||
|
||||
if output and '1,None' in output:
|
||||
account.relay_enabled = False
|
||||
account.save()
|
||||
return JsonResponse({'success': True, 'message': 'SMTP relay disabled successfully'})
|
||||
else:
|
||||
return JsonResponse({'success': False, 'error': 'Failed to remove Postfix relay: %s' % str(output)})
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.disableRelay] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def getStats(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
|
||||
result = self._accountApiCall(account, 'api/stats/', {'email': account.email})
|
||||
|
||||
# Normalize for JS: platform returns data.total_sent etc at top level
|
||||
if result.get('success') and result.get('data'):
|
||||
d = result['data']
|
||||
result['data'] = {
|
||||
'total_sent': d.get('total_sent', 0),
|
||||
'delivered': d.get('delivered', 0),
|
||||
'bounced': d.get('bounced', 0),
|
||||
'failed': d.get('failed', 0),
|
||||
'delivery_rate': d.get('delivery_rate', 0),
|
||||
}
|
||||
|
||||
return JsonResponse(result)
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.getStats] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def getDomainStats(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
|
||||
result = self._accountApiCall(account, 'api/stats/domains/', {'email': account.email})
|
||||
|
||||
# Normalize: platform returns data.domains as dict, JS expects array
|
||||
if result.get('success') and result.get('data'):
|
||||
domainsData = result['data'].get('domains', {})
|
||||
if isinstance(domainsData, dict):
|
||||
domainsList = []
|
||||
for domainName, stats in domainsData.items():
|
||||
entry = {'domain': domainName}
|
||||
if isinstance(stats, dict):
|
||||
entry.update(stats)
|
||||
domainsList.append(entry)
|
||||
result['data']['domains'] = domainsList
|
||||
|
||||
return JsonResponse(result)
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.getDomainStats] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def getLogs(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
data = json.loads(request.body)
|
||||
|
||||
result = self._accountApiCall(account, 'api/logs/', {
|
||||
'email': account.email,
|
||||
'page': data.get('page', 1),
|
||||
'per_page': data.get('per_page', 50),
|
||||
'status': data.get('status', ''),
|
||||
'from_domain': data.get('from_domain', ''),
|
||||
'days': data.get('days', 7),
|
||||
})
|
||||
|
||||
# Normalize field names and pagination
|
||||
if result.get('success') and result.get('data'):
|
||||
pagination = result['data'].get('pagination', {})
|
||||
result['data']['total_pages'] = pagination.get('total_pages', 1)
|
||||
result['data']['page'] = pagination.get('page', 1)
|
||||
|
||||
# Map platform field names to what JS/template expects
|
||||
logs = result['data'].get('logs', [])
|
||||
for log in logs:
|
||||
log['date'] = log.get('queued_at', '')
|
||||
log['from'] = log.get('from_email', '')
|
||||
log['to'] = log.get('to_email', '')
|
||||
|
||||
return JsonResponse(result)
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.getLogs] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def disconnect(self, request, userID):
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
account = CyberMailAccount.objects.get(admin=admin, is_connected=True)
|
||||
|
||||
# Disable relay first if enabled
|
||||
if account.relay_enabled:
|
||||
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/mailUtilities.py removeRelayHost"
|
||||
ProcessUtilities.outputExecutioner(execPath)
|
||||
|
||||
account.is_connected = False
|
||||
account.relay_enabled = False
|
||||
account.api_key = ''
|
||||
account.smtp_credential_id = None
|
||||
account.smtp_username = ''
|
||||
account.platform_account_id = None
|
||||
account.save()
|
||||
|
||||
# Remove local domain records
|
||||
CyberMailDomain.objects.filter(account=account).delete()
|
||||
|
||||
return JsonResponse({'success': True, 'message': 'Disconnected from CyberMail'})
|
||||
|
||||
except CyberMailAccount.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Account not connected'})
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.disconnect] Error: %s' % str(e))
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
def checkStatus(self, request, userID):
|
||||
try:
|
||||
result = self._apiCall('api/health/', {})
|
||||
return JsonResponse(result)
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
0
emailDelivery/migrations/__init__.py
Normal file
0
emailDelivery/migrations/__init__.py
Normal file
44
emailDelivery/models.py
Normal file
44
emailDelivery/models.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from django.db import models
|
||||
from loginSystem.models import Administrator
|
||||
|
||||
|
||||
class CyberMailAccount(models.Model):
|
||||
admin = models.OneToOneField(Administrator, on_delete=models.CASCADE, related_name='cybermail_account')
|
||||
platform_account_id = models.IntegerField(null=True)
|
||||
api_key = models.CharField(max_length=255, blank=True)
|
||||
email = models.CharField(max_length=255)
|
||||
plan_name = models.CharField(max_length=100, default='Free')
|
||||
plan_slug = models.CharField(max_length=50, default='free')
|
||||
emails_per_month = models.IntegerField(default=15000)
|
||||
is_connected = models.BooleanField(default=False)
|
||||
relay_enabled = models.BooleanField(default=False)
|
||||
smtp_credential_id = models.IntegerField(null=True)
|
||||
smtp_username = models.CharField(max_length=255, blank=True)
|
||||
smtp_host = models.CharField(max_length=255, default='mail.cyberpersons.com')
|
||||
smtp_port = models.IntegerField(default=587)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'cybermail_accounts'
|
||||
|
||||
def __str__(self):
|
||||
return f"CyberMail Account for {self.admin.userName}"
|
||||
|
||||
|
||||
class CyberMailDomain(models.Model):
|
||||
account = models.ForeignKey(CyberMailAccount, on_delete=models.CASCADE, related_name='domains')
|
||||
domain = models.CharField(max_length=255)
|
||||
platform_domain_id = models.IntegerField(null=True)
|
||||
status = models.CharField(max_length=50, default='pending')
|
||||
spf_verified = models.BooleanField(default=False)
|
||||
dkim_verified = models.BooleanField(default=False)
|
||||
dmarc_verified = models.BooleanField(default=False)
|
||||
dns_configured = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'cybermail_domains'
|
||||
|
||||
def __str__(self):
|
||||
return f"CyberMail Domain: {self.domain}"
|
||||
326
emailDelivery/static/emailDelivery/emailDelivery.js
Normal file
326
emailDelivery/static/emailDelivery/emailDelivery.js
Normal file
@@ -0,0 +1,326 @@
|
||||
app.controller('emailDeliveryCtrl', ['$scope', '$http', '$timeout', function($scope, $http, $timeout) {
|
||||
|
||||
function apiCall(url, data, callback, errback) {
|
||||
var config = {headers: {'X-CSRFToken': getCookie('csrftoken')}};
|
||||
$http.post(url, data || {}, config).then(function(resp) {
|
||||
if (callback) callback(resp.data);
|
||||
}, function(err) {
|
||||
console.error('API error:', url, err);
|
||||
if (errback) errback(err);
|
||||
});
|
||||
}
|
||||
|
||||
function notify(msg, type) {
|
||||
new PNotify({title: type === 'error' ? 'Error' : 'Email Delivery', text: msg, type: type || 'success'});
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.activeTab = 'domains';
|
||||
|
||||
// Account data
|
||||
$scope.account = {};
|
||||
$scope.stats = {};
|
||||
$scope.domains = [];
|
||||
$scope.smtpCredentials = [];
|
||||
$scope.logs = [];
|
||||
$scope.detailedStats = {};
|
||||
$scope.domainStats = [];
|
||||
|
||||
// Form data
|
||||
$scope.connectEmail = '';
|
||||
$scope.connectPassword = '';
|
||||
$scope.connectLoading = false;
|
||||
$scope.newDomainName = '';
|
||||
$scope.addDomainLoading = false;
|
||||
$scope.newSmtpDescription = '';
|
||||
$scope.createSmtpLoading = false;
|
||||
$scope.oneTimePassword = null;
|
||||
|
||||
// Relay
|
||||
$scope.relayLoading = false;
|
||||
|
||||
// Logs
|
||||
$scope.logFilters = {status: '', from_domain: '', days: 7};
|
||||
$scope.logsPage = 1;
|
||||
$scope.logsTotalPages = 1;
|
||||
$scope.logsLoading = false;
|
||||
|
||||
// Domain stats
|
||||
$scope.domainStatsLoading = false;
|
||||
|
||||
// Disconnect
|
||||
$scope.disconnectLoading = false;
|
||||
|
||||
$scope.init = function(isConnected, adminEmail) {
|
||||
$scope.isConnected = isConnected;
|
||||
$scope.connectEmail = adminEmail || '';
|
||||
if (isConnected) {
|
||||
$scope.refreshDashboard();
|
||||
} else {
|
||||
$scope.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.refreshDashboard = function() {
|
||||
$scope.loading = true;
|
||||
apiCall('/emailDelivery/status/', {}, function(data) {
|
||||
if (data.success) {
|
||||
$scope.account = data.account;
|
||||
$scope.stats = data.stats || {};
|
||||
$scope.domains = data.domains || [];
|
||||
}
|
||||
$scope.loading = false;
|
||||
}, function() {
|
||||
$scope.loading = false;
|
||||
notify('Failed to load dashboard data', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getUsagePercent = function() {
|
||||
if (!$scope.account.emails_per_month) return 0;
|
||||
return Math.min(100, Math.round(($scope.stats.emails_sent || 0) / $scope.account.emails_per_month * 100));
|
||||
};
|
||||
|
||||
// ============ Connect ============
|
||||
$scope.connectAccount = function() {
|
||||
if (!$scope.connectEmail || !$scope.connectPassword) {
|
||||
notify('Please fill in all fields.', 'error');
|
||||
return;
|
||||
}
|
||||
$scope.connectLoading = true;
|
||||
apiCall('/emailDelivery/connect/', {
|
||||
email: $scope.connectEmail,
|
||||
password: $scope.connectPassword
|
||||
}, function(data) {
|
||||
$scope.connectLoading = false;
|
||||
if (data.success) {
|
||||
$('#connectModal').modal('hide');
|
||||
notify('Connected to CyberMail successfully!');
|
||||
$timeout(function() { window.location.reload(); }, 1000);
|
||||
} else {
|
||||
notify(data.error || 'Connection failed.', 'error');
|
||||
}
|
||||
}, function() {
|
||||
$scope.connectLoading = false;
|
||||
notify('Connection failed. Please try again.', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
// ============ Domains ============
|
||||
$scope.addDomain = function() {
|
||||
if (!$scope.newDomainName) {
|
||||
notify('Please enter a domain name.', 'error');
|
||||
return;
|
||||
}
|
||||
$scope.addDomainLoading = true;
|
||||
apiCall('/emailDelivery/domains/add/', {
|
||||
domain: $scope.newDomainName
|
||||
}, function(data) {
|
||||
$scope.addDomainLoading = false;
|
||||
if (data.success) {
|
||||
$('#addDomainModal').modal('hide');
|
||||
$scope.newDomainName = '';
|
||||
$scope.refreshDashboard();
|
||||
var msg = 'Domain added successfully.';
|
||||
if (data.dns_configured) msg += ' ' + data.dns_message;
|
||||
notify(msg);
|
||||
} else {
|
||||
notify(data.error || 'Failed to add domain.', 'error');
|
||||
}
|
||||
}, function() {
|
||||
$scope.addDomainLoading = false;
|
||||
notify('Failed to add domain.', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.verifyDomain = function(domain) {
|
||||
apiCall('/emailDelivery/domains/verify/', {domain: domain}, function(data) {
|
||||
if (data.success) {
|
||||
$scope.refreshDashboard();
|
||||
notify('Domain verification completed.');
|
||||
} else {
|
||||
notify(data.error || 'Verification failed.', 'error');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.configureDns = function(domain) {
|
||||
apiCall('/emailDelivery/domains/auto-configure-dns/', {domain: domain}, function(data) {
|
||||
if (data.success) {
|
||||
$scope.refreshDashboard();
|
||||
notify(data.message || 'DNS configured.');
|
||||
} else {
|
||||
notify(data.message || data.error || 'DNS configuration failed.', 'error');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeDomain = function(domain) {
|
||||
if (!confirm('Remove domain ' + domain + ' from CyberMail?')) return;
|
||||
apiCall('/emailDelivery/domains/remove/', {domain: domain}, function(data) {
|
||||
if (data.success) {
|
||||
$scope.refreshDashboard();
|
||||
notify('Domain removed.');
|
||||
} else {
|
||||
notify(data.error || 'Failed to remove domain.', 'error');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ============ SMTP Credentials ============
|
||||
$scope.switchTab = function(tab) {
|
||||
$scope.activeTab = tab;
|
||||
if (tab === 'smtp') $scope.loadSmtpCredentials();
|
||||
if (tab === 'logs') $scope.loadLogs();
|
||||
if (tab === 'stats') $scope.loadStats();
|
||||
};
|
||||
|
||||
$scope.loadSmtpCredentials = function() {
|
||||
apiCall('/emailDelivery/smtp/list/', {}, function(data) {
|
||||
if (data.success) {
|
||||
$scope.smtpCredentials = data.data ? data.data.credentials || [] : [];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createSmtpCredential = function() {
|
||||
$scope.createSmtpLoading = true;
|
||||
apiCall('/emailDelivery/smtp/create/', {
|
||||
description: $scope.newSmtpDescription
|
||||
}, function(data) {
|
||||
$scope.createSmtpLoading = false;
|
||||
if (data.success) {
|
||||
$('#createSmtpModal').modal('hide');
|
||||
$scope.newSmtpDescription = '';
|
||||
$scope.oneTimePassword = data.data ? data.data.password : null;
|
||||
$scope.loadSmtpCredentials();
|
||||
if ($scope.oneTimePassword) {
|
||||
$timeout(function() { $('#passwordModal').modal('show'); }, 500);
|
||||
}
|
||||
notify('SMTP credential created.');
|
||||
} else {
|
||||
notify(data.error || 'Failed to create credential.', 'error');
|
||||
}
|
||||
}, function() {
|
||||
$scope.createSmtpLoading = false;
|
||||
notify('Failed to create credential.', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.rotatePassword = function(credId) {
|
||||
if (!confirm("Rotate this credential's password? The old password will stop working immediately.")) return;
|
||||
apiCall('/emailDelivery/smtp/rotate/', {credential_id: credId}, function(data) {
|
||||
if (data.success) {
|
||||
$scope.oneTimePassword = data.data ? data.data.password : null;
|
||||
if ($scope.oneTimePassword) {
|
||||
$timeout(function() { $('#passwordModal').modal('show'); }, 300);
|
||||
}
|
||||
notify('Password rotated.');
|
||||
} else {
|
||||
notify(data.error || 'Failed to rotate password.', 'error');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteCredential = function(credId) {
|
||||
if (!confirm('Delete this SMTP credential? This cannot be undone.')) return;
|
||||
apiCall('/emailDelivery/smtp/delete/', {credential_id: credId}, function(data) {
|
||||
if (data.success) {
|
||||
$scope.loadSmtpCredentials();
|
||||
notify('Credential deleted.');
|
||||
} else {
|
||||
notify(data.error || 'Failed to delete credential.', 'error');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ============ Relay ============
|
||||
$scope.enableRelay = function() {
|
||||
$scope.relayLoading = true;
|
||||
apiCall('/emailDelivery/relay/enable/', {}, function(data) {
|
||||
$scope.relayLoading = false;
|
||||
if (data.success) {
|
||||
$scope.account.relay_enabled = true;
|
||||
notify('SMTP relay enabled!');
|
||||
} else {
|
||||
notify(data.error || 'Failed to enable relay.', 'error');
|
||||
}
|
||||
}, function() {
|
||||
$scope.relayLoading = false;
|
||||
notify('Failed to enable relay.', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.disableRelay = function() {
|
||||
if (!confirm('Disable SMTP relay? Postfix will send directly again.')) return;
|
||||
$scope.relayLoading = true;
|
||||
apiCall('/emailDelivery/relay/disable/', {}, function(data) {
|
||||
$scope.relayLoading = false;
|
||||
if (data.success) {
|
||||
$scope.account.relay_enabled = false;
|
||||
notify('SMTP relay disabled.');
|
||||
} else {
|
||||
notify(data.error || 'Failed to disable relay.', 'error');
|
||||
}
|
||||
}, function() {
|
||||
$scope.relayLoading = false;
|
||||
notify('Failed to disable relay.', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
// ============ Logs ============
|
||||
$scope.loadLogs = function() {
|
||||
$scope.logsLoading = true;
|
||||
apiCall('/emailDelivery/logs/', {
|
||||
page: $scope.logsPage,
|
||||
status: $scope.logFilters.status,
|
||||
from_domain: $scope.logFilters.from_domain,
|
||||
days: $scope.logFilters.days || 7
|
||||
}, function(data) {
|
||||
$scope.logsLoading = false;
|
||||
if (data.success) {
|
||||
$scope.logs = data.data ? data.data.logs || [] : [];
|
||||
$scope.logsTotalPages = data.data ? data.data.total_pages || 1 : 1;
|
||||
}
|
||||
}, function() {
|
||||
$scope.logsLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
// ============ Stats ============
|
||||
$scope.loadStats = function() {
|
||||
apiCall('/emailDelivery/stats/', {}, function(data) {
|
||||
if (data.success) {
|
||||
$scope.detailedStats = data.data || {};
|
||||
}
|
||||
});
|
||||
$scope.domainStatsLoading = true;
|
||||
apiCall('/emailDelivery/stats/domains/', {}, function(data) {
|
||||
$scope.domainStatsLoading = false;
|
||||
if (data.success) {
|
||||
$scope.domainStats = data.data ? data.data.domains || [] : [];
|
||||
}
|
||||
}, function() {
|
||||
$scope.domainStatsLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
// ============ Disconnect ============
|
||||
$scope.disconnectAccount = function() {
|
||||
$scope.disconnectLoading = true;
|
||||
apiCall('/emailDelivery/disconnect/', {}, function(data) {
|
||||
$scope.disconnectLoading = false;
|
||||
if (data.success) {
|
||||
$('#disconnectModal').modal('hide');
|
||||
notify('Disconnected from CyberMail.');
|
||||
$timeout(function() { window.location.reload(); }, 1000);
|
||||
} else {
|
||||
notify(data.error || 'Failed to disconnect.', 'error');
|
||||
}
|
||||
}, function() {
|
||||
$scope.disconnectLoading = false;
|
||||
notify('Failed to disconnect.', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
}]);
|
||||
1095
emailDelivery/templates/emailDelivery/index.html
Normal file
1095
emailDelivery/templates/emailDelivery/index.html
Normal file
File diff suppressed because it is too large
Load Diff
30
emailDelivery/urls.py
Normal file
30
emailDelivery/urls.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='emailDeliveryHome'),
|
||||
path('connect/', views.connect, name='emailDeliveryConnect'),
|
||||
path('status/', views.getStatus, name='emailDeliveryStatus'),
|
||||
path('disconnect/', views.disconnect, name='emailDeliveryDisconnect'),
|
||||
# Domains
|
||||
path('domains/add/', views.addDomain, name='emailDeliveryAddDomain'),
|
||||
path('domains/list/', views.listDomains, name='emailDeliveryListDomains'),
|
||||
path('domains/verify/', views.verifyDomain, name='emailDeliveryVerifyDomain'),
|
||||
path('domains/dns-records/', views.getDnsRecords, name='emailDeliveryDnsRecords'),
|
||||
path('domains/auto-configure-dns/', views.autoConfigureDns, name='emailDeliveryAutoConfigureDns'),
|
||||
path('domains/remove/', views.removeDomain, name='emailDeliveryRemoveDomain'),
|
||||
# SMTP Credentials
|
||||
path('smtp/create/', views.createSmtpCredential, name='emailDeliverySmtpCreate'),
|
||||
path('smtp/list/', views.listSmtpCredentials, name='emailDeliverySmtpList'),
|
||||
path('smtp/rotate/', views.rotateSmtpPassword, name='emailDeliverySmtpRotate'),
|
||||
path('smtp/delete/', views.deleteSmtpCredential, name='emailDeliverySmtpDelete'),
|
||||
# Relay
|
||||
path('relay/enable/', views.enableRelay, name='emailDeliveryRelayEnable'),
|
||||
path('relay/disable/', views.disableRelay, name='emailDeliveryRelayDisable'),
|
||||
# Stats & Logs
|
||||
path('stats/', views.getStats, name='emailDeliveryStats'),
|
||||
path('stats/domains/', views.getDomainStats, name='emailDeliveryDomainStats'),
|
||||
path('logs/', views.getLogs, name='emailDeliveryLogs'),
|
||||
# Health
|
||||
path('health/', views.checkStatus, name='emailDeliveryHealth'),
|
||||
]
|
||||
184
emailDelivery/views.py
Normal file
184
emailDelivery/views.py
Normal file
@@ -0,0 +1,184 @@
|
||||
from django.shortcuts import redirect
|
||||
from django.http import JsonResponse
|
||||
from loginSystem.views import loadLoginPage
|
||||
from .emailDeliveryManager import EmailDeliveryManager
|
||||
|
||||
|
||||
def index(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.home(request, userID)
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
|
||||
def connect(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.connect(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def getStatus(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.getStatus(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def disconnect(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.disconnect(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def addDomain(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.addDomain(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def listDomains(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.listDomains(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def verifyDomain(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.verifyDomain(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def getDnsRecords(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.getDnsRecords(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def autoConfigureDns(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.autoConfigureDns(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def removeDomain(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.removeDomain(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def createSmtpCredential(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.createSmtpCredential(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def listSmtpCredentials(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.listSmtpCredentials(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def rotateSmtpPassword(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.rotateSmtpPassword(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def deleteSmtpCredential(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.deleteSmtpCredential(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def enableRelay(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.enableRelay(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def disableRelay(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.disableRelay(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def getStats(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.getStats(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def getDomainStats(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.getDomainStats(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def getLogs(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.getLogs(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
|
||||
|
||||
def checkStatus(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
em = EmailDeliveryManager()
|
||||
return em.checkStatus(request, userID)
|
||||
except KeyError:
|
||||
return JsonResponse({'success': False, 'error': 'Not authenticated'})
|
||||
@@ -7,6 +7,21 @@
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||
|
||||
<div id="cybermailBanner" style="display:none; margin-bottom:20px;">
|
||||
<div style="background:linear-gradient(135deg,#4f46e5 0%,#7c3aed 50%,#9333ea 100%);border-radius:10px;padding:20px 24px;display:flex;align-items:center;gap:18px;box-shadow:0 4px 15px rgba(79,70,229,0.3);">
|
||||
<div style="flex-shrink:0;font-size:32px;">✉</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:800;font-size:18px;color:#fff;margin-bottom:4px;letter-spacing:-0.3px;">Stop Landing in Spam</div>
|
||||
<div style="font-size:13.5px;color:rgba(255,255,255,0.85);line-height:1.5;">Route your emails through CyberMail's optimized servers. <strong style="color:#fff;">15,000 emails/month free</strong> with automatic DNS configuration, real-time analytics, and dedicated IPs. <a href="https://cyberpanel.net/KnowledgeBase/cybermail-user-guide/" target="_blank" style="color:rgba(255,255,255,0.9);text-decoration:underline;font-weight:500;">Learn more</a></div>
|
||||
</div>
|
||||
<a href="/emailDelivery/" style="background:#fff;color:#4f46e5;padding:10px 24px;border-radius:7px;font-weight:700;font-size:13px;text-decoration:none;white-space:nowrap;flex-shrink:0;box-shadow:0 2px 8px rgba(0,0,0,0.1);transition:transform 0.15s;">Get Started Free →</a>
|
||||
<button onclick="dismissCyberMailBanner()" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:20px;cursor:pointer;padding:0 4px;line-height:1;flex-shrink:0;" title="Dismiss">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){if(!document.cookie.includes('cybermail_dismiss=1')){document.getElementById('cybermailBanner').style.display='';}})();
|
||||
function dismissCyberMailBanner(){document.getElementById('cybermailBanner').style.display='none';document.cookie='cybermail_dismiss=1; path=/; max-age='+7*86400;}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
|
||||
|
||||
@@ -7,6 +7,22 @@
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||
|
||||
<div id="cybermailBanner" style="display:none; margin-bottom:20px;">
|
||||
<div style="background:linear-gradient(135deg,#4f46e5 0%,#7c3aed 50%,#9333ea 100%);border-radius:10px;padding:20px 24px;display:flex;align-items:center;gap:18px;box-shadow:0 4px 15px rgba(79,70,229,0.3);">
|
||||
<div style="flex-shrink:0;font-size:32px;">✉</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:800;font-size:18px;color:#fff;margin-bottom:4px;letter-spacing:-0.3px;">Stop Landing in Spam</div>
|
||||
<div style="font-size:13.5px;color:rgba(255,255,255,0.85);line-height:1.5;">Route your emails through CyberMail's optimized servers. <strong style="color:#fff;">15,000 emails/month free</strong> with automatic DNS configuration, real-time analytics, and dedicated IPs. <a href="https://cyberpanel.net/KnowledgeBase/cybermail-user-guide/" target="_blank" style="color:rgba(255,255,255,0.9);text-decoration:underline;font-weight:500;">Learn more</a></div>
|
||||
</div>
|
||||
<a href="/emailDelivery/" style="background:#fff;color:#4f46e5;padding:10px 24px;border-radius:7px;font-weight:700;font-size:13px;text-decoration:none;white-space:nowrap;flex-shrink:0;box-shadow:0 2px 8px rgba(0,0,0,0.1);transition:transform 0.15s;">Get Started Free →</a>
|
||||
<button onclick="dismissCyberMailBanner()" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:20px;cursor:pointer;padding:0 4px;line-height:1;flex-shrink:0;" title="Dismiss">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){if(!document.cookie.includes('cybermail_dismiss=1')){document.getElementById('cybermailBanner').style.display='';}})();
|
||||
function dismissCyberMailBanner(){document.getElementById('cybermailBanner').style.display='none';document.cookie='cybermail_dismiss=1; path=/; max-age='+7*86400;}
|
||||
</script>
|
||||
|
||||
<div ng-controller="emailPage" class="container">
|
||||
|
||||
<div id="page-title">
|
||||
|
||||
@@ -317,6 +317,22 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="cybermailBanner" style="display:none; margin-bottom:20px;">
|
||||
<div style="background:linear-gradient(135deg,#4f46e5 0%,#7c3aed 50%,#9333ea 100%);border-radius:10px;padding:20px 24px;display:flex;align-items:center;gap:18px;box-shadow:0 4px 15px rgba(79,70,229,0.3);">
|
||||
<div style="flex-shrink:0;font-size:32px;">✉</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:800;font-size:18px;color:#fff;margin-bottom:4px;letter-spacing:-0.3px;">Stop Landing in Spam</div>
|
||||
<div style="font-size:13.5px;color:rgba(255,255,255,0.85);line-height:1.5;">Route your emails through CyberMail's optimized servers. <strong style="color:#fff;">15,000 emails/month free</strong> with automatic DNS configuration, real-time analytics, and dedicated IPs. <a href="https://cyberpanel.net/KnowledgeBase/cybermail-user-guide/" target="_blank" style="color:rgba(255,255,255,0.9);text-decoration:underline;font-weight:500;">Learn more</a></div>
|
||||
</div>
|
||||
<a href="/emailDelivery/" style="background:#fff;color:#4f46e5;padding:10px 24px;border-radius:7px;font-weight:700;font-size:13px;text-decoration:none;white-space:nowrap;flex-shrink:0;box-shadow:0 2px 8px rgba(0,0,0,0.1);transition:transform 0.15s;">Get Started Free →</a>
|
||||
<button onclick="dismissCyberMailBanner()" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:20px;cursor:pointer;padding:0 4px;line-height:1;flex-shrink:0;" title="Dismiss">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){if(!document.cookie.includes('cybermail_dismiss=1')){document.getElementById('cybermailBanner').style.display='';}})();
|
||||
function dismissCyberMailBanner(){document.getElementById('cybermailBanner').style.display='none';document.cookie='cybermail_dismiss=1; path=/; max-age='+7*86400;}
|
||||
</script>
|
||||
|
||||
<div class="modern-container" ng-controller="createEmailAccount">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">
|
||||
|
||||
@@ -398,6 +398,22 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="cybermailBanner" style="display:none; margin-bottom:20px;">
|
||||
<div style="background:linear-gradient(135deg,#4f46e5 0%,#7c3aed 50%,#9333ea 100%);border-radius:10px;padding:20px 24px;display:flex;align-items:center;gap:18px;box-shadow:0 4px 15px rgba(79,70,229,0.3);">
|
||||
<div style="flex-shrink:0;font-size:32px;">✉</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:800;font-size:18px;color:#fff;margin-bottom:4px;letter-spacing:-0.3px;">Stop Landing in Spam</div>
|
||||
<div style="font-size:13.5px;color:rgba(255,255,255,0.85);line-height:1.5;">Route your emails through CyberMail's optimized servers. <strong style="color:#fff;">15,000 emails/month free</strong> with automatic DNS configuration, real-time analytics, and dedicated IPs. <a href="https://cyberpanel.net/KnowledgeBase/cybermail-user-guide/" target="_blank" style="color:rgba(255,255,255,0.9);text-decoration:underline;font-weight:500;">Learn more</a></div>
|
||||
</div>
|
||||
<a href="/emailDelivery/" style="background:#fff;color:#4f46e5;padding:10px 24px;border-radius:7px;font-weight:700;font-size:13px;text-decoration:none;white-space:nowrap;flex-shrink:0;box-shadow:0 2px 8px rgba(0,0,0,0.1);transition:transform 0.15s;">Get Started Free →</a>
|
||||
<button onclick="dismissCyberMailBanner()" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:20px;cursor:pointer;padding:0 4px;line-height:1;flex-shrink:0;" title="Dismiss">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){if(!document.cookie.includes('cybermail_dismiss=1')){document.getElementById('cybermailBanner').style.display='';}})();
|
||||
function dismissCyberMailBanner(){document.getElementById('cybermailBanner').style.display='none';document.cookie='cybermail_dismiss=1; path=/; max-age='+7*86400;}
|
||||
</script>
|
||||
|
||||
<div class="modern-container" ng-controller="dkimManager">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">
|
||||
|
||||
@@ -7,6 +7,22 @@
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||
|
||||
<div id="cybermailBanner" style="display:none; margin-bottom:20px;">
|
||||
<div style="background:linear-gradient(135deg,#4f46e5 0%,#7c3aed 50%,#9333ea 100%);border-radius:10px;padding:20px 24px;display:flex;align-items:center;gap:18px;box-shadow:0 4px 15px rgba(79,70,229,0.3);">
|
||||
<div style="flex-shrink:0;font-size:32px;">✉</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:800;font-size:18px;color:#fff;margin-bottom:4px;letter-spacing:-0.3px;">Stop Landing in Spam</div>
|
||||
<div style="font-size:13.5px;color:rgba(255,255,255,0.85);line-height:1.5;">Route your emails through CyberMail's optimized servers. <strong style="color:#fff;">15,000 emails/month free</strong> with automatic DNS configuration, real-time analytics, and dedicated IPs. <a href="https://cyberpanel.net/KnowledgeBase/cybermail-user-guide/" target="_blank" style="color:rgba(255,255,255,0.9);text-decoration:underline;font-weight:500;">Learn more</a></div>
|
||||
</div>
|
||||
<a href="/emailDelivery/" style="background:#fff;color:#4f46e5;padding:10px 24px;border-radius:7px;font-weight:700;font-size:13px;text-decoration:none;white-space:nowrap;flex-shrink:0;box-shadow:0 2px 8px rgba(0,0,0,0.1);transition:transform 0.15s;">Get Started Free →</a>
|
||||
<button onclick="dismissCyberMailBanner()" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:20px;cursor:pointer;padding:0 4px;line-height:1;flex-shrink:0;" title="Dismiss">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){if(!document.cookie.includes('cybermail_dismiss=1')){document.getElementById('cybermailBanner').style.display='';}})();
|
||||
function dismissCyberMailBanner(){document.getElementById('cybermailBanner').style.display='none';document.cookie='cybermail_dismiss=1; path=/; max-age='+7*86400;}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div id="page-title">
|
||||
<h2>{% trans "Mail Functions" %}</h2>
|
||||
|
||||
@@ -1704,6 +1704,103 @@ LogFile /var/log/clamav/clamav.log
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def configureRelayHost(smtpHost, smtpPort, smtpUser, smtpPassword):
|
||||
try:
|
||||
postfixPath = '/etc/postfix/main.cf'
|
||||
|
||||
with open(postfixPath, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
relayKeys = ['relayhost', 'smtp_sasl_auth_enable', 'smtp_sasl_password_maps',
|
||||
'smtp_sasl_security_options', 'smtp_tls_security_level']
|
||||
|
||||
filteredLines = []
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
skip = False
|
||||
for key in relayKeys:
|
||||
if stripped.startswith(key + ' ') or stripped.startswith(key + '='):
|
||||
skip = True
|
||||
break
|
||||
if not skip:
|
||||
filteredLines.append(line)
|
||||
|
||||
relayConfig = [
|
||||
'\n# CyberMail SMTP Relay Configuration\n',
|
||||
'relayhost = [%s]:%s\n' % (smtpHost, smtpPort),
|
||||
'smtp_sasl_auth_enable = yes\n',
|
||||
'smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd\n',
|
||||
'smtp_sasl_security_options = noanonymous\n',
|
||||
'smtp_tls_security_level = encrypt\n',
|
||||
]
|
||||
|
||||
with open(postfixPath, 'w') as f:
|
||||
f.writelines(filteredLines)
|
||||
f.writelines(relayConfig)
|
||||
|
||||
saslPath = '/etc/postfix/sasl_passwd'
|
||||
with open(saslPath, 'w') as f:
|
||||
f.write('[%s]:%s %s:%s\n' % (smtpHost, smtpPort, smtpUser, smtpPassword))
|
||||
|
||||
os.chmod(saslPath, 0o600)
|
||||
|
||||
ProcessUtilities.executioner('postmap /etc/postfix/sasl_passwd')
|
||||
ProcessUtilities.executioner('systemctl reload postfix')
|
||||
|
||||
print('1,None')
|
||||
|
||||
except BaseException as msg:
|
||||
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [configureRelayHost]')
|
||||
print('0,%s' % str(msg))
|
||||
|
||||
@staticmethod
|
||||
def removeRelayHost():
|
||||
try:
|
||||
postfixPath = '/etc/postfix/main.cf'
|
||||
|
||||
with open(postfixPath, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
relayKeys = ['relayhost', 'smtp_sasl_auth_enable', 'smtp_sasl_password_maps',
|
||||
'smtp_sasl_security_options']
|
||||
commentLine = '# CyberMail SMTP Relay Configuration\n'
|
||||
|
||||
filteredLines = []
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if line == commentLine:
|
||||
continue
|
||||
skip = False
|
||||
for key in relayKeys:
|
||||
if stripped.startswith(key + ' ') or stripped.startswith(key + '='):
|
||||
skip = True
|
||||
break
|
||||
if not skip:
|
||||
if stripped.startswith('smtp_tls_security_level'):
|
||||
filteredLines.append('smtp_tls_security_level = may\n')
|
||||
else:
|
||||
filteredLines.append(line)
|
||||
|
||||
with open(postfixPath, 'w') as f:
|
||||
f.writelines(filteredLines)
|
||||
|
||||
saslPath = '/etc/postfix/sasl_passwd'
|
||||
saslDbPath = '/etc/postfix/sasl_passwd.db'
|
||||
|
||||
if os.path.exists(saslPath):
|
||||
os.remove(saslPath)
|
||||
if os.path.exists(saslDbPath):
|
||||
os.remove(saslDbPath)
|
||||
|
||||
ProcessUtilities.executioner('systemctl reload postfix')
|
||||
|
||||
print('1,None')
|
||||
|
||||
except BaseException as msg:
|
||||
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [removeRelayHost]')
|
||||
print('0,%s' % str(msg))
|
||||
|
||||
####### Imported below functions from mailserver/mailservermanager, need to refactor later
|
||||
|
||||
class MailServerManagerUtils(multi.Thread):
|
||||
@@ -2719,6 +2816,7 @@ milter_default_action = accept
|
||||
return 1, 'All checks are OK.'
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser(description='CyberPanel Installer')
|
||||
@@ -2729,6 +2827,10 @@ def main():
|
||||
parser.add_argument('--tempConfigPath', help='Temporary Configuration Path!')
|
||||
parser.add_argument('--install', help='Enable/Disable Policy Server!')
|
||||
parser.add_argument('--tempStatusPath', help='Path of temporary status file.')
|
||||
parser.add_argument('--smtpHost', help='SMTP relay host!')
|
||||
parser.add_argument('--smtpPort', help='SMTP relay port!')
|
||||
parser.add_argument('--smtpUser', help='SMTP relay username!')
|
||||
parser.add_argument('--smtpPassword', help='SMTP relay password!')
|
||||
|
||||
|
||||
|
||||
@@ -2772,6 +2874,10 @@ def main():
|
||||
mailUtilities.SetupEmailLimits()
|
||||
elif args.function == 'SaveEmailLimitsNew':
|
||||
mailUtilities.SaveEmailLimitsNew(args.tempConfigPath)
|
||||
elif args.function == 'configureRelayHost':
|
||||
mailUtilities.configureRelayHost(args.smtpHost, args.smtpPort, args.smtpUser, args.smtpPassword)
|
||||
elif args.function == 'removeRelayHost':
|
||||
mailUtilities.removeRelayHost()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1678,6 +1678,54 @@ $cfg['Servers'][$i]['LogoutURL'] = 'phpmyadminsignin.php?logout';
|
||||
except:
|
||||
pass
|
||||
|
||||
# CyberMail Email Delivery Tables
|
||||
try:
|
||||
cursor.execute('''
|
||||
CREATE TABLE `cybermail_accounts` (
|
||||
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
|
||||
`admin_id` integer NOT NULL UNIQUE,
|
||||
`platform_account_id` integer DEFAULT NULL,
|
||||
`api_key` varchar(255) NOT NULL DEFAULT '',
|
||||
`email` varchar(255) NOT NULL DEFAULT '',
|
||||
`plan_name` varchar(100) NOT NULL DEFAULT 'Free',
|
||||
`plan_slug` varchar(50) NOT NULL DEFAULT 'free',
|
||||
`emails_per_month` integer NOT NULL DEFAULT 15000,
|
||||
`is_connected` bool NOT NULL DEFAULT 0,
|
||||
`relay_enabled` bool NOT NULL DEFAULT 0,
|
||||
`smtp_credential_id` integer DEFAULT NULL,
|
||||
`smtp_username` varchar(255) NOT NULL DEFAULT '',
|
||||
`smtp_host` varchar(255) NOT NULL DEFAULT 'mail.cyberpersons.com',
|
||||
`smtp_port` integer NOT NULL DEFAULT 587,
|
||||
`created_at` datetime(6) NOT NULL,
|
||||
`updated_at` datetime(6) NOT NULL,
|
||||
CONSTRAINT `cybermail_accounts_admin_id_fk` FOREIGN KEY (`admin_id`)
|
||||
REFERENCES `loginSystem_administrator` (`id`) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
cursor.execute('''
|
||||
CREATE TABLE `cybermail_domains` (
|
||||
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
|
||||
`account_id` integer NOT NULL,
|
||||
`domain` varchar(255) NOT NULL DEFAULT '',
|
||||
`platform_domain_id` integer DEFAULT NULL,
|
||||
`status` varchar(50) NOT NULL DEFAULT 'pending',
|
||||
`spf_verified` bool NOT NULL DEFAULT 0,
|
||||
`dkim_verified` bool NOT NULL DEFAULT 0,
|
||||
`dmarc_verified` bool NOT NULL DEFAULT 0,
|
||||
`dns_configured` bool NOT NULL DEFAULT 0,
|
||||
`created_at` datetime(6) NOT NULL,
|
||||
KEY `cybermail_domains_account_id_idx` (`account_id`),
|
||||
CONSTRAINT `cybermail_domains_account_id_fk` FOREIGN KEY (`account_id`)
|
||||
REFERENCES `cybermail_accounts` (`id`) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
cursor.execute(
|
||||
'CREATE TABLE `loginSystem_acl` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(50) NOT NULL UNIQUE, `adminStatus` integer NOT NULL DEFAULT 0, `versionManagement` integer NOT NULL DEFAULT 0, `createNewUser` integer NOT NULL DEFAULT 0, `deleteUser` integer NOT NULL DEFAULT 0, `resellerCenter` integer NOT NULL DEFAULT 0, `changeUserACL` integer NOT NULL DEFAULT 0, `createWebsite` integer NOT NULL DEFAULT 0, `modifyWebsite` integer NOT NULL DEFAULT 0, `suspendWebsite` integer NOT NULL DEFAULT 0, `deleteWebsite` integer NOT NULL DEFAULT 0, `createPackage` integer NOT NULL DEFAULT 0, `deletePackage` integer NOT NULL DEFAULT 0, `modifyPackage` integer NOT NULL DEFAULT 0, `createDatabase` integer NOT NULL DEFAULT 0, `deleteDatabase` integer NOT NULL DEFAULT 0, `listDatabases` integer NOT NULL DEFAULT 0, `createNameServer` integer NOT NULL DEFAULT 0, `createDNSZone` integer NOT NULL DEFAULT 0, `deleteZone` integer NOT NULL DEFAULT 0, `addDeleteRecords` integer NOT NULL DEFAULT 0, `createEmail` integer NOT NULL DEFAULT 0, `deleteEmail` integer NOT NULL DEFAULT 0, `emailForwarding` integer NOT NULL DEFAULT 0, `changeEmailPassword` integer NOT NULL DEFAULT 0, `dkimManager` integer NOT NULL DEFAULT 0, `createFTPAccount` integer NOT NULL DEFAULT 0, `deleteFTPAccount` integer NOT NULL DEFAULT 0, `listFTPAccounts` integer NOT NULL DEFAULT 0, `createBackup` integer NOT NULL DEFAULT 0, `restoreBackup` integer NOT NULL DEFAULT 0, `addDeleteDestinations` integer NOT NULL DEFAULT 0, `scheduleBackups` integer NOT NULL DEFAULT 0, `remoteBackups` integer NOT NULL DEFAULT 0, `manageSSL` integer NOT NULL DEFAULT 0, `hostnameSSL` integer NOT NULL DEFAULT 0, `mailServerSSL` integer NOT NULL DEFAULT 0)')
|
||||
|
||||
@@ -8,6 +8,21 @@
|
||||
|
||||
<div class="webmail-container" ng-controller="webmailCtrl" ng-init="init()">
|
||||
|
||||
<div id="cybermailBanner" style="display:none;flex-shrink:0;">
|
||||
<div style="padding:10px 20px;display:flex;align-items:center;gap:14px;background:linear-gradient(135deg,#4f46e5 0%,#7c3aed 50%,#9333ea 100%);">
|
||||
<div style="flex-shrink:0;font-size:22px;">✉</div>
|
||||
<div style="flex:1;min-width:0;font-size:12.5px;color:rgba(255,255,255,0.85);">
|
||||
<span style="color:#fff;font-weight:800;font-size:13.5px;">Stop Landing in Spam</span> — Route emails through CyberMail. <strong style="color:#fff;">15,000 emails/month free.</strong> <a href="https://cyberpanel.net/KnowledgeBase/cybermail-user-guide/" target="_blank" style="color:rgba(255,255,255,0.9);text-decoration:underline;">Learn more</a>
|
||||
</div>
|
||||
<a href="/emailDelivery/" style="background:#fff;color:#4f46e5;padding:6px 18px;border-radius:5px;font-weight:700;font-size:11.5px;text-decoration:none;white-space:nowrap;flex-shrink:0;">Get Started Free →</a>
|
||||
<button onclick="dismissCyberMailBanner()" style="background:none;border:none;color:rgba(255,255,255,0.5);font-size:18px;cursor:pointer;padding:0 2px;line-height:1;flex-shrink:0;" title="Dismiss">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){if(!document.cookie.includes('cybermail_dismiss=1')){document.getElementById('cybermailBanner').style.display='';}})();
|
||||
function dismissCyberMailBanner(){document.getElementById('cybermailBanner').style.display='none';document.cookie='cybermail_dismiss=1; path=/; max-age='+7*86400;}
|
||||
</script>
|
||||
|
||||
<!-- Account Switcher Bar -->
|
||||
<div class="wm-account-bar" ng-show="managedAccounts.length > 1">
|
||||
<div class="wm-account-current">
|
||||
|
||||
Reference in New Issue
Block a user