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:
usmannasir
2026-03-06 00:19:53 +05:00
parent 4a082f5484
commit 1e00f6eff5
23 changed files with 3529 additions and 1 deletions

View File

@@ -76,6 +76,7 @@ INSTALLED_APPS = [
'IncBackups',
'aiScanner',
'webmail',
'emailDelivery',
# 'WebTerminal'
]

View File

@@ -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')),
]

View File

@@ -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>

View 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

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

View 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

View File

6
emailDelivery/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class EmaildeliveryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'emailDelivery'

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

View File

44
emailDelivery/models.py Normal file
View 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}"

View 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');
});
};
}]);

File diff suppressed because it is too large Load Diff

30
emailDelivery/urls.py Normal file
View 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
View 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'})

View File

@@ -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;">&#9993;</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 &rarr;</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">&times;</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">

View File

@@ -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;">&#9993;</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 &rarr;</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">&times;</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">

View File

@@ -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;">&#9993;</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 &rarr;</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">&times;</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">

View File

@@ -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;">&#9993;</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 &rarr;</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">&times;</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">

View File

@@ -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;">&#9993;</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 &rarr;</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">&times;</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>

View File

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

View File

@@ -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)')

View File

@@ -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;">&#9993;</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> &mdash; 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 &rarr;</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">&times;</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">