diff --git a/.wiki/Email-Accounts-Integration.md b/.wiki/Email-Accounts-Integration.md new file mode 100644 index 00000000..7cfd92d6 --- /dev/null +++ b/.wiki/Email-Accounts-Integration.md @@ -0,0 +1,140 @@ +# Email Accounts Integration + +The Email Accounts feature allows your customers to create and manage email accounts directly from their WordPress dashboard. This powerful feature integrates with various email providers to automate email provisioning as part of your multisite service offering. + +## Overview + +When enabled, customers can: +- Create email accounts using their mapped domains +- View credentials and connection details (IMAP/SMTP/Webmail) +- Change email account passwords +- Delete email accounts they no longer need + +Network administrators can: +- Choose which email providers to enable +- Set limits on email accounts per membership plan +- Offer email accounts as membership-included or per-account purchases +- Configure default quotas and restrictions + +## Supported Providers + +Ultimate Multisite supports the following email providers: + +| Provider | Best For | API Type | +|----------|----------|----------| +| [cPanel Email](cPanel-Email-Integration) | Self-hosted servers with cPanel | UAPI | +| [Purelymail](Purelymail-Email-Integration) | Budget-friendly email hosting | REST API | +| [Google Workspace](Google-Workspace-Email-Integration) | Enterprise email with Google apps | Admin SDK | +| [Microsoft 365](Microsoft-365-Email-Integration) | Enterprise email with Microsoft apps | Graph API | + +## How It Works + +### For Network Administrators + +1. **Enable the Feature**: Go to **WP Ultimo > Settings > Email Accounts** and enable the email accounts feature. + +2. **Configure a Provider**: Click the "Configure" button next to your chosen provider and follow the setup wizard: + - Add API credentials to your `wp-config.php` file + - Test the connection + - Complete the setup + +3. **Set Membership Limits**: Edit your membership products to configure email account limits: + - Enable/disable email accounts for the plan + - Set the maximum number of accounts + - Choose unlimited accounts if desired + +### For Customers + +1. **Access Email Management**: Customers see an "Email Accounts" section in their account dashboard. + +2. **Create an Account**: They enter a username and select a domain from their mapped domains. + +3. **View Credentials**: After creation, they can view: + - Email address and password + - IMAP server and port + - SMTP server and port + - Webmail URL + +4. **Manage Accounts**: They can change passwords or delete accounts as needed. + +## Database Schema + +Email accounts are stored in the `{prefix}_wu_email_accounts` table with the following key fields: + +| Field | Description | +|-------|-------------| +| `email_address` | Full email address (user@domain.com) | +| `customer_id` | Associated customer | +| `membership_id` | Associated membership | +| `provider` | Email provider identifier | +| `status` | pending, active, suspended, or failed | +| `quota_mb` | Storage quota in megabytes | +| `external_id` | Provider's external identifier | + +## Limitations Integration + +Email accounts integrate with the limitations system. You can configure: + +- **Enabled**: Whether email accounts are available +- **Limit**: Maximum number of accounts (0 = unlimited) + +These settings appear in the product editor under the "Limits" tab. + +## Security Considerations + +### API Credentials + +All API credentials are stored as constants in `wp-config.php`, not in the database. This provides: +- Better security (credentials not exposed in database dumps) +- Easier deployment across environments +- Protection from accidental exposure in admin panels + +### Password Handling + +- Passwords are generated securely using WordPress's `wp_generate_password()` +- Passwords are only displayed once to the customer after account creation +- Passwords are not stored in the database after successful provisioning + +### Domain Validation + +- Customers can only create email accounts on domains they own +- Domain ownership is verified through the domain mapping system +- Subdomains of the main network domain are excluded + +## Troubleshooting + +### Common Issues + +1. **"No domains available"**: The customer hasn't mapped any custom domains yet, or mapped domains aren't verified. + +2. **"Provider not configured"**: The email provider's API credentials are missing from `wp-config.php`. + +3. **"Connection failed"**: Check that: + - API credentials are correct + - Server can reach the provider's API + - Firewall isn't blocking outgoing connections + +4. **"Account creation failed"**: Check: + - The domain's DNS records are properly configured + - The domain is verified with the email provider + - You haven't exceeded provider quotas + +### Debug Mode + +Enable WordPress debug logging to see detailed error messages: + +```php +define('WP_DEBUG', true); +define('WP_DEBUG_LOG', true); +``` + +Logs are written to `wp-content/debug.log`. + +## Related Documentation + +- [cPanel Email Integration](cPanel-Email-Integration) +- [Purelymail Email Integration](Purelymail-Email-Integration) +- [Google Workspace Email Integration](Google-Workspace-Email-Integration) +- [Microsoft 365 Email Integration](Microsoft-365-Email-Integration) +- [Domain Mapping](Domain-Mapping) +- [Limitations System](Limitations) diff --git a/.wiki/Google-Workspace-Email-Integration.md b/.wiki/Google-Workspace-Email-Integration.md new file mode 100644 index 00000000..307295d7 --- /dev/null +++ b/.wiki/Google-Workspace-Email-Integration.md @@ -0,0 +1,301 @@ +# Google Workspace Email Integration + +Google Workspace (formerly G Suite) integration allows you to provision professional email accounts with Gmail's powerful interface. This is ideal for businesses that want the reliability and features of Google's enterprise email solution. + +## Overview + +- **Provider ID**: `google_workspace` +- **API Type**: Google Admin SDK Directory API +- **Best For**: Enterprise customers wanting Gmail +- **Webmail**: https://mail.google.com/ +- **Minimum Plan**: Google Workspace Business Starter ($6/user/month) + +## Features + +- Gmail interface for all email accounts +- Google Calendar, Drive, Docs, and Meet included +- 30GB-5TB storage per user (plan dependent) +- Advanced spam filtering +- Enterprise-grade security +- Mobile apps for iOS and Android + +## Requirements + +- Google Workspace account with Admin privileges +- Google Cloud project with Admin SDK enabled +- Service account with domain-wide delegation +- Domains verified in Google Workspace admin console + +## Pricing + +Google Workspace is licensed per user: + +| Plan | Price | Storage | Features | +|------|-------|---------|----------| +| Business Starter | $6/user/month | 30GB | Email, Calendar, Meet (100 participants) | +| Business Standard | $12/user/month | 2TB | + Recording, 150 participants | +| Business Plus | $18/user/month | 5TB | + eDiscovery, vault | +| Enterprise | Custom | Unlimited | + Advanced security | + +## Setup Instructions + +### Step 1: Create a Google Cloud Project + +1. Go to [Google Cloud Console](https://console.cloud.google.com/) +2. Create a new project or select an existing one +3. Note your project ID + +### Step 2: Enable the Admin SDK API + +1. In Google Cloud Console, go to **APIs & Services > Library** +2. Search for "Admin SDK API" +3. Click **Enable** + +### Step 3: Create a Service Account + +1. Go to **APIs & Services > Credentials** +2. Click **Create Credentials > Service Account** +3. Name your service account (e.g., "ultimate-multisite-email") +4. Grant no additional roles (we'll use domain-wide delegation) +5. Click **Done** + +### Step 4: Create Service Account Key + +1. Click on your new service account +2. Go to the **Keys** tab +3. Click **Add Key > Create new key** +4. Select **JSON** format +5. Download and save the JSON file securely + +### Step 5: Enable Domain-Wide Delegation + +1. In the service account details, click **Show Advanced Settings** +2. Enable **Domain-wide Delegation** +3. Note the **Client ID** (a long number) + +### Step 6: Authorize in Google Workspace Admin + +1. Go to [Google Workspace Admin Console](https://admin.google.com/) +2. Navigate to **Security > Access and data control > API controls** +3. Click **Manage Domain Wide Delegation** +4. Click **Add new** +5. Enter the **Client ID** from Step 5 +6. Add these OAuth scopes: + ``` + https://www.googleapis.com/auth/admin.directory.user + ``` +7. Click **Authorize** + +### Step 7: Get Your Customer ID + +1. In Google Workspace Admin Console, go to **Account > Account settings** +2. Find your **Customer ID** (starts with "C") + +### Step 8: Configure WordPress + +Add the following constants to your `wp-config.php` file: + +```php +// Google Workspace Email Provider Configuration +define('WU_GOOGLE_SERVICE_ACCOUNT_JSON', '/path/to/service-account.json'); +define('WU_GOOGLE_ADMIN_EMAIL', 'admin@yourdomain.com'); +define('WU_GOOGLE_CUSTOMER_ID', 'C0xxxxxxx'); +``` + +**Options for the JSON file**: + +Option A: File path (recommended for security) +```php +define('WU_GOOGLE_SERVICE_ACCOUNT_JSON', '/secure/path/outside/webroot/service-account.json'); +``` + +Option B: Inline JSON (if file storage isn't possible) +```php +define('WU_GOOGLE_SERVICE_ACCOUNT_JSON', '{"type":"service_account","project_id":"..."}'); +``` + +**Important**: +- The admin email must be a super admin in your Google Workspace +- Place constants BEFORE the line `/* That's all, stop editing! */` + +### Step 9: Add Domains to Google Workspace + +1. In Google Admin Console, go to **Account > Domains** +2. Click **Add a domain** +3. Complete domain verification +4. Set up email routing (MX records) + +### Step 10: Complete the Setup Wizard + +1. Go to **WP Ultimo > Settings > Email Accounts** +2. Click **Configure** next to "Google Workspace" +3. Follow the wizard to test your connection +4. Once successful, the provider is ready to use + +## Configuration Options + +| Constant | Required | Description | +|----------|----------|-------------| +| `WU_GOOGLE_SERVICE_ACCOUNT_JSON` | Yes | Path to JSON key file or JSON string | +| `WU_GOOGLE_ADMIN_EMAIL` | Yes | Super admin email for impersonation | +| `WU_GOOGLE_CUSTOMER_ID` | Yes | Google Workspace customer ID | + +## How It Works + +### Email Account Creation + +When a customer creates an email account: + +1. Ultimate Multisite authenticates using the service account +2. It impersonates the admin email for API access +3. Creates the user via Admin SDK Directory API +4. Sets the password and email routing + +### API Endpoints Used + +**Base URL**: `https://admin.googleapis.com/admin/directory/v1` + +| Operation | Endpoint | Method | +|-----------|----------|--------| +| Create User | `/users` | POST | +| Delete User | `/users/{userKey}` | DELETE | +| Update User | `/users/{userKey}` | PUT | +| Get User | `/users/{userKey}` | GET | + +### Authentication Flow + +1. Load service account credentials from JSON +2. Create JWT signed with service account private key +3. Exchange JWT for access token (with subject impersonation) +4. Use access token for API requests + +## Email Client Settings + +Customers can configure their email clients with these settings: + +### IMAP (Incoming Mail) + +| Setting | Value | +|---------|-------| +| Server | imap.gmail.com | +| Port | 993 | +| Security | SSL/TLS | +| Username | Full email address | + +### SMTP (Outgoing Mail) + +| Setting | Value | +|---------|-------| +| Server | smtp.gmail.com | +| Port | 465 or 587 | +| Security | SSL/TLS or STARTTLS | +| Username | Full email address | + +**Note**: Users may need to enable "Less secure app access" or create an App Password for third-party email clients. + +### Webmail + +``` +https://mail.google.com/ +``` + +Or with account selection: +``` +https://mail.google.com/mail/u/0/ +``` + +## DNS Configuration + +For each domain using Google Workspace email, configure these DNS records: + +### MX Records + +``` +Type Host Value Priority +MX @ aspmx.l.google.com 1 +MX @ alt1.aspmx.l.google.com 5 +MX @ alt2.aspmx.l.google.com 5 +MX @ alt3.aspmx.l.google.com 10 +MX @ alt4.aspmx.l.google.com 10 +``` + +### SPF Record + +``` +Type Host Value +TXT @ v=spf1 include:_spf.google.com ~all +``` + +### DKIM + +DKIM is configured in Google Admin Console: +1. Go to **Apps > Google Workspace > Gmail > Authenticate email** +2. Generate DKIM record +3. Add the provided TXT record to DNS + +### DMARC (Recommended) + +``` +Type Host Value +TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com +``` + +## Troubleshooting + +### "Invalid credentials" Error + +1. Verify the service account JSON file exists and is readable +2. Ensure the JSON file is valid (not corrupted) +3. Regenerate the key if needed + +### "Not authorized" Error + +1. Check domain-wide delegation is enabled +2. Verify the OAuth scopes are correct in Admin Console +3. Ensure the Client ID matches your service account +4. Confirm the admin email is a super admin + +### "User not found" or "Domain not found" Error + +1. Verify the domain is added to Google Workspace +2. Check the domain is verified +3. Ensure email routing is configured + +### "Quota exceeded" Error + +Google APIs have rate limits. If creating many accounts: +1. Space out requests +2. Request quota increase from Google Cloud Console + +### Service Account Issues + +1. **File not found**: Check the path to your JSON file +2. **Permission denied**: Ensure web server can read the file +3. **Invalid JSON**: Validate the JSON structure + +## Security Best Practices + +1. **Protect the service account key**: Store outside web root with restrictive permissions + +2. **Use a dedicated admin**: Create an admin account specifically for API access + +3. **Audit access**: Regularly review Admin Console security reports + +4. **Enable 2FA**: Require 2-factor authentication for all admin accounts + +5. **Limit delegation scope**: Only grant necessary OAuth scopes + +## License Management + +Google Workspace requires a license for each user. Consider: + +1. **Automatic licensing**: New users get the default license +2. **License costs**: Factor per-user costs into your pricing +3. **License limits**: Monitor your available licenses + +## Related Documentation + +- [Email Accounts Integration](Email-Accounts-Integration) +- [Google Workspace Admin Help](https://support.google.com/a) +- [Admin SDK Directory API](https://developers.google.com/admin-sdk/directory) +- [Domain-Wide Delegation](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority) diff --git a/.wiki/Microsoft-365-Email-Integration.md b/.wiki/Microsoft-365-Email-Integration.md new file mode 100644 index 00000000..bc87f3c8 --- /dev/null +++ b/.wiki/Microsoft-365-Email-Integration.md @@ -0,0 +1,340 @@ +# Microsoft 365 Email Integration + +Microsoft 365 (formerly Office 365) integration enables provisioning of professional email accounts with Outlook. This enterprise solution includes the full Microsoft productivity suite and is ideal for businesses already invested in the Microsoft ecosystem. + +## Overview + +- **Provider ID**: `microsoft365` +- **API Type**: Microsoft Graph API +- **Best For**: Enterprise customers wanting Outlook and Microsoft apps +- **Webmail**: https://outlook.office365.com/ +- **Minimum Plan**: Microsoft 365 Business Basic ($6/user/month) + +## Features + +- Outlook web and desktop clients +- Microsoft Teams, OneDrive, SharePoint included +- 50GB mailbox storage +- Enterprise-grade security with Microsoft Defender +- Advanced spam and malware protection +- Mobile apps for iOS and Android +- Integration with Windows and Microsoft Office + +## Requirements + +- Microsoft 365 tenant with admin access +- Azure AD application registration +- Application permissions for user management +- Domains verified in Microsoft 365 admin center +- Available Microsoft 365 licenses + +## Pricing + +Microsoft 365 is licensed per user: + +| Plan | Price | Storage | Features | +|------|-------|---------|----------| +| Business Basic | $6/user/month | 50GB email, 1TB OneDrive | Web apps, Teams | +| Business Standard | $12.50/user/month | 50GB email, 1TB OneDrive | + Desktop apps | +| Business Premium | $22/user/month | 50GB email, 1TB OneDrive | + Advanced security | +| Enterprise E3 | $36/user/month | 100GB email, Unlimited OneDrive | Full enterprise | + +## Setup Instructions + +### Step 1: Register an Azure AD Application + +1. Go to [Azure Portal](https://portal.azure.com/) +2. Navigate to **Azure Active Directory > App registrations** +3. Click **New registration** +4. Name: "Ultimate Multisite Email Integration" +5. Supported account types: "Accounts in this organizational directory only" +6. Redirect URI: Leave blank (not needed for client credentials) +7. Click **Register** +8. Note the **Application (client) ID** and **Directory (tenant) ID** + +### Step 2: Create a Client Secret + +1. In your app registration, go to **Certificates & secrets** +2. Click **New client secret** +3. Description: "Ultimate Multisite" +4. Expiration: Choose based on your security policy (24 months recommended) +5. Click **Add** +6. **Important**: Copy the secret value immediately (it won't be shown again) + +### Step 3: Configure API Permissions + +1. Go to **API permissions** +2. Click **Add a permission** +3. Select **Microsoft Graph** +4. Choose **Application permissions** (not Delegated) +5. Add these permissions: + - `User.ReadWrite.All` - Create, read, update, delete users + - `Directory.ReadWrite.All` - Manage directory data +6. Click **Grant admin consent for [Your Organization]** + +### Step 4: Get Your License SKU ID + +To assign licenses automatically, you need the SKU ID: + +1. Open PowerShell and connect to Microsoft Graph: + ```powershell + Connect-MgGraph -Scopes "Organization.Read.All" + Get-MgSubscribedSku | Select SkuPartNumber, SkuId + ``` + +2. Common SKU Part Numbers: + - `O365_BUSINESS_ESSENTIALS` - Business Basic + - `O365_BUSINESS_PREMIUM` - Business Standard + - `SPB` - Business Premium + +3. Note the `SkuId` (GUID format) for your plan + +### Step 5: Configure WordPress + +Add the following constants to your `wp-config.php` file: + +```php +// Microsoft 365 Email Provider Configuration +define('WU_MS365_CLIENT_ID', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'); +define('WU_MS365_CLIENT_SECRET', 'your_client_secret_here'); +define('WU_MS365_TENANT_ID', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'); +define('WU_MS365_LICENSE_SKU', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'); // Optional +``` + +**Important**: Place these constants BEFORE the line `/* That's all, stop editing! */` + +### Step 6: Add Domains to Microsoft 365 + +1. Go to [Microsoft 365 Admin Center](https://admin.microsoft.com/) +2. Navigate to **Settings > Domains** +3. Click **Add domain** +4. Complete domain verification (TXT record) +5. Configure email DNS records as instructed + +### Step 7: Complete the Setup Wizard + +1. Go to **WP Ultimo > Settings > Email Accounts** +2. Click **Configure** next to "Microsoft 365" +3. Follow the wizard to test your connection +4. Once successful, the provider is ready to use + +## Configuration Options + +| Constant | Required | Description | +|----------|----------|-------------| +| `WU_MS365_CLIENT_ID` | Yes | Azure AD application ID | +| `WU_MS365_CLIENT_SECRET` | Yes | Application client secret | +| `WU_MS365_TENANT_ID` | Yes | Azure AD tenant ID | +| `WU_MS365_LICENSE_SKU` | No | License SKU ID for auto-assignment | + +## How It Works + +### Email Account Creation + +When a customer creates an email account: + +1. Ultimate Multisite authenticates using client credentials flow +2. Creates the user via Microsoft Graph API +3. Optionally assigns a license (if `WU_MS365_LICENSE_SKU` is configured) +4. Sets up the mailbox with the specified password + +### API Endpoints Used + +**Base URL**: `https://graph.microsoft.com/v1.0` + +| Operation | Endpoint | Method | +|-----------|----------|--------| +| Create User | `/users` | POST | +| Delete User | `/users/{id}` | DELETE | +| Update User | `/users/{id}` | PATCH | +| Get User | `/users/{id}` | GET | +| Assign License | `/users/{id}/assignLicense` | POST | + +### Authentication Flow + +1. Request access token from Azure AD token endpoint +2. Use client credentials (client ID + secret) +3. Token is cached and refreshed automatically +4. All API requests include Bearer token + +### User Creation Payload + +```json +{ + "accountEnabled": true, + "displayName": "John Doe", + "mailNickname": "john", + "userPrincipalName": "john@domain.com", + "passwordProfile": { + "forceChangePasswordNextSignIn": false, + "password": "generated_password" + }, + "usageLocation": "US" +} +``` + +**Note**: `usageLocation` is required for license assignment. Default is "US". + +## Email Client Settings + +Customers can configure their email clients with these settings: + +### IMAP (Incoming Mail) + +| Setting | Value | +|---------|-------| +| Server | outlook.office365.com | +| Port | 993 | +| Security | SSL/TLS | +| Username | Full email address | + +### SMTP (Outgoing Mail) + +| Setting | Value | +|---------|-------| +| Server | smtp.office365.com | +| Port | 587 | +| Security | STARTTLS | +| Username | Full email address | + +### Webmail + +``` +https://outlook.office365.com/ +``` + +Or via Microsoft 365 portal: +``` +https://www.office.com/ +``` + +## DNS Configuration + +For each domain using Microsoft 365 email, configure these DNS records: + +### MX Record + +``` +Type Host Value Priority +MX @ {domain-com}.mail.protection.outlook.com 0 +``` + +Replace `{domain-com}` with your domain using hyphens (e.g., `contoso-com`). + +### SPF Record + +``` +Type Host Value +TXT @ v=spf1 include:spf.protection.outlook.com -all +``` + +### Autodiscover (Required for Outlook) + +``` +Type Host Value +CNAME autodiscover autodiscover.outlook.com +``` + +### DKIM Records + +Configure DKIM in Microsoft 365 Admin Center: +1. Go to **Settings > Domains** +2. Select your domain +3. Click **DKIM** +4. Enable DKIM signing +5. Add the provided CNAME records + +### DMARC (Recommended) + +``` +Type Host Value +TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com +``` + +## Troubleshooting + +### "Invalid client" Error + +1. Verify Client ID is correct (from Azure AD app registration) +2. Ensure the application is registered in the correct tenant +3. Check that the app hasn't been deleted + +### "Invalid client secret" Error + +1. Client secrets expire - check the expiration date +2. Generate a new secret if expired +3. Ensure no extra whitespace in the secret value + +### "Insufficient privileges" Error + +1. Verify API permissions are granted +2. Check that admin consent was given +3. Required permissions: + - `User.ReadWrite.All` + - `Directory.ReadWrite.All` + +### "License not available" Error + +1. Check available licenses in Microsoft 365 Admin Center +2. Purchase additional licenses if needed +3. Verify the SKU ID is correct + +### "Domain not verified" Error + +1. Ensure domain is added to Microsoft 365 +2. Complete domain verification +3. Wait for DNS propagation (can take up to 48 hours) + +### "Usage location required" Error + +Users need a usage location for license assignment. The integration defaults to "US". Contact support if you need to change this. + +## Security Best Practices + +1. **Rotate client secrets**: Create new secrets before expiration and update `wp-config.php` + +2. **Use least privilege**: Only grant necessary API permissions + +3. **Monitor sign-ins**: Review Azure AD sign-in logs for unusual activity + +4. **Enable Conditional Access**: Add policies to protect admin accounts + +5. **Audit regularly**: Use Microsoft 365 security center for compliance + +## License Management + +### Automatic Assignment + +If `WU_MS365_LICENSE_SKU` is configured, licenses are assigned automatically when users are created. + +### Manual Assignment + +If not using automatic assignment: +1. User is created without email capabilities +2. Admin must assign license in Microsoft 365 Admin Center +3. Email becomes available after license assignment + +### License Costs + +Factor per-user license costs into your pricing model. Consider: +- Microsoft 365 Business Basic: $6/user/month +- Overage charges for exceeding license count +- Annual vs. monthly billing discounts + +## Comparison with Other Providers + +| Feature | Microsoft 365 | Google Workspace | Purelymail | +|---------|--------------|------------------|------------| +| Price | $6+/user/month | $6+/user/month | $0.10/user/month | +| Storage | 50GB-100GB | 30GB-5TB | 10GB included | +| Productivity Apps | Yes | Yes | No | +| Desktop Apps | With Standard+ | No | No | +| API Complexity | Medium | High | Low | +| Setup Difficulty | Medium | High | Low | + +## Related Documentation + +- [Email Accounts Integration](Email-Accounts-Integration) +- [Microsoft 365 Admin Documentation](https://docs.microsoft.com/microsoft-365/admin/) +- [Microsoft Graph API Reference](https://docs.microsoft.com/graph/api/overview) +- [Azure AD App Registration](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) diff --git a/.wiki/Purelymail-Email-Integration.md b/.wiki/Purelymail-Email-Integration.md new file mode 100644 index 00000000..1f82bf2a --- /dev/null +++ b/.wiki/Purelymail-Email-Integration.md @@ -0,0 +1,232 @@ +# Purelymail Email Integration + +Purelymail is a privacy-focused, affordable email hosting service that offers a simple API for email account management. It's an excellent choice for multisite operators who want reliable email without the complexity of enterprise solutions. + +## Overview + +- **Provider ID**: `purelymail` +- **API Type**: REST API +- **Best For**: Budget-conscious operators wanting reliable email +- **Webmail**: https://app.purelymail.com/ +- **Affiliate Program**: Yes + +## Features + +- Simple REST API for account management +- Competitive pricing ($10/year unlimited domains, $0.10/user/month) +- Privacy-focused (no ads, no tracking) +- Automatic spam filtering +- DKIM/SPF/DMARC support +- Catch-all addresses + +## Requirements + +- Purelymail account with API access +- API key from Purelymail dashboard +- Domains verified in Purelymail + +## Pricing + +Purelymail offers straightforward pricing: + +- **Routing Only**: $10/year for unlimited domains +- **Per User**: $0.10/user/month (billed annually) +- **Storage**: First 10GB included, then $3/year per additional 10GB + +This makes it very cost-effective for multisite operators. + +## Setup Instructions + +### Step 1: Create a Purelymail Account + +1. Visit [Purelymail](https://purelymail.com/?ref=ultimatemultisite) +2. Create an account and complete email verification +3. Add a payment method + +### Step 2: Get Your API Key + +1. Log in to the [Purelymail Dashboard](https://app.purelymail.com/) +2. Go to **Account Settings** +3. Find the **API** section +4. Generate or copy your API key + +### Step 3: Configure WordPress + +Add the following constant to your `wp-config.php` file: + +```php +// Purelymail Email Provider Configuration +define('WU_PURELYMAIL_API_KEY', 'your_api_key_here'); +``` + +**Important**: Place this constant BEFORE the line `/* That's all, stop editing! */` + +### Step 4: Add Your Domains to Purelymail + +Before customers can create email accounts, each domain must be added to your Purelymail account: + +1. In Purelymail dashboard, go to **Domains** +2. Click **Add Domain** +3. Enter the domain name +4. Configure DNS records as instructed +5. Verify the domain + +### Step 5: Complete the Setup Wizard + +1. Go to **WP Ultimo > Settings > Email Accounts** +2. Click **Configure** next to "Purelymail" +3. Follow the wizard to test your connection +4. Once successful, the provider is ready to use + +## Configuration Options + +| Constant | Required | Description | +|----------|----------|-------------| +| `WU_PURELYMAIL_API_KEY` | Yes | Your Purelymail API key | + +## How It Works + +### Email Account Creation + +When a customer creates an email account: + +1. Ultimate Multisite calls the Purelymail API `/createUser` endpoint +2. The user is created under your Purelymail account +3. The account becomes immediately available + +### API Endpoints Used + +**Base URL**: `https://purelymail.com/api/v0` + +| Operation | Endpoint | Method | +|-----------|----------|--------| +| Create Account | `/createUser` | POST | +| Delete Account | `/deleteUser` | POST | +| Change Password | `/modifyUserPassword` | POST | +| Get Account Info | `/getUser` | POST | + +### Authentication + +All API requests include the header: +``` +Purelymail-Token: {your_api_key} +``` + +## Email Client Settings + +Customers can configure their email clients with these settings: + +### IMAP (Incoming Mail) + +| Setting | Value | +|---------|-------| +| Server | mailserver.purelymail.com | +| Port | 993 | +| Security | SSL/TLS | +| Username | Full email address | + +### SMTP (Outgoing Mail) + +| Setting | Value | +|---------|-------| +| Server | mailserver.purelymail.com | +| Port | 465 | +| Security | SSL/TLS | +| Username | Full email address | + +### Webmail + +Customers can access webmail at: +``` +https://app.purelymail.com/ +``` + +## DNS Configuration + +For each domain using Purelymail, configure these DNS records: + +### Required Records + +``` +Type Host Value Priority +MX @ mailserver.purelymail.com 10 +TXT @ v=spf1 include:_spf.purelymail.com ~all - +``` + +### DKIM Records + +``` +Type Host Value +CNAME purelymail1._domainkey key1.dkimroot.purelymail.com +CNAME purelymail2._domainkey key2.dkimroot.purelymail.com +CNAME purelymail3._domainkey key3.dkimroot.purelymail.com +``` + +### DMARC Record (Recommended) + +``` +Type Host Value +TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com +``` + +## Troubleshooting + +### "Invalid API key" Error + +1. Verify your API key is correct in `wp-config.php` +2. Generate a new API key if needed +3. Ensure there are no extra spaces or characters + +### "Domain not found" Error + +The domain must be added and verified in your Purelymail account before creating email accounts on it. + +### "User already exists" Error + +An email account with that address already exists in Purelymail. Check your Purelymail dashboard for existing accounts. + +### "Rate limit exceeded" Error + +Purelymail has API rate limits. If you're creating many accounts, space out the requests or contact Purelymail support. + +### Connection Timeout + +1. Check that your server can reach `purelymail.com` +2. Verify no firewall is blocking outgoing HTTPS connections +3. Check PHP cURL extension is enabled + +## Best Practices + +### Domain Management + +1. **Pre-add domains**: Add all customer domains to Purelymail proactively +2. **Verify DNS**: Ensure DNS is configured before offering email on a domain +3. **Document DNS**: Provide customers with DNS instructions for their domains + +### Account Management + +1. **Set quotas**: Configure appropriate storage quotas for accounts +2. **Monitor usage**: Check your Purelymail dashboard for usage statistics +3. **Handle failures gracefully**: If account creation fails, provide clear error messages + +### Cost Management + +1. **Estimate users**: Plan for the number of email accounts your customers will create +2. **Monitor billing**: Keep track of per-user costs in Purelymail +3. **Set limits**: Use membership limits to control the number of email accounts + +## Purelymail vs. Alternatives + +| Feature | Purelymail | Google Workspace | Microsoft 365 | +|---------|------------|------------------|---------------| +| Starting Price | $10/year + $0.10/user/month | $6/user/month | $6/user/month | +| Unlimited Domains | Yes | No | No | +| API Complexity | Simple | Complex | Complex | +| Privacy Focus | High | Medium | Medium | +| Storage | 10GB included | 30GB | 50GB | + +## Related Documentation + +- [Email Accounts Integration](Email-Accounts-Integration) +- [Purelymail Official Documentation](https://purelymail.com/docs) +- [Purelymail API Reference](https://purelymail.com/docs/api) diff --git a/.wiki/cPanel-Email-Integration.md b/.wiki/cPanel-Email-Integration.md new file mode 100644 index 00000000..7baf5142 --- /dev/null +++ b/.wiki/cPanel-Email-Integration.md @@ -0,0 +1,191 @@ +# cPanel Email Integration + +The cPanel Email integration allows you to automatically provision email accounts on servers running cPanel. This is ideal if you're hosting your multisite on a cPanel-based server or have access to a cPanel server for email hosting. + +## Overview + +- **Provider ID**: `cpanel` +- **API Type**: cPanel UAPI +- **Best For**: Self-hosted environments with cPanel access +- **Webmail**: Roundcube at port 2096 + +## Features + +- Automatic email account creation via cPanel UAPI +- Password management (create, change) +- Account deletion +- Configurable storage quotas +- Webmail access via Roundcube + +## Requirements + +- cPanel server with UAPI access enabled +- cPanel account credentials with email management permissions +- Network connectivity from WordPress server to cPanel server +- PHP cURL extension enabled + +## Setup Instructions + +### Step 1: Gather cPanel Credentials + +1. Log in to your cPanel account +2. Note your cPanel username +3. Create an API token or use your cPanel password (API token recommended): + - Go to **Security > Manage API Tokens** + - Click **Create** and name your token + - Copy the generated token + +4. Note your cPanel hostname (e.g., `server.yourdomain.com`) + +### Step 2: Configure WordPress + +Add the following constants to your `wp-config.php` file: + +```php +// cPanel Email Provider Configuration +define('WU_CPANEL_USERNAME', 'your_cpanel_username'); +define('WU_CPANEL_PASSWORD', 'your_cpanel_password_or_api_token'); +define('WU_CPANEL_HOST', 'server.yourdomain.com'); +define('WU_CPANEL_PORT', 2083); // Optional, defaults to 2083 +``` + +**Important**: Place these constants BEFORE the line `/* That's all, stop editing! */` + +### Step 3: Complete the Setup Wizard + +1. Go to **WP Ultimo > Settings > Email Accounts** +2. Click **Configure** next to "cPanel Email" +3. Follow the wizard to test your connection +4. Once successful, the provider is ready to use + +## Configuration Options + +| Constant | Required | Default | Description | +|----------|----------|---------|-------------| +| `WU_CPANEL_USERNAME` | Yes | - | Your cPanel username | +| `WU_CPANEL_PASSWORD` | Yes | - | cPanel password or API token | +| `WU_CPANEL_HOST` | Yes | - | cPanel server hostname | +| `WU_CPANEL_PORT` | No | 2083 | cPanel port (2083 for HTTPS) | + +## How It Works + +### Email Account Creation + +When a customer creates an email account: + +1. Ultimate Multisite calls the cPanel UAPI `Email/add_pop` endpoint +2. The account is created with the specified username, password, and quota +3. The account becomes immediately available + +### API Endpoint Used + +``` +POST https://{host}:{port}/execute/Email/add_pop +``` + +Parameters: +- `email`: Username portion of the email +- `password`: Generated password +- `quota`: Storage quota in MB (0 = unlimited) +- `domain`: Domain for the email account + +### Password Changes + +Password changes use the `Email/passwd_pop` UAPI endpoint. + +### Account Deletion + +Deletion uses the `Email/delete_pop` UAPI endpoint. + +## Email Client Settings + +Customers can configure their email clients with these settings: + +### IMAP (Incoming Mail) + +| Setting | Value | +|---------|-------| +| Server | Your cPanel hostname | +| Port | 993 | +| Security | SSL/TLS | +| Username | Full email address | + +### SMTP (Outgoing Mail) + +| Setting | Value | +|---------|-------| +| Server | Your cPanel hostname | +| Port | 465 | +| Security | SSL/TLS | +| Username | Full email address | + +### Webmail + +Customers can access webmail at: +``` +https://{cpanel_host}:2096/ +``` + +## DNS Configuration + +For each domain using cPanel email, configure these DNS records: + +``` +Type Host Value Priority +MX @ mail.yourdomain.com 10 +A mail {server_ip_address} - +``` + +### SPF Record (Recommended) + +``` +TXT @ v=spf1 +a +mx ~all +``` + +### DKIM (Optional) + +cPanel can generate DKIM records. Find them in: +**cPanel > Email > Email Deliverability** + +## Troubleshooting + +### "Connection failed" Error + +1. **Check credentials**: Verify username and password/token are correct +2. **Check hostname**: Ensure the hostname resolves correctly +3. **Check port**: Default is 2083 for HTTPS +4. **Check firewall**: Ensure your WordPress server can connect to cPanel + +### "Account creation failed" Error + +1. **Domain not on cPanel**: The domain must be added to cPanel first +2. **Account exists**: An account with that username may already exist +3. **Quota exceeded**: Check your cPanel account's email quota limits + +### "Permission denied" Error + +Your cPanel account may not have permission to create email accounts. Contact your hosting provider to enable this feature. + +### SSL Certificate Issues + +If you're getting SSL errors, you may need to add: + +```php +// Only use this for testing - not recommended for production +define('WU_CPANEL_VERIFY_SSL', false); +``` + +## Security Best Practices + +1. **Use API Tokens**: Instead of your cPanel password, create a dedicated API token with limited permissions + +2. **Restrict Token Permissions**: When creating an API token, only grant access to email-related functions + +3. **Secure wp-config.php**: Ensure your `wp-config.php` file is not publicly accessible + +4. **Use HTTPS**: Always use port 2083 (HTTPS) instead of 2082 (HTTP) + +## Related Documentation + +- [Email Accounts Integration](Email-Accounts-Integration) +- [cPanel API Documentation](https://api.docs.cpanel.net/cpanel/operation/Email-add_pop/) diff --git a/EMAIL_ACCOUNTS_FEATURE.md b/EMAIL_ACCOUNTS_FEATURE.md new file mode 100644 index 00000000..faf7d000 --- /dev/null +++ b/EMAIL_ACCOUNTS_FEATURE.md @@ -0,0 +1,296 @@ +# Email Accounts Feature + +This document describes the Email Accounts feature introduced in Ultimate Multisite 2.3.0, which allows network administrators to provision and manage email accounts for their customers. + +## Overview + +The Email Accounts feature enables: +- **Network admins** to configure email providers (cPanel, Purelymail, Google Workspace, Microsoft 365) +- **Customers** to create and manage email accounts through their wp-admin dashboard +- Support for both **membership-included quotas** AND **per-account purchases** +- Multiple providers can be enabled simultaneously - customers choose when creating accounts + +## Configuration + +### Enabling the Feature + +1. Go to **Ultimate Multisite > Settings > Email Accounts** +2. Enable "Email Accounts" toggle +3. Configure default quota and per-account purchase settings +4. Enable and configure one or more email providers + +### Provider Configuration + +Each provider requires specific constants to be defined in `wp-config.php`: + +#### cPanel Email +```php +define('WU_CPANEL_USERNAME', 'your_cpanel_username'); +define('WU_CPANEL_PASSWORD', 'your_cpanel_password'); +define('WU_CPANEL_HOST', 'your-server.com'); +define('WU_CPANEL_PORT', 2083); // Optional, defaults to 2083 +``` + +#### Purelymail +```php +define('WU_PURELYMAIL_API_KEY', 'your_api_key'); +``` + +Get your API key from your [Purelymail account settings](https://purelymail.com/manage/account). + +#### Google Workspace +```php +define('WU_GOOGLE_SERVICE_ACCOUNT_JSON', '/path/to/service-account.json'); +define('WU_GOOGLE_ADMIN_EMAIL', 'admin@yourdomain.com'); +define('WU_GOOGLE_CUSTOMER_ID', 'C01234567'); +``` + +Requirements: +1. Create a project in Google Cloud Console +2. Enable the Admin SDK API +3. Create a service account with domain-wide delegation +4. Download the JSON credentials file +5. Grant the service account the `https://www.googleapis.com/auth/admin.directory.user` scope + +#### Microsoft 365 +```php +define('WU_MS365_CLIENT_ID', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'); +define('WU_MS365_CLIENT_SECRET', 'your_client_secret'); +define('WU_MS365_TENANT_ID', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'); +define('WU_MS365_LICENSE_SKU', 'license_sku_id'); // Optional +``` + +Requirements: +1. Register an application in Azure Active Directory +2. Grant the application `User.ReadWrite.All` permission +3. Create a client secret +4. Note: License assignment requires the SKU ID of the license to assign + +### Membership Limitations + +Control email account quotas per product/membership: + +1. Edit a product in **Ultimate Multisite > Products** +2. Go to the **Limitations** tab +3. Enable "Email Accounts" limitation +4. Set the maximum number of accounts (0 = unlimited) + +## Customer Experience + +### Creating Email Accounts + +Customers can create email accounts from their **Account** page: + +1. Customer navigates to their Account page +2. Clicks "Create Email Account" +3. Selects provider (if multiple are enabled) +4. Enters username and selects domain +5. System provisions the account and displays credentials + +### Managing Email Accounts + +From the Account page, customers can: +- View all their email accounts +- Open webmail (provider-specific) +- View IMAP/SMTP settings +- View DNS setup instructions +- Delete email accounts + +### DNS Configuration + +When customers create email accounts, they need to configure DNS records for their domain. The system provides provider-specific DNS instructions including: +- MX records for mail routing +- SPF records for email authentication +- DKIM records for signature verification +- Optional DMARC policies + +## Developer Reference + +### Database Schema + +**Table:** `{prefix}_wu_email_accounts` + +| Column | Type | Description | +|--------|------|-------------| +| id | bigint(20) | Primary key | +| customer_id | bigint(20) | FK to customers | +| membership_id | bigint(20) | FK to memberships (nullable) | +| site_id | bigint(20) | FK to sites (nullable) | +| email_address | varchar(255) | Full email address | +| domain | varchar(191) | Domain portion | +| provider | varchar(50) | Provider ID | +| status | varchar(20) | pending/provisioning/active/suspended/failed | +| quota_mb | int | Storage quota in MB (0 = unlimited) | +| external_id | varchar(255) | Provider's external ID | +| password_hash | text | Encrypted password | +| purchase_type | varchar(50) | membership_included/per_account | +| payment_id | bigint(20) | FK to payments (for per-account) | +| date_created | datetime | Created timestamp | +| date_modified | datetime | Modified timestamp | + +### Helper Functions + +```php +// Get an email account by ID +$account = wu_get_email_account($id); + +// Get all accounts for a customer +$accounts = wu_get_email_accounts([ + 'customer_id' => $customer_id, +]); + +// Create an email account +$account = wu_create_email_account([ + 'customer_id' => $customer_id, + 'membership_id' => $membership_id, + 'email_address' => 'user@example.com', + 'provider' => 'cpanel', +]); + +// Check if customer can create more accounts +$can_create = wu_can_create_email_account($customer_id, $membership_id); + +// Count accounts +$count = wu_count_email_accounts($customer_id, $membership_id); + +// Get enabled providers +$providers = wu_get_enabled_email_providers(); +``` + +### Hooks and Filters + +#### Actions + +```php +// Fired when account is successfully provisioned +do_action('wu_email_account_provisioned', $email_account, $password); + +// Fired when provisioning fails +do_action('wu_email_account_provisioning_failed', $email_account, $error); + +// Fired when account is suspended +do_action('wu_email_account_suspended', $email_account); + +// Fired when account is reactivated +do_action('wu_email_account_reactivated', $email_account); + +// Load additional providers +add_action('wu_email_providers_load', function() { + // Register custom provider +}); +``` + +#### Filters + +```php +// Filter the list of registered providers +add_filter('wu_email_manager_get_providers', function($providers, $manager) { + $providers['custom'] = My_Custom_Provider::class; + return $providers; +}, 10, 2); +``` + +### Creating Custom Providers + +Extend `Base_Email_Provider` to create custom email providers: + +```php +namespace My_Plugin; + +use WP_Ultimo\Integrations\Email_Providers\Base_Email_Provider; +use WP_Ultimo\Models\Email_Account; + +class My_Custom_Provider extends Base_Email_Provider { + + protected $id = 'my_provider'; + protected $title = 'My Provider'; + protected $constants = ['MY_PROVIDER_API_KEY']; + + public function create_email_account(array $params) { + // Implementation + } + + public function delete_email_account($email_address) { + // Implementation + } + + public function change_password($email_address, $new_password) { + // Implementation + } + + public function get_account_info($email_address) { + // Implementation + } + + public function get_webmail_url(Email_Account $account) { + return 'https://webmail.myprovider.com/'; + } + + public function get_dns_instructions($domain) { + return [ + [ + 'type' => 'MX', + 'name' => '@', + 'value' => 'mail.myprovider.com', + 'priority' => 10, + 'description' => 'Mail server record', + ], + ]; + } +} +``` + +Register your provider: + +```php +add_action('wu_email_providers_load', function() { + My_Custom_Provider::get_instance(); +}); +``` + +## File Structure + +``` +inc/ +├── database/email-accounts/ +│ ├── class-email-accounts-schema.php +│ ├── class-email-accounts-table.php +│ ├── class-email-account-query.php +│ └── class-email-account-status.php +├── models/ +│ └── class-email-account.php +├── functions/ +│ └── email-account.php +├── integrations/email-providers/ +│ ├── class-base-email-provider.php +│ ├── class-cpanel-email-provider.php +│ ├── class-purelymail-provider.php +│ ├── class-google-workspace-provider.php +│ └── class-microsoft365-provider.php +├── managers/ +│ └── class-email-account-manager.php +├── limitations/ +│ └── class-limit-email-accounts.php +└── ui/ + └── class-email-accounts-element.php + +views/dashboard-widgets/ +└── email-accounts.php +``` + +## Security Considerations + +1. **Password Storage**: Passwords are encrypted using sodium and stored temporarily with time-limited tokens for one-time display +2. **API Credentials**: All provider credentials are stored in `wp-config.php` constants, not in the database +3. **Permission Checks**: All operations verify customer ownership before execution +4. **Domain Validation**: Email addresses are validated against customer-owned domains +5. **Rate Limiting**: Account creation is controlled through membership quotas + +## Provider Affiliate Links + +When displaying setup instructions, the plugin includes affiliate referral links: +- **Purelymail**: `https://purelymail.com/?ref=ultimatemultisite` +- **Google Workspace**: Google Workspace partner program +- **Microsoft 365**: Microsoft partner program + +These help support Ultimate Multisite development while providing quality email services. diff --git a/inc/admin-pages/class-email-integration-wizard-admin-page.php b/inc/admin-pages/class-email-integration-wizard-admin-page.php new file mode 100644 index 00000000..c8b3114c --- /dev/null +++ b/inc/admin-pages/class-email-integration-wizard-admin-page.php @@ -0,0 +1,409 @@ + 'manage_network', + ]; + + /** + * Current integration being setup. + * + * @since 2.3.0 + * @var \WP_Ultimo\Integrations\Email_Providers\Base_Email_Provider + */ + protected $integration; + + /** + * Allow child classes to add further initializations. + * + * @since 2.3.0 + * @return void + */ + public function page_loaded(): void { + + if (isset($_GET['integration'])) { // phpcs:ignore WordPress.Security.NonceVerification + $email_manager = \WP_Ultimo\Managers\Email_Account_Manager::get_instance(); + + $this->integration = $email_manager->get_provider_instance(sanitize_text_field(wp_unslash($_GET['integration']))); // phpcs:ignore WordPress.Security.NonceVerification + } + + if ( ! $this->integration) { + wp_safe_redirect(network_admin_url('admin.php?page=wp-ultimo-settings&tab=email-accounts')); + + exit; + } + + parent::page_loaded(); + } + + /** + * Returns the title of the page. + * + * @since 2.3.0 + * @return string Title of the page. + */ + public function get_title(): string { + + return __('Email Provider Setup', 'ultimate-multisite'); + } + + /** + * Returns the title of menu for this page. + * + * @since 2.3.0 + * @return string Menu label of the page. + */ + public function get_menu_title() { + + return __('Email Provider Integration', 'ultimate-multisite'); + } + + /** + * Returns the sections for this Wizard. + * + * @since 2.3.0 + * @return array + */ + public function get_sections() { + + $sections = [ + 'activation' => [ + 'title' => __('Activation', 'ultimate-multisite'), + 'view' => [$this, 'section_activation'], + 'handler' => [$this, 'handle_activation'], + ], + 'instructions' => [ + 'title' => __('Instructions', 'ultimate-multisite'), + 'view' => [$this, 'section_instructions'], + ], + 'config' => [ + 'title' => __('Configuration', 'ultimate-multisite'), + 'view' => [$this, 'section_configuration'], + 'handler' => [$this, 'handle_configuration'], + ], + 'testing' => [ + 'title' => __('Testing Integration', 'ultimate-multisite'), + 'view' => [$this, 'section_test'], + ], + 'done' => [ + 'title' => __('Ready!', 'ultimate-multisite'), + 'view' => [$this, 'section_ready'], + ], + ]; + + /* + * Some providers require no instructions. + */ + if (method_exists($this->integration, 'supports') && $this->integration->supports('no-instructions')) { + unset($sections['instructions']); + } + + /* + * Some providers require no additional setup. + */ + if (method_exists($this->integration, 'supports') && $this->integration->supports('no-config')) { + unset($sections['config']); + } + + return $sections; + } + + /** + * Displays the content of the activation section. + * + * @since 2.3.0 + * @return void + */ + public function section_activation(): void { + + $explainer_lines = $this->integration->get_explainer_lines(); + + wu_get_template( + 'wizards/email-integrations/activation', + [ + 'screen' => get_current_screen(), + 'page' => $this, + 'integration' => $this->integration, + 'will' => $explainer_lines['will'], + 'will_not' => $explainer_lines['will_not'], + ] + ); + } + + /** + * Displays the contents of the instructions section. + * + * @since 2.3.0 + * @return void + */ + public function section_instructions(): void { + + if (method_exists($this->integration, 'get_instructions')) { + call_user_func([$this->integration, 'get_instructions']); + } else { + wu_get_template( + 'wizards/email-integrations/default-instructions', + [ + 'screen' => get_current_screen(), + 'page' => $this, + 'integration' => $this->integration, + ] + ); + } + + $this->render_submit_box(); + } + + /** + * Displays the content of the configuration section. + * + * @since 2.3.0 + * @return void + */ + public function section_configuration(): void { + + $fields = $this->integration->get_fields(); + + foreach ($fields as $field_constant => &$field) { + $field['value'] = defined($field_constant) && constant($field_constant) ? constant($field_constant) : ''; + } + + $form = new \WP_Ultimo\UI\Form( + $this->get_current_section(), + $fields, + [ + 'views' => 'admin-pages/fields', + 'classes' => 'wu-widget-list wu-striped wu-m-0 wu--mt-2 wu--mb-3 wu--mx-3', + 'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-px-6 wu-py-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid', + ] + ); + + if (wu_request('manual')) { + wu_get_template( + 'wizards/email-integrations/configuration-results', + [ + 'screen' => get_current_screen(), + 'page' => $this, + 'integration' => $this->integration, + 'form' => $form, + 'post' => wu_request('post'), + ] + ); + + return; + } + + wu_get_template( + 'wizards/email-integrations/configuration', + [ + 'screen' => get_current_screen(), + 'page' => $this, + 'integration' => $this->integration, + 'form' => $form, + ] + ); + } + + /** + * Displays the content of the final section. + * + * @since 2.3.0 + * @return void + */ + public function section_ready(): void { + + wu_get_template( + 'wizards/email-integrations/ready', + [ + 'screen' => get_current_screen(), + 'page' => $this, + 'integration' => $this->integration, + ] + ); + } + + /** + * Handles the activation of a given integration. + * + * @since 2.3.0 + * @return void + */ + public function handle_activation(): void { + + $is_enabled = $this->integration->is_enabled(); + + if ($is_enabled) { + $this->integration->disable(); + + return; + } + + $this->integration->enable(); + + wp_safe_redirect($this->get_next_section_link()); + + exit; + } + + /** + * Handles the configuration of a given integration. + * + * @since 2.3.0 + * @return void + */ + public function handle_configuration(): void { + + check_admin_referer('saving_config', 'saving_config'); + + $allowed_fields = array_keys($this->integration->get_fields()); + + // Filter and sanitize $_POST to only include allowed integration fields + $filtered_data = []; + foreach ($allowed_fields as $field) { + if (isset($_POST[ $field ])) { + $filtered_data[ $field ] = sanitize_text_field(wp_unslash($_POST[ $field ])); + } + } + + if ((int) wu_request('submit') === 0) { + $redirect_url = add_query_arg( + [ + 'manual' => '1', + 'post' => wp_json_encode($filtered_data), + ] + ); + + wp_safe_redirect($redirect_url); + + exit; + } + + if ((int) wu_request('submit') === 1) { + $this->integration->setup_constants($filtered_data); + } + + $redirect_url = $this->get_next_section_link(); + + $redirect_url = remove_query_arg('post', $redirect_url); + + $redirect_url = remove_query_arg('manual', $redirect_url); + + wp_safe_redirect($redirect_url); + + exit; + } + + /** + * Handles the testing of a given configuration. + * + * @since 2.3.0 + * @return void + */ + public function section_test(): void { + + wp_enqueue_script('wu-vue'); + + wu_get_template( + 'wizards/email-integrations/test', + [ + 'screen' => get_current_screen(), + 'page' => $this, + 'integration' => $this->integration, + ] + ); + } + + /** + * Register the script for the test page. + * + * @since 2.3.0 + * @return void + */ + public function register_scripts() { + + parent::register_scripts(); + + wp_enqueue_script( + 'wu-email-integration-test', + wu_get_asset('email-integration-test.js', 'js'), + [ + 'jquery', + 'wu-vue', + ], + wu_get_version(), + true + ); + + wp_add_inline_script( + 'wu-email-integration-test', + 'var wu_email_integration_test_data = { + integration_id: "' . esc_js($this->integration->get_id()) . '", + waiting_message: "' . esc_js(__('Waiting for results...', 'ultimate-multisite')) . '" + };', + 'before' + ); + } +} diff --git a/inc/admin-pages/customer-panel/class-account-admin-page.php b/inc/admin-pages/customer-panel/class-account-admin-page.php index 850f57dd..0f113e41 100644 --- a/inc/admin-pages/customer-panel/class-account-admin-page.php +++ b/inc/admin-pages/customer-panel/class-account-admin-page.php @@ -182,6 +182,8 @@ public function register_widgets(): void { \WP_Ultimo\UI\Domain_Mapping_Element::get_instance()->as_metabox(get_current_screen()->id, 'side'); + \WP_Ultimo\UI\Email_Accounts_Element::get_instance()->as_metabox(get_current_screen()->id, 'side'); + \WP_Ultimo\UI\Login_Form_Element::get_instance()->as_inline_content(get_current_screen()->id, 'wu_dash_before_metaboxes'); \WP_Ultimo\UI\Simple_Text_Element::get_instance()->as_inline_content(get_current_screen()->id, 'wu_dash_before_metaboxes'); diff --git a/inc/class-settings.php b/inc/class-settings.php index 26d63eb7..380d8903 100644 --- a/inc/class-settings.php +++ b/inc/class-settings.php @@ -1473,6 +1473,22 @@ public function default_sections(): void { do_action('wu_settings_domain_mapping'); + /* + * Email Accounts + * This section holds the Email Accounts settings. + */ + + $this->add_section( + 'email-accounts', + [ + 'title' => __('Email Accounts', 'ultimate-multisite'), + 'desc' => __('Email Accounts', 'ultimate-multisite'), + 'icon' => 'dashicons-wu-mail', + ] + ); + + do_action('wu_settings_email-accounts'); + /* * Single Sign-on * This section includes settings related to the single sign-on functionality diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php index 0057eff1..6a7b96d3 100644 --- a/inc/class-wp-ultimo.php +++ b/inc/class-wp-ultimo.php @@ -353,6 +353,7 @@ public function load_public_apis(): void { require_once wu_path('inc/functions/site.php'); require_once wu_path('inc/functions/user.php'); require_once wu_path('inc/functions/webhook.php'); + require_once wu_path('inc/functions/email-account.php'); /** * URL and Asset Helpers @@ -504,6 +505,7 @@ protected function load_extra_components(): void { \WP_Ultimo\UI\Account_Summary_Element::get_instance(); \WP_Ultimo\UI\Limits_Element::get_instance(); \WP_Ultimo\UI\Domain_Mapping_Element::get_instance(); + \WP_Ultimo\UI\Email_Accounts_Element::get_instance(); \WP_Ultimo\UI\Site_Maintenance_Element::get_instance(); \WP_Ultimo\UI\Template_Switching_Element::get_instance(); @@ -762,6 +764,11 @@ protected function load_admin_pages(): void { */ new WP_Ultimo\Admin_Pages\Hosting_Integration_Wizard_Admin_Page(); + /* + * Loads the Email Integration Wizard + */ + new WP_Ultimo\Admin_Pages\Email_Integration_Wizard_Admin_Page(); + /* * Loads the Events Pages */ @@ -926,6 +933,12 @@ protected function load_managers(): void { * Loads the Cache manager. */ WP_Ultimo\Managers\Cache_Manager::get_instance(); + + /* + * Loads the Email Account manager. + */ + WP_Ultimo\Managers\Email_Account_Manager::get_instance(); + WP_Ultimo\Orphaned_Tables_Manager::get_instance(); WP_Ultimo\Orphaned_Users_Manager::get_instance(); diff --git a/inc/database/email-accounts/class-email-account-query.php b/inc/database/email-accounts/class-email-account-query.php new file mode 100644 index 00000000..0f688553 --- /dev/null +++ b/inc/database/email-accounts/class-email-account-query.php @@ -0,0 +1,111 @@ + CSS Classes. + * + * @since 2.3.0 + * @return array + */ + protected function classes() { + + return [ + static::PENDING => 'wu-bg-gray-200 wu-text-gray-700', + static::PROVISIONING => 'wu-bg-blue-200 wu-text-blue-700', + static::ACTIVE => 'wu-bg-green-200 wu-text-green-700', + static::SUSPENDED => 'wu-bg-yellow-200 wu-text-yellow-700', + static::FAILED => 'wu-bg-red-200 wu-text-red-700', + ]; + } + + /** + * Returns an array with values => labels. + * + * @since 2.3.0 + * @return array + */ + protected function labels() { + + return [ + static::PENDING => __('Pending', 'ultimate-multisite'), + static::PROVISIONING => __('Provisioning', 'ultimate-multisite'), + static::ACTIVE => __('Active', 'ultimate-multisite'), + static::SUSPENDED => __('Suspended', 'ultimate-multisite'), + static::FAILED => __('Failed', 'ultimate-multisite'), + ]; + } + + /** + * Returns an array with values => icons. + * + * @since 2.3.0 + * @return array + */ + protected function icons() { + + return [ + static::PENDING => 'dashicons-wu-clock', + static::PROVISIONING => 'dashicons-wu-loader', + static::ACTIVE => 'dashicons-wu-check', + static::SUSPENDED => 'dashicons-wu-block', + static::FAILED => 'dashicons-wu-circle-with-cross', + ]; + } + + /** + * Get the icon for the current status. + * + * @since 2.3.0 + * @return string + */ + public function get_icon(): string { + + $icons = $this->icons(); + + return $icons[ $this->get_value() ] ?? ''; + } +} diff --git a/inc/database/email-accounts/class-email-accounts-schema.php b/inc/database/email-accounts/class-email-accounts-schema.php new file mode 100644 index 00000000..ce62e3ad --- /dev/null +++ b/inc/database/email-accounts/class-email-accounts-schema.php @@ -0,0 +1,162 @@ + 'id', + 'type' => 'bigint', + 'length' => '20', + 'unsigned' => true, + 'extra' => 'auto_increment', + 'primary' => true, + 'sortable' => true, + ], + + [ + 'name' => 'customer_id', + 'type' => 'bigint', + 'length' => '20', + 'unsigned' => true, + 'searchable' => true, + 'sortable' => true, + ], + + [ + 'name' => 'membership_id', + 'type' => 'bigint', + 'length' => '20', + 'unsigned' => true, + 'allow_null' => true, + 'searchable' => true, + 'sortable' => true, + ], + + [ + 'name' => 'site_id', + 'type' => 'bigint', + 'length' => '20', + 'unsigned' => true, + 'allow_null' => true, + 'aliases' => ['blog_id'], + 'searchable' => true, + 'sortable' => true, + ], + + [ + 'name' => 'email_address', + 'type' => 'varchar', + 'length' => '255', + 'searchable' => true, + 'sortable' => true, + ], + + [ + 'name' => 'domain', + 'type' => 'varchar', + 'length' => '191', + 'searchable' => true, + 'sortable' => true, + ], + + [ + 'name' => 'provider', + 'type' => 'varchar', + 'length' => '50', + 'searchable' => true, + 'sortable' => true, + ], + + [ + 'name' => 'status', + 'type' => 'enum(\'pending\', \'provisioning\', \'active\', \'suspended\', \'failed\')', + 'default' => 'pending', + 'transition' => true, + 'sortable' => true, + ], + + [ + 'name' => 'quota_mb', + 'type' => 'int', + 'unsigned' => true, + 'default' => 0, + 'sortable' => true, + ], + + [ + 'name' => 'external_id', + 'type' => 'varchar', + 'length' => '255', + 'allow_null' => true, + ], + + [ + 'name' => 'password_hash', + 'type' => 'text', + 'allow_null' => true, + ], + + [ + 'name' => 'purchase_type', + 'type' => 'enum(\'membership_included\', \'per_account\')', + 'default' => 'membership_included', + 'sortable' => true, + ], + + [ + 'name' => 'payment_id', + 'type' => 'bigint', + 'length' => '20', + 'unsigned' => true, + 'allow_null' => true, + ], + + [ + 'name' => 'date_created', + 'type' => 'datetime', + 'default' => null, + 'created' => true, + 'date_query' => true, + 'sortable' => true, + 'allow_null' => true, + ], + + [ + 'name' => 'date_modified', + 'type' => 'datetime', + 'default' => null, + 'modified' => true, + 'date_query' => true, + 'sortable' => true, + 'allow_null' => true, + ], + + ]; +} diff --git a/inc/database/email-accounts/class-email-accounts-table.php b/inc/database/email-accounts/class-email-accounts-table.php new file mode 100644 index 00000000..69b95f83 --- /dev/null +++ b/inc/database/email-accounts/class-email-accounts-table.php @@ -0,0 +1,88 @@ +schema = "id bigint(20) NOT NULL auto_increment, + customer_id bigint(20) NOT NULL, + membership_id bigint(20) NULL, + site_id bigint(20) NULL, + email_address varchar(255) NOT NULL, + domain varchar(191) NOT NULL, + provider varchar(50) NOT NULL, + status enum('pending', 'provisioning', 'active', 'suspended', 'failed') DEFAULT 'pending', + quota_mb int(11) unsigned DEFAULT 0, + external_id varchar(255) NULL, + password_hash text NULL, + purchase_type enum('membership_included', 'per_account') DEFAULT 'membership_included', + payment_id bigint(20) NULL, + date_created datetime NULL, + date_modified datetime NULL, + PRIMARY KEY (id), + KEY customer_id (customer_id), + KEY membership_id (membership_id), + KEY site_id (site_id), + KEY email_address (email_address), + KEY domain (domain), + KEY provider (provider), + KEY status (status)"; + } +} diff --git a/inc/functions/email-account.php b/inc/functions/email-account.php new file mode 100644 index 00000000..bc5b6efb --- /dev/null +++ b/inc/functions/email-account.php @@ -0,0 +1,400 @@ + $customer_id, + ] + ); +} + +/** + * Gets email accounts for a site. + * + * @since 2.3.0 + * + * @param int $site_id The site ID. + * @return \WP_Ultimo\Models\Email_Account[] + */ +function wu_get_email_accounts_by_site($site_id) { + + return wu_get_email_accounts( + [ + 'site_id' => $site_id, + ] + ); +} + +/** + * Gets email accounts for a membership. + * + * @since 2.3.0 + * + * @param int $membership_id The membership ID. + * @return \WP_Ultimo\Models\Email_Account[] + */ +function wu_get_email_accounts_by_membership($membership_id) { + + return wu_get_email_accounts( + [ + 'membership_id' => $membership_id, + ] + ); +} + +/** + * Creates a new email account. + * + * @since 2.3.0 + * + * @param array $email_account_data Email account attributes. + * @return \WP_Error|\WP_Ultimo\Models\Email_Account + */ +function wu_create_email_account($email_account_data) { + + $email_account_data = wp_parse_args( + $email_account_data, + [ + 'customer_id' => 0, + 'membership_id' => null, + 'site_id' => null, + 'email_address' => '', + 'domain' => '', + 'provider' => '', + 'status' => 'pending', + 'quota_mb' => wu_get_setting('email_default_quota_mb', 1024), + 'purchase_type' => 'membership_included', + 'payment_id' => null, + 'date_created' => wu_get_current_time('mysql', true), + 'date_modified' => wu_get_current_time('mysql', true), + ] + ); + + // Auto-extract domain from email if not provided + if (empty($email_account_data['domain']) && ! empty($email_account_data['email_address'])) { + $parts = explode('@', $email_account_data['email_address']); + if (2 === count($parts)) { + $email_account_data['domain'] = $parts[1]; + } + } + + $email_account = new Email_Account($email_account_data); + + $saved = $email_account->save(); + + if (is_wp_error($saved)) { + return $saved; + } + + /** + * Enqueue the provisioning action. + */ + wu_enqueue_async_action( + 'wu_async_provision_email_account', + ['email_account_id' => $email_account->get_id()], + 'email_account' + ); + + /** + * Triggers when a new email account is created. + * + * @since 2.3.0 + * + * @param \WP_Ultimo\Models\Email_Account $email_account The email account object. + */ + do_action('wu_email_account_created', $email_account); + + return $email_account; +} + +/** + * Counts email accounts for a customer. + * + * @since 2.3.0 + * + * @param int $customer_id The customer ID. + * @param int|null $membership_id Optional membership ID. + * @return int + */ +function wu_count_email_accounts($customer_id, $membership_id = null) { + + $args = [ + 'customer_id' => $customer_id, + 'count' => true, + ]; + + if ($membership_id) { + $args['membership_id'] = $membership_id; + } + + return (int) wu_get_email_accounts($args); +} + +/** + * Checks if a customer can create more email accounts. + * + * @since 2.3.0 + * + * @param int $customer_id The customer ID. + * @param int $membership_id The membership ID. + * @return bool + */ +function wu_can_create_email_account($customer_id, $membership_id) { + + // Check if email accounts feature is enabled + if ( ! wu_get_setting('enable_email_accounts', false)) { + return false; + } + + $membership = wu_get_membership($membership_id); + + if ( ! $membership) { + return false; + } + + // Check if membership has email accounts enabled + if ($membership->has_limitations()) { + $limitations = $membership->get_limitations(); + + if ( ! isset($limitations->email_accounts) || ! $limitations->email_accounts->is_enabled()) { + return false; + } + + $limit = $limitations->email_accounts->get_limit(); + + // 0 means unlimited + if ($limit > 0) { + $current_count = wu_count_email_accounts($customer_id, $membership_id); + + if ($current_count >= $limit) { + return false; + } + } + } + + /** + * Filter whether a customer can create an email account. + * + * @since 2.3.0 + * + * @param bool $can_create Whether the customer can create an email account. + * @param int $customer_id The customer ID. + * @param int $membership_id The membership ID. + */ + return apply_filters('wu_can_create_email_account', true, $customer_id, $membership_id); +} + +/** + * Gets the enabled email providers. + * + * @since 2.3.0 + * + * @return array Array of enabled provider instances. + */ +function wu_get_enabled_email_providers() { + + $manager = \WP_Ultimo\Managers\Email_Account_Manager::get_instance(); + + return $manager->get_enabled_providers(); +} + +/** + * Gets a specific email provider by ID. + * + * @since 2.3.0 + * + * @param string $provider_id The provider ID. + * @return \WP_Ultimo\Integrations\Email_Providers\Base_Email_Provider|null + */ +function wu_get_email_provider($provider_id) { + + $manager = \WP_Ultimo\Managers\Email_Account_Manager::get_instance(); + + return $manager->get_provider($provider_id); +} + +/** + * Gets the per-account email price. + * + * @since 2.3.0 + * + * @return float + */ +function wu_get_email_account_price() { + + return (float) wu_get_setting('email_account_price', 5.00); +} + +/** + * Encrypts a password for temporary storage. + * + * @since 2.3.0 + * + * @param string $password The password to encrypt. + * @return string The encrypted password. + */ +function wu_encrypt_email_password($password) { + + if (function_exists('sodium_crypto_secretbox')) { + $key = substr(hash('sha256', wp_salt('auth'), true), 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES); + $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + + $encrypted = sodium_crypto_secretbox($password, $nonce, $key); + + return base64_encode($nonce . $encrypted); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + } + + // Fallback to basic encoding (not secure, but better than plaintext) + return base64_encode($password); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode +} + +/** + * Decrypts a password from storage. + * + * @since 2.3.0 + * + * @param string $encrypted The encrypted password. + * @return string|false The decrypted password or false on failure. + */ +function wu_decrypt_email_password($encrypted) { + + if (function_exists('sodium_crypto_secretbox_open')) { + $key = substr(hash('sha256', wp_salt('auth'), true), 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES); + $decoded = base64_decode($encrypted, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode + + if (false === $decoded) { + return false; + } + + $nonce = substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $ciphertext = substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + + $decrypted = sodium_crypto_secretbox_open($ciphertext, $nonce, $key); + + return false !== $decrypted ? $decrypted : false; + } + + // Fallback for basic encoding + return base64_decode($encrypted, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode +} + +/** + * Stores a temporary password token for one-time display. + * + * @since 2.3.0 + * + * @param int $email_account_id The email account ID. + * @param string $password The password to store. + * @return string The access token. + */ +function wu_store_email_password_token($email_account_id, $password) { + + $token = wp_generate_password(32, false); + $encrypted = wu_encrypt_email_password($password); + + set_transient( + 'wu_email_pwd_' . $token, + [ + 'email_account_id' => $email_account_id, + 'password' => $encrypted, + ], + 600 // 10 minutes + ); + + return $token; +} + +/** + * Retrieves and deletes a temporary password token. + * + * @since 2.3.0 + * + * @param string $token The access token. + * @param int $email_account_id The email account ID for verification. + * @return string|false The password or false on failure. + */ +function wu_get_email_password_from_token($token, $email_account_id) { + + $data = get_transient('wu_email_pwd_' . $token); + + if ( ! $data || ! isset($data['email_account_id']) || (int) $data['email_account_id'] !== (int) $email_account_id) { + return false; + } + + // Delete the transient after retrieval (one-time use) + delete_transient('wu_email_pwd_' . $token); + + return wu_decrypt_email_password($data['password']); +} + +/** + * Generates a strong random password for email accounts. + * + * @since 2.3.0 + * + * @param int $length The password length. + * @return string + */ +function wu_generate_email_password($length = 16) { + + return wp_generate_password($length, true, false); +} diff --git a/inc/integrations/email-providers/class-base-email-provider.php b/inc/integrations/email-providers/class-base-email-provider.php new file mode 100644 index 00000000..80e1e37e --- /dev/null +++ b/inc/integrations/email-providers/class-base-email-provider.php @@ -0,0 +1,603 @@ +is_enabled() && $this->is_setup()) { + $this->register_hooks(); + } + } + + /** + * Let the class register itself on the manager. + * + * @since 2.3.0 + * + * @param array $providers List of providers added so far. + * @return array + */ + final public function self_register($providers) { + + $providers[ $this->get_id() ] = static::class; + + return $providers; + } + + /** + * Get the list of enabled email providers. + * + * @since 2.3.0 + * @return array + */ + protected function get_enabled_list() { + + return get_network_option(null, 'wu_email_providers_enabled', []); + } + + /** + * Check if this provider is enabled. + * + * @since 2.3.0 + * @return boolean + */ + final public function is_enabled() { + + $list = $this->get_enabled_list(); + + return wu_get_isset($list, $this->get_id(), false); + } + + /** + * Enables this provider. + * + * @since 2.3.0 + * @return boolean + */ + public function enable() { + + $list = $this->get_enabled_list(); + + $list[ $this->get_id() ] = true; + + return update_network_option(null, 'wu_email_providers_enabled', $list); + } + + /** + * Disables this provider. + * + * @since 2.3.0 + * @return boolean + */ + public function disable() { + + $list = $this->get_enabled_list(); + + $list[ $this->get_id() ] = false; + + return update_network_option(null, 'wu_email_providers_enabled', $list); + } + + /** + * Returns the provider id. + * + * @since 2.3.0 + * @return string + */ + public function get_id() { + + return $this->id; + } + + /** + * Returns the provider title. + * + * @since 2.3.0 + * @return string + */ + public function get_title() { + + return $this->title; + } + + /** + * Returns the affiliate URL. + * + * @since 2.3.0 + * @return string + */ + public function get_affiliate_url() { + + return $this->affiliate_url; + } + + /** + * Returns the documentation link. + * + * @since 2.3.0 + * @return string + */ + public function get_documentation_link() { + + return $this->documentation_link; + } + + /** + * Checks if the integration is correctly setup. + * + * @since 2.3.0 + * @return boolean + */ + public function is_setup() { + + foreach ($this->constants as $constant) { + $constants = is_array($constant) ? $constant : [$constant]; + + $found = false; + + foreach ($constants as $const) { + if (defined($const) && constant($const)) { + $found = true; + break; + } + } + + if ( ! $found) { + return false; + } + } + + return true; + } + + /** + * Returns a list of missing constants. + * + * @since 2.3.0 + * @return array + */ + public function get_missing_constants() { + + $missing = []; + + foreach ($this->constants as $constant) { + $constants = is_array($constant) ? $constant : [$constant]; + + $found = false; + + foreach ($constants as $const) { + if (defined($const) && constant($const)) { + $found = true; + break; + } + } + + if ( ! $found) { + $missing = array_merge($missing, $constants); + } + } + + return $missing; + } + + /** + * Returns all constants. + * + * @since 2.3.0 + * @return array + */ + public function get_all_constants() { + + $constants = []; + + foreach ($this->constants as $constant) { + $current = is_array($constant) ? $constant : [$constant]; + $constants = array_merge($constants, $current); + } + + return array_merge($constants, $this->optional_constants); + } + + /** + * Get Fields for the integration configuration. + * + * @since 2.3.0 + * @return array + */ + public function get_fields() { + + return []; + } + + /** + * Adds the constants with their values into wp-config.php. + * + * @since 2.3.0 + * + * @param array $constant_values Key => Value of the constants. + * @return void + */ + public function setup_constants($constant_values): void { + + $values = shortcode_atts(array_flip($this->get_all_constants()), $constant_values); + + foreach ($values as $constant => $value) { + WP_Config::get_instance()->inject_wp_config_constant($constant, $value); + } + } + + /** + * Generates a define string for manual insertion. + * + * @since 2.3.0 + * + * @param array $constant_values Key => Value of the constants. + * @return string + */ + public function get_constants_string($constant_values) { + + $content = [ + sprintf('// Ultimate Multisite - Email Provider - %s', $this->get_title()), + ]; + + $constant_values = shortcode_atts(array_flip($this->get_all_constants()), $constant_values); + + foreach ($constant_values as $constant => $value) { + $content[] = sprintf("define( '%s', '%s' );", $constant, $value); + } + + $content[] = sprintf('// Ultimate Multisite - Email Provider - %s - End', $this->get_title()); + + return implode(PHP_EOL, $content); + } + + /** + * Adds the provider to the settings list. + * + * @since 2.3.0 + * @return void + */ + public function add_to_integration_list(): void { + + if ( ! wu_get_setting('enable_email_accounts', false)) { + return; + } + + $slug = $this->get_id(); + + // Build status indicator + $html = ''; + + if ($this->is_enabled()) { + if ($this->is_setup()) { + $html .= sprintf( + '
%s
', + __('Activated', 'ultimate-multisite') + ); + } else { + $html .= sprintf( + '
%s
', + __('Not Configured', 'ultimate-multisite') + ); + } + } + + // Add Configuration button + $url = wu_network_admin_url( + 'wp-ultimo-email-integration-wizard', + [ + 'integration' => $slug, + ] + ); + + $html .= sprintf( + '%s', + esc_url($url), + __('Configuration', 'ultimate-multisite') + ); + + wu_register_settings_field( + 'email-accounts', + "email_provider_{$slug}", + [ + 'type' => 'note', + // translators: %s is the provider name (e.g. "cPanel", "Purelymail") + 'title' => sprintf(__('%s Integration', 'ultimate-multisite'), $this->get_title()), + 'desc' => $html, + ] + ); + } + + /** + * Returns explainer lines for the activation wizard. + * + * @since 2.3.0 + * @return array + */ + public function get_explainer_lines() { + + return [ + 'will' => [ + __('Allow customers to create email accounts with this provider', 'ultimate-multisite'), + __('Automatically provision email accounts via API', 'ultimate-multisite'), + __('Allow customers to manage their email account passwords', 'ultimate-multisite'), + ], + 'will_not' => [ + __('Automatically configure DNS records (customers must do this manually)', 'ultimate-multisite'), + __('Provide email hosting (you need an account with the provider)', 'ultimate-multisite'), + ], + ]; + } + + /** + * Checks if the integration supports a given feature. + * + * @since 2.3.0 + * + * @param string $feature The feature to check. + * @return bool + */ + public function supports($feature) { + + $supports = property_exists($this, 'supports') ? $this->supports : []; + + return in_array($feature, $supports, true); + } + + /** + * Register hooks for this provider. + * + * @since 2.3.0 + * @return void + */ + public function register_hooks(): void { + + // Providers can override this to add specific hooks + } + + /** + * Returns the description of this provider. + * + * @since 2.3.0 + * @return string + */ + abstract public function get_description(); + + /** + * Returns the logo URL for the provider. + * + * @since 2.3.0 + * @return string + */ + abstract public function get_logo(); + + /** + * Creates an email account with the provider. + * + * @since 2.3.0 + * + * @param array $params The account parameters. + * - username: string The username portion of the email. + * - domain: string The domain for the email. + * - password: string The password for the account. + * - quota_mb: int Optional quota in MB. + * - display_name: string Optional display name. + * @return array|\WP_Error Array with account details on success, WP_Error on failure. + */ + abstract public function create_email_account(array $params); + + /** + * Deletes an email account from the provider. + * + * @since 2.3.0 + * + * @param string $email_address The email address to delete. + * @return bool|\WP_Error True on success, WP_Error on failure. + */ + abstract public function delete_email_account($email_address); + + /** + * Changes the password for an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address. + * @param string $new_password The new password. + * @return bool|\WP_Error True on success, WP_Error on failure. + */ + abstract public function change_password($email_address, $new_password); + + /** + * Gets information about an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address. + * @return array|\WP_Error Account info on success, WP_Error on failure. + */ + abstract public function get_account_info($email_address); + + /** + * Gets the webmail URL for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return string The webmail URL. + */ + abstract public function get_webmail_url(Email_Account $account); + + /** + * Gets the DNS instructions for a domain. + * + * @since 2.3.0 + * + * @param string $domain The domain. + * @return array Array of DNS record instructions. + */ + abstract public function get_dns_instructions($domain); + + /** + * Gets the IMAP settings for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return array IMAP settings. + */ + public function get_imap_settings(Email_Account $account) { + + return [ + 'server' => '', + 'port' => 993, + 'security' => 'SSL/TLS', + 'username' => $account->get_email_address(), + ]; + } + + /** + * Gets the SMTP settings for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return array SMTP settings. + */ + public function get_smtp_settings(Email_Account $account) { + + return [ + 'server' => '', + 'port' => 587, + 'security' => 'STARTTLS', + 'username' => $account->get_email_address(), + ]; + } + + /** + * Tests the connection with the provider API. + * + * @since 2.3.0 + * @return bool|\WP_Error True on success, WP_Error on failure. + */ + public function test_connection() { + + return true; + } + + /** + * Logs a message for this provider. + * + * @since 2.3.0 + * + * @param string $message The message to log. + * @param string $level The log level. + * @return void + */ + protected function log($message, $level = 'info'): void { + + wu_log_add('email-provider-' . $this->get_id(), $message, $level); + } + + /** + * Gets the signup instructions with affiliate link. + * + * @since 2.3.0 + * @return string + */ + public function get_signup_instructions() { + + $affiliate_url = $this->get_affiliate_url(); + + if (empty($affiliate_url)) { + return ''; + } + + return sprintf( + /* translators: %1$s is the provider name, %2$s is the affiliate URL */ + __('Don\'t have a %1$s account yet? Sign up here.', 'ultimate-multisite'), + $this->get_title(), + esc_url($affiliate_url) + ); + } +} diff --git a/inc/integrations/email-providers/class-cpanel-email-provider.php b/inc/integrations/email-providers/class-cpanel-email-provider.php new file mode 100644 index 00000000..33880646 --- /dev/null +++ b/inc/integrations/email-providers/class-cpanel-email-provider.php @@ -0,0 +1,466 @@ + [ + 'title' => __('cPanel Username', 'ultimate-multisite'), + 'placeholder' => __('e.g. username', 'ultimate-multisite'), + ], + 'WU_CPANEL_PASSWORD' => [ + 'type' => 'password', + 'title' => __('cPanel Password', 'ultimate-multisite'), + 'placeholder' => __('password', 'ultimate-multisite'), + ], + 'WU_CPANEL_HOST' => [ + 'title' => __('cPanel Host', 'ultimate-multisite'), + 'placeholder' => __('e.g. yourdomain.com', 'ultimate-multisite'), + ], + 'WU_CPANEL_PORT' => [ + 'title' => __('cPanel Port', 'ultimate-multisite'), + 'placeholder' => __('Defaults to 2083', 'ultimate-multisite'), + 'value' => 2083, + ], + ]; + } + + /** + * Load the cPanel API. + * + * @since 2.3.0 + * @return CPanel_API + */ + protected function load_api() { + + if (null === $this->api) { + $username = defined('WU_CPANEL_USERNAME') ? WU_CPANEL_USERNAME : ''; + $password = defined('WU_CPANEL_PASSWORD') ? WU_CPANEL_PASSWORD : ''; + $host = defined('WU_CPANEL_HOST') ? WU_CPANEL_HOST : ''; + $port = defined('WU_CPANEL_PORT') && WU_CPANEL_PORT ? WU_CPANEL_PORT : 2083; + + $this->api = new CPanel_API($username, $password, preg_replace('#^https?://#', '', (string) $host), $port); + } + + return $this->api; + } + + /** + * Creates an email account. + * + * @since 2.3.0 + * + * @param array $params The account parameters. + * @return array|\WP_Error + */ + public function create_email_account(array $params) { + + $defaults = [ + 'username' => '', + 'domain' => '', + 'password' => '', + 'quota_mb' => 0, // 0 = unlimited + ]; + + $params = wp_parse_args($params, $defaults); + + if (empty($params['username']) || empty($params['domain']) || empty($params['password'])) { + return new \WP_Error( + 'missing_params', + __('Username, domain, and password are required.', 'ultimate-multisite') + ); + } + + try { + // Use UAPI for newer cPanel versions + $result = $this->load_api()->uapi( + 'Email', + 'add_pop', + [ + 'email' => $params['username'], + 'domain' => $params['domain'], + 'password' => $params['password'], + 'quota' => $params['quota_mb'] > 0 ? $params['quota_mb'] : 0, + ] + ); + + if (isset($result->status) && 1 === $result->status) { + $this->log(sprintf('Email account created: %s@%s', $params['username'], $params['domain'])); + + return [ + 'email_address' => $params['username'] . '@' . $params['domain'], + 'external_id' => $params['username'] . '@' . $params['domain'], + 'quota_mb' => $params['quota_mb'], + ]; + } + + // Check for errors + $error_message = isset($result->errors) && is_array($result->errors) + ? implode(', ', $result->errors) + : __('Failed to create email account.', 'ultimate-multisite'); + + $this->log('Error creating email: ' . $error_message, 'error'); + + return new \WP_Error('cpanel_error', $error_message); + } catch (\Exception $e) { + $this->log('Exception creating email: ' . $e->getMessage(), 'error'); + + return new \WP_Error('cpanel_exception', $e->getMessage()); + } + } + + /** + * Deletes an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address to delete. + * @return bool|\WP_Error + */ + public function delete_email_account($email_address) { + + $parts = explode('@', $email_address); + + if (count($parts) !== 2) { + return new \WP_Error('invalid_email', __('Invalid email address format.', 'ultimate-multisite')); + } + + [$username, $domain] = $parts; + + try { + $result = $this->load_api()->uapi( + 'Email', + 'delete_pop', + [ + 'email' => $username, + 'domain' => $domain, + ] + ); + + if (isset($result->status) && 1 === $result->status) { + $this->log(sprintf('Email account deleted: %s', $email_address)); + + return true; + } + + $error_message = isset($result->errors) && is_array($result->errors) + ? implode(', ', $result->errors) + : __('Failed to delete email account.', 'ultimate-multisite'); + + return new \WP_Error('cpanel_error', $error_message); + } catch (\Exception $e) { + return new \WP_Error('cpanel_exception', $e->getMessage()); + } + } + + /** + * Changes the password for an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address. + * @param string $new_password The new password. + * @return bool|\WP_Error + */ + public function change_password($email_address, $new_password) { + + $parts = explode('@', $email_address); + + if (count($parts) !== 2) { + return new \WP_Error('invalid_email', __('Invalid email address format.', 'ultimate-multisite')); + } + + [$username, $domain] = $parts; + + try { + $result = $this->load_api()->uapi( + 'Email', + 'passwd_pop', + [ + 'email' => $username, + 'domain' => $domain, + 'password' => $new_password, + ] + ); + + if (isset($result->status) && 1 === $result->status) { + $this->log(sprintf('Password changed for: %s', $email_address)); + + return true; + } + + $error_message = isset($result->errors) && is_array($result->errors) + ? implode(', ', $result->errors) + : __('Failed to change password.', 'ultimate-multisite'); + + return new \WP_Error('cpanel_error', $error_message); + } catch (\Exception $e) { + return new \WP_Error('cpanel_exception', $e->getMessage()); + } + } + + /** + * Gets information about an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address. + * @return array|\WP_Error + */ + public function get_account_info($email_address) { + + $parts = explode('@', $email_address); + + if (count($parts) !== 2) { + return new \WP_Error('invalid_email', __('Invalid email address format.', 'ultimate-multisite')); + } + + [$username, $domain] = $parts; + + try { + $result = $this->load_api()->uapi( + 'Email', + 'list_pops_with_disk', + [ + 'domain' => $domain, + ] + ); + + if (isset($result->status) && 1 === $result->status && isset($result->data)) { + foreach ($result->data as $account) { + if (isset($account->email) && $email_address === $account->email) { + return [ + 'email_address' => $account->email, + 'quota_mb' => isset($account->_diskquota) ? (int) $account->_diskquota : 0, + 'disk_used_mb' => isset($account->_diskused) ? (float) $account->_diskused : 0, + 'disk_used_pct' => isset($account->_diskusedpercent) ? (float) $account->_diskusedpercent : 0, + ]; + } + } + + return new \WP_Error('not_found', __('Email account not found.', 'ultimate-multisite')); + } + + return new \WP_Error('cpanel_error', __('Failed to get account info.', 'ultimate-multisite')); + } catch (\Exception $e) { + return new \WP_Error('cpanel_exception', $e->getMessage()); + } + } + + /** + * Gets the webmail URL for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return string + */ + public function get_webmail_url(Email_Account $account) { + + $host = defined('WU_CPANEL_HOST') ? WU_CPANEL_HOST : ''; + $host = preg_replace('#^https?://#', '', $host); + + // cPanel webmail runs on port 2096 + return sprintf('https://%s:2096/', $host); + } + + /** + * Gets the DNS instructions for a domain. + * + * @since 2.3.0 + * + * @param string $domain The domain. + * @return array + */ + public function get_dns_instructions($domain) { + + $host = defined('WU_CPANEL_HOST') ? WU_CPANEL_HOST : 'your-server.com'; + $host = preg_replace('#^https?://#', '', $host); + + return [ + [ + 'type' => 'MX', + 'name' => '@', + 'value' => 'mail.' . $domain, + 'priority' => 10, + 'description' => __('Mail exchanger record for receiving email.', 'ultimate-multisite'), + ], + [ + 'type' => 'A', + 'name' => 'mail', + 'value' => __('[Your Server IP]', 'ultimate-multisite'), + 'description' => __('Points the mail subdomain to your server. Replace with your actual server IP.', 'ultimate-multisite'), + ], + [ + 'type' => 'TXT', + 'name' => '@', + 'value' => 'v=spf1 +a +mx ~all', + 'description' => __('SPF record to help prevent email spoofing.', 'ultimate-multisite'), + ], + ]; + } + + /** + * Gets the IMAP settings for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return array + */ + public function get_imap_settings(Email_Account $account) { + + $host = defined('WU_CPANEL_HOST') ? WU_CPANEL_HOST : ''; + $host = preg_replace('#^https?://#', '', $host); + + return [ + 'server' => 'mail.' . $account->get_domain(), + 'port' => 993, + 'security' => 'SSL/TLS', + 'username' => $account->get_email_address(), + ]; + } + + /** + * Gets the SMTP settings for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return array + */ + public function get_smtp_settings(Email_Account $account) { + + return [ + 'server' => 'mail.' . $account->get_domain(), + 'port' => 587, + 'security' => 'STARTTLS', + 'username' => $account->get_email_address(), + ]; + } + + /** + * Tests the connection with cPanel. + * + * @since 2.3.0 + * @return bool|\WP_Error + */ + public function test_connection() { + + try { + // Try to list email accounts - this will test the connection + $result = $this->load_api()->uapi('Email', 'list_pops', []); + + if (isset($result->status) && 1 === $result->status) { + return true; + } + + return new \WP_Error('connection_failed', __('Failed to connect to cPanel.', 'ultimate-multisite')); + } catch (\Exception $e) { + return new \WP_Error('connection_exception', $e->getMessage()); + } + } +} diff --git a/inc/integrations/email-providers/class-google-workspace-provider.php b/inc/integrations/email-providers/class-google-workspace-provider.php new file mode 100644 index 00000000..0b80955d --- /dev/null +++ b/inc/integrations/email-providers/class-google-workspace-provider.php @@ -0,0 +1,609 @@ + [ + 'title' => __('Service Account JSON Path', 'ultimate-multisite'), + 'placeholder' => __('/path/to/service-account.json', 'ultimate-multisite'), + 'desc' => sprintf( + /* translators: %s is the link to Google Cloud Console */ + __('Path to your service account JSON file. Create one in the %s.', 'ultimate-multisite'), + '' . __('Google Cloud Console', 'ultimate-multisite') . '' + ), + ], + 'WU_GOOGLE_ADMIN_EMAIL' => [ + 'title' => __('Admin Email', 'ultimate-multisite'), + 'placeholder' => __('admin@yourdomain.com', 'ultimate-multisite'), + 'desc' => __('A super admin email address for domain-wide delegation.', 'ultimate-multisite'), + ], + 'WU_GOOGLE_CUSTOMER_ID' => [ + 'title' => __('Customer ID', 'ultimate-multisite'), + 'placeholder' => __('C01234567', 'ultimate-multisite'), + 'desc' => sprintf( + /* translators: %s is the link to Google Admin Console */ + __('Your Google Workspace customer ID. Find it in %s.', 'ultimate-multisite'), + '' . __('Admin Console > Account Settings', 'ultimate-multisite') . '' + ), + ], + ]; + } + + /** + * Gets an access token using service account credentials. + * + * @since 2.3.0 + * @return string|\WP_Error + */ + protected function get_access_token() { + + if (null !== $this->access_token) { + return $this->access_token; + } + + // Check for cached token + $cached = get_transient('wu_google_workspace_token'); + if ($cached) { + $this->access_token = $cached; + return $cached; + } + + $json_path = defined('WU_GOOGLE_SERVICE_ACCOUNT_JSON') ? WU_GOOGLE_SERVICE_ACCOUNT_JSON : ''; + + if (empty($json_path) || ! file_exists($json_path)) { + return new \WP_Error('no_credentials', __('Google service account JSON file not found.', 'ultimate-multisite')); + } + + $credentials = json_decode(file_get_contents($json_path), true); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + + if (! $credentials || ! isset($credentials['private_key']) || ! isset($credentials['client_email'])) { + return new \WP_Error('invalid_credentials', __('Invalid service account JSON format.', 'ultimate-multisite')); + } + + $admin_email = defined('WU_GOOGLE_ADMIN_EMAIL') ? WU_GOOGLE_ADMIN_EMAIL : ''; + + if (empty($admin_email)) { + return new \WP_Error('no_admin_email', __('Google admin email is not configured.', 'ultimate-multisite')); + } + + // Create JWT for authentication + $now = time(); + $header = [ + 'alg' => 'RS256', + 'typ' => 'JWT', + ]; + + $payload = [ + 'iss' => $credentials['client_email'], + 'sub' => $admin_email, // Impersonate admin for domain-wide delegation + 'scope' => 'https://www.googleapis.com/auth/admin.directory.user', + 'aud' => self::TOKEN_URL, + 'iat' => $now, + 'exp' => $now + 3600, + ]; + + $jwt = $this->create_jwt($header, $payload, $credentials['private_key']); + + if (is_wp_error($jwt)) { + return $jwt; + } + + // Exchange JWT for access token + $response = wp_remote_post( + self::TOKEN_URL, + [ + 'body' => [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => $jwt, + ], + ] + ); + + if (is_wp_error($response)) { + return $response; + } + + $body = json_decode(wp_remote_retrieve_body($response), true); + + if (isset($body['error'])) { + $error_msg = isset($body['error_description']) ? $body['error_description'] : $body['error']; + return new \WP_Error('token_error', $error_msg); + } + + if (! isset($body['access_token'])) { + return new \WP_Error('no_token', __('Failed to obtain access token.', 'ultimate-multisite')); + } + + $this->access_token = $body['access_token']; + + // Cache for slightly less than expiry + $expires_in = isset($body['expires_in']) ? (int) $body['expires_in'] - 60 : 3540; + set_transient('wu_google_workspace_token', $this->access_token, $expires_in); + + return $this->access_token; + } + + /** + * Creates a JWT token. + * + * @since 2.3.0 + * + * @param array $header The JWT header. + * @param array $payload The JWT payload. + * @param string $private_key The private key. + * @return string|\WP_Error + */ + protected function create_jwt($header, $payload, $private_key) { + + $header_encoded = $this->base64url_encode(wp_json_encode($header)); + $payload_encoded = $this->base64url_encode(wp_json_encode($payload)); + + $signature_input = $header_encoded . '.' . $payload_encoded; + + $signature = ''; + $result = openssl_sign($signature_input, $signature, $private_key, OPENSSL_ALGO_SHA256); + + if (! $result) { + return new \WP_Error('sign_error', __('Failed to sign JWT.', 'ultimate-multisite')); + } + + return $signature_input . '.' . $this->base64url_encode($signature); + } + + /** + * Base64URL encodes a string. + * + * @since 2.3.0 + * + * @param string $data The data to encode. + * @return string + */ + protected function base64url_encode($data) { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + } + + /** + * Makes an API request to Google Admin SDK. + * + * @since 2.3.0 + * + * @param string $endpoint The API endpoint. + * @param array $data The data to send. + * @param string $method The HTTP method. + * @return object|\WP_Error + */ + protected function api_request($endpoint, $data = [], $method = 'GET') { + + $token = $this->get_access_token(); + + if (is_wp_error($token)) { + return $token; + } + + $url = self::API_BASE_URL . '/' . ltrim($endpoint, '/'); + + $args = [ + 'method' => $method, + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/json', + ], + 'timeout' => 30, + ]; + + if (! empty($data) && in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + $args['body'] = wp_json_encode($data); + } + + $response = wp_remote_request($url, $args); + + if (is_wp_error($response)) { + $this->log('API request error: ' . $response->get_error_message(), 'error'); + return $response; + } + + $body = wp_remote_retrieve_body($response); + $code = wp_remote_retrieve_response_code($response); + + $result = json_decode($body); + + // 204 No Content is success for DELETE + if (204 === $code) { + return (object) ['success' => true]; + } + + if ($code >= 400) { + $error_message = isset($result->error->message) ? $result->error->message : __('API request failed.', 'ultimate-multisite'); + $this->log('API error: ' . $error_message, 'error'); + return new \WP_Error('api_error', $error_message); + } + + return $result; + } + + /** + * Creates an email account. + * + * @since 2.3.0 + * + * @param array $params The account parameters. + * @return array|\WP_Error + */ + public function create_email_account(array $params) { + + $defaults = [ + 'username' => '', + 'domain' => '', + 'password' => '', + 'first_name' => '', + 'last_name' => '', + 'quota_mb' => 0, + ]; + + $params = wp_parse_args($params, $defaults); + + if (empty($params['username']) || empty($params['domain']) || empty($params['password'])) { + return new \WP_Error( + 'missing_params', + __('Username, domain, and password are required.', 'ultimate-multisite') + ); + } + + $email_address = $params['username'] . '@' . $params['domain']; + + // Default names if not provided + $first_name = ! empty($params['first_name']) ? $params['first_name'] : $params['username']; + $last_name = ! empty($params['last_name']) ? $params['last_name'] : 'User'; + + $result = $this->api_request( + 'users', + [ + 'primaryEmail' => $email_address, + 'name' => [ + 'givenName' => $first_name, + 'familyName' => $last_name, + ], + 'password' => $params['password'], + ], + 'POST' + ); + + if (is_wp_error($result)) { + return $result; + } + + $this->log(sprintf('Email account created: %s', $email_address)); + + return [ + 'email_address' => $email_address, + 'external_id' => isset($result->id) ? $result->id : $email_address, + 'quota_mb' => $params['quota_mb'], + ]; + } + + /** + * Deletes an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address to delete. + * @return bool|\WP_Error + */ + public function delete_email_account($email_address) { + + $result = $this->api_request( + 'users/' . urlencode($email_address), + [], + 'DELETE' + ); + + if (is_wp_error($result)) { + return $result; + } + + $this->log(sprintf('Email account deleted: %s', $email_address)); + + return true; + } + + /** + * Changes the password for an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address. + * @param string $new_password The new password. + * @return bool|\WP_Error + */ + public function change_password($email_address, $new_password) { + + $result = $this->api_request( + 'users/' . urlencode($email_address), + [ + 'password' => $new_password, + ], + 'PUT' + ); + + if (is_wp_error($result)) { + return $result; + } + + $this->log(sprintf('Password changed for: %s', $email_address)); + + return true; + } + + /** + * Gets information about an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address. + * @return array|\WP_Error + */ + public function get_account_info($email_address) { + + $result = $this->api_request('users/' . urlencode($email_address)); + + if (is_wp_error($result)) { + return $result; + } + + return [ + 'email_address' => $email_address, + 'first_name' => isset($result->name->givenName) ? $result->name->givenName : '', + 'last_name' => isset($result->name->familyName) ? $result->name->familyName : '', + 'suspended' => isset($result->suspended) ? $result->suspended : false, + 'quota_mb' => 0, // Google Workspace uses license-based storage + ]; + } + + /** + * Gets the webmail URL for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return string + */ + public function get_webmail_url(Email_Account $account) { + + return 'https://mail.google.com/'; + } + + /** + * Gets the DNS instructions for a domain. + * + * @since 2.3.0 + * + * @param string $domain The domain. + * @return array + */ + public function get_dns_instructions($domain) { + + return [ + [ + 'type' => 'MX', + 'name' => '@', + 'value' => 'aspmx.l.google.com', + 'priority' => 1, + 'description' => __('Primary Google mail server.', 'ultimate-multisite'), + ], + [ + 'type' => 'MX', + 'name' => '@', + 'value' => 'alt1.aspmx.l.google.com', + 'priority' => 5, + 'description' => __('Backup Google mail server.', 'ultimate-multisite'), + ], + [ + 'type' => 'MX', + 'name' => '@', + 'value' => 'alt2.aspmx.l.google.com', + 'priority' => 5, + 'description' => __('Backup Google mail server.', 'ultimate-multisite'), + ], + [ + 'type' => 'MX', + 'name' => '@', + 'value' => 'alt3.aspmx.l.google.com', + 'priority' => 10, + 'description' => __('Backup Google mail server.', 'ultimate-multisite'), + ], + [ + 'type' => 'MX', + 'name' => '@', + 'value' => 'alt4.aspmx.l.google.com', + 'priority' => 10, + 'description' => __('Backup Google mail server.', 'ultimate-multisite'), + ], + [ + 'type' => 'TXT', + 'name' => '@', + 'value' => 'v=spf1 include:_spf.google.com ~all', + 'description' => __('SPF record to authorize Google to send email.', 'ultimate-multisite'), + ], + ]; + } + + /** + * Gets the IMAP settings for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return array + */ + public function get_imap_settings(Email_Account $account) { + + return [ + 'server' => 'imap.gmail.com', + 'port' => 993, + 'security' => 'SSL/TLS', + 'username' => $account->get_email_address(), + ]; + } + + /** + * Gets the SMTP settings for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return array + */ + public function get_smtp_settings(Email_Account $account) { + + return [ + 'server' => 'smtp.gmail.com', + 'port' => 587, + 'security' => 'STARTTLS', + 'username' => $account->get_email_address(), + ]; + } + + /** + * Tests the connection with Google Workspace. + * + * @since 2.3.0 + * @return bool|\WP_Error + */ + public function test_connection() { + + $customer_id = defined('WU_GOOGLE_CUSTOMER_ID') ? WU_GOOGLE_CUSTOMER_ID : ''; + + if (empty($customer_id)) { + return new \WP_Error('no_customer_id', __('Google customer ID is not configured.', 'ultimate-multisite')); + } + + // Try to get customer info + $result = $this->api_request('customers/' . urlencode($customer_id)); + + if (is_wp_error($result)) { + return $result; + } + + return true; + } +} diff --git a/inc/integrations/email-providers/class-microsoft365-provider.php b/inc/integrations/email-providers/class-microsoft365-provider.php new file mode 100644 index 00000000..92f20b71 --- /dev/null +++ b/inc/integrations/email-providers/class-microsoft365-provider.php @@ -0,0 +1,594 @@ + [ + 'title' => __('Application (Client) ID', 'ultimate-multisite'), + 'placeholder' => __('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 'ultimate-multisite'), + 'desc' => sprintf( + /* translators: %s is the link to Azure portal */ + __('From your Azure AD app registration in the %s.', 'ultimate-multisite'), + '' . __('Azure Portal', 'ultimate-multisite') . '' + ), + ], + 'WU_MS365_CLIENT_SECRET' => [ + 'type' => 'password', + 'title' => __('Client Secret', 'ultimate-multisite'), + 'placeholder' => __('Your client secret', 'ultimate-multisite'), + 'desc' => __('Create a client secret in your Azure AD app registration.', 'ultimate-multisite'), + ], + 'WU_MS365_TENANT_ID' => [ + 'title' => __('Tenant ID', 'ultimate-multisite'), + 'placeholder' => __('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 'ultimate-multisite'), + 'desc' => __('Your Azure AD tenant ID (Directory ID).', 'ultimate-multisite'), + ], + 'WU_MS365_LICENSE_SKU' => [ + 'title' => __('License SKU ID (Optional)', 'ultimate-multisite'), + 'placeholder' => __('e.g. 18181a46-0d4e-45cd-891e-60aabd171b4e', 'ultimate-multisite'), + 'desc' => __('The SKU ID of the license to assign. Leave empty to skip license assignment.', 'ultimate-multisite'), + ], + ]; + } + + /** + * Gets an access token using client credentials. + * + * @since 2.3.0 + * @return string|\WP_Error + */ + protected function get_access_token() { + + if (null !== $this->access_token) { + return $this->access_token; + } + + // Check for cached token + $cached = get_transient('wu_ms365_token'); + if ($cached) { + $this->access_token = $cached; + return $cached; + } + + $tenant_id = defined('WU_MS365_TENANT_ID') ? WU_MS365_TENANT_ID : ''; + $client_id = defined('WU_MS365_CLIENT_ID') ? WU_MS365_CLIENT_ID : ''; + $client_secret = defined('WU_MS365_CLIENT_SECRET') ? WU_MS365_CLIENT_SECRET : ''; + + if (empty($tenant_id) || empty($client_id) || empty($client_secret)) { + return new \WP_Error('missing_credentials', __('Microsoft 365 credentials are not configured.', 'ultimate-multisite')); + } + + $token_url = "https://login.microsoftonline.com/{$tenant_id}/oauth2/v2.0/token"; + + $response = wp_remote_post( + $token_url, + [ + 'body' => [ + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'scope' => 'https://graph.microsoft.com/.default', + 'grant_type' => 'client_credentials', + ], + ] + ); + + if (is_wp_error($response)) { + $this->log('Token request error: ' . $response->get_error_message(), 'error'); + return $response; + } + + $body = json_decode(wp_remote_retrieve_body($response), true); + + if (isset($body['error'])) { + $error_msg = isset($body['error_description']) ? $body['error_description'] : $body['error']; + $this->log('Token error: ' . $error_msg, 'error'); + return new \WP_Error('token_error', $error_msg); + } + + if (! isset($body['access_token'])) { + return new \WP_Error('no_token', __('Failed to obtain access token.', 'ultimate-multisite')); + } + + $this->access_token = $body['access_token']; + + // Cache for slightly less than expiry + $expires_in = isset($body['expires_in']) ? (int) $body['expires_in'] - 60 : 3540; + set_transient('wu_ms365_token', $this->access_token, $expires_in); + + return $this->access_token; + } + + /** + * Makes an API request to Microsoft Graph. + * + * @since 2.3.0 + * + * @param string $endpoint The API endpoint. + * @param array $data The data to send. + * @param string $method The HTTP method. + * @return object|\WP_Error + */ + protected function api_request($endpoint, $data = [], $method = 'GET') { + + $token = $this->get_access_token(); + + if (is_wp_error($token)) { + return $token; + } + + $url = self::GRAPH_API_URL . '/' . ltrim($endpoint, '/'); + + $args = [ + 'method' => $method, + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/json', + ], + 'timeout' => 30, + ]; + + if (! empty($data) && in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + $args['body'] = wp_json_encode($data); + } + + $response = wp_remote_request($url, $args); + + if (is_wp_error($response)) { + $this->log('API request error: ' . $response->get_error_message(), 'error'); + return $response; + } + + $body = wp_remote_retrieve_body($response); + $code = wp_remote_retrieve_response_code($response); + + // 204 No Content is success for DELETE + if (204 === $code) { + return (object) ['success' => true]; + } + + $result = json_decode($body); + + if ($code >= 400) { + $error_message = isset($result->error->message) ? $result->error->message : __('API request failed.', 'ultimate-multisite'); + $this->log('API error: ' . $error_message, 'error'); + return new \WP_Error('api_error', $error_message); + } + + return $result; + } + + /** + * Creates an email account. + * + * @since 2.3.0 + * + * @param array $params The account parameters. + * @return array|\WP_Error + */ + public function create_email_account(array $params) { + + $defaults = [ + 'username' => '', + 'domain' => '', + 'password' => '', + 'display_name' => '', + 'first_name' => '', + 'last_name' => '', + 'quota_mb' => 0, + ]; + + $params = wp_parse_args($params, $defaults); + + if (empty($params['username']) || empty($params['domain']) || empty($params['password'])) { + return new \WP_Error( + 'missing_params', + __('Username, domain, and password are required.', 'ultimate-multisite') + ); + } + + $email_address = $params['username'] . '@' . $params['domain']; + + // Default display name if not provided + $display_name = ! empty($params['display_name']) ? $params['display_name'] : $params['username']; + $first_name = ! empty($params['first_name']) ? $params['first_name'] : $params['username']; + $last_name = ! empty($params['last_name']) ? $params['last_name'] : 'User'; + + // Microsoft requires mailNickname (alias before @) + $mail_nickname = $params['username']; + + $user_data = [ + 'accountEnabled' => true, + 'displayName' => $display_name, + 'mailNickname' => $mail_nickname, + 'userPrincipalName' => $email_address, + 'givenName' => $first_name, + 'surname' => $last_name, + 'passwordProfile' => [ + 'forceChangePasswordNextSignIn' => false, + 'password' => $params['password'], + ], + 'usageLocation' => 'US', // Required for license assignment + ]; + + $result = $this->api_request('users', $user_data, 'POST'); + + if (is_wp_error($result)) { + return $result; + } + + $user_id = isset($result->id) ? $result->id : ''; + + // Assign license if configured + if (! empty($user_id)) { + $license_result = $this->assign_license($user_id); + if (is_wp_error($license_result)) { + $this->log('License assignment failed: ' . $license_result->get_error_message(), 'warning'); + // Don't fail the account creation, just log the warning + } + } + + $this->log(sprintf('Email account created: %s', $email_address)); + + return [ + 'email_address' => $email_address, + 'external_id' => $user_id, + 'quota_mb' => $params['quota_mb'], + ]; + } + + /** + * Assigns a license to a user. + * + * @since 2.3.0 + * + * @param string $user_id The user ID. + * @return bool|\WP_Error + */ + protected function assign_license($user_id) { + + $license_sku = defined('WU_MS365_LICENSE_SKU') ? WU_MS365_LICENSE_SKU : ''; + + if (empty($license_sku)) { + return true; // No license to assign + } + + $result = $this->api_request( + "users/{$user_id}/assignLicense", + [ + 'addLicenses' => [ + [ + 'skuId' => $license_sku, + ], + ], + 'removeLicenses' => [], + ], + 'POST' + ); + + if (is_wp_error($result)) { + return $result; + } + + $this->log(sprintf('License assigned to user: %s', $user_id)); + + return true; + } + + /** + * Deletes an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address to delete. + * @return bool|\WP_Error + */ + public function delete_email_account($email_address) { + + $result = $this->api_request( + 'users/' . urlencode($email_address), + [], + 'DELETE' + ); + + if (is_wp_error($result)) { + return $result; + } + + $this->log(sprintf('Email account deleted: %s', $email_address)); + + return true; + } + + /** + * Changes the password for an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address. + * @param string $new_password The new password. + * @return bool|\WP_Error + */ + public function change_password($email_address, $new_password) { + + $result = $this->api_request( + 'users/' . urlencode($email_address), + [ + 'passwordProfile' => [ + 'forceChangePasswordNextSignIn' => false, + 'password' => $new_password, + ], + ], + 'PATCH' + ); + + if (is_wp_error($result)) { + return $result; + } + + $this->log(sprintf('Password changed for: %s', $email_address)); + + return true; + } + + /** + * Gets information about an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address. + * @return array|\WP_Error + */ + public function get_account_info($email_address) { + + $result = $this->api_request('users/' . urlencode($email_address)); + + if (is_wp_error($result)) { + return $result; + } + + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Microsoft Graph API response properties. + return [ + 'email_address' => $email_address, + 'display_name' => isset($result->displayName) ? $result->displayName : '', + 'first_name' => isset($result->givenName) ? $result->givenName : '', + 'last_name' => isset($result->surname) ? $result->surname : '', + 'suspended' => isset($result->accountEnabled) ? ! $result->accountEnabled : false, + 'quota_mb' => 0, // Microsoft 365 uses license-based storage + ]; + // phpcs:enable + } + + /** + * Gets the webmail URL for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return string + */ + public function get_webmail_url(Email_Account $account) { + + return 'https://outlook.office365.com/'; + } + + /** + * Gets the DNS instructions for a domain. + * + * @since 2.3.0 + * + * @param string $domain The domain. + * @return array + */ + public function get_dns_instructions($domain) { + + return [ + [ + 'type' => 'MX', + 'name' => '@', + 'value' => $domain . '.mail.protection.outlook.com', + 'priority' => 0, + 'description' => __('Microsoft 365 mail server.', 'ultimate-multisite'), + ], + [ + 'type' => 'TXT', + 'name' => '@', + 'value' => 'v=spf1 include:spf.protection.outlook.com -all', + 'description' => __('SPF record to authorize Microsoft to send email.', 'ultimate-multisite'), + ], + [ + 'type' => 'CNAME', + 'name' => 'autodiscover', + 'value' => 'autodiscover.outlook.com', + 'description' => __('Autodiscover for automatic email client configuration.', 'ultimate-multisite'), + ], + [ + 'type' => 'CNAME', + 'name' => 'selector1._domainkey', + 'value' => 'selector1-' . str_replace('.', '-', $domain) . '._domainkey.YOUR_TENANT.onmicrosoft.com', + 'description' => __('DKIM record for email authentication. Replace YOUR_TENANT with your Microsoft tenant name.', 'ultimate-multisite'), + ], + [ + 'type' => 'CNAME', + 'name' => 'selector2._domainkey', + 'value' => 'selector2-' . str_replace('.', '-', $domain) . '._domainkey.YOUR_TENANT.onmicrosoft.com', + 'description' => __('Secondary DKIM record. Replace YOUR_TENANT with your Microsoft tenant name.', 'ultimate-multisite'), + ], + ]; + } + + /** + * Gets the IMAP settings for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return array + */ + public function get_imap_settings(Email_Account $account) { + + return [ + 'server' => 'outlook.office365.com', + 'port' => 993, + 'security' => 'SSL/TLS', + 'username' => $account->get_email_address(), + ]; + } + + /** + * Gets the SMTP settings for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return array + */ + public function get_smtp_settings(Email_Account $account) { + + return [ + 'server' => 'smtp.office365.com', + 'port' => 587, + 'security' => 'STARTTLS', + 'username' => $account->get_email_address(), + ]; + } + + /** + * Tests the connection with Microsoft 365. + * + * @since 2.3.0 + * @return bool|\WP_Error + */ + public function test_connection() { + + // Try to get organization info + $result = $this->api_request('organization'); + + if (is_wp_error($result)) { + return $result; + } + + return true; + } +} diff --git a/inc/integrations/email-providers/class-purelymail-provider.php b/inc/integrations/email-providers/class-purelymail-provider.php new file mode 100644 index 00000000..49daaf23 --- /dev/null +++ b/inc/integrations/email-providers/class-purelymail-provider.php @@ -0,0 +1,461 @@ + [ + 'type' => 'password', + 'title' => __('Purelymail API Key', 'ultimate-multisite'), + 'placeholder' => __('Your API key from Purelymail settings', 'ultimate-multisite'), + 'desc' => sprintf( + /* translators: %s is the link to get an API key */ + __('Get your API key from your %s.', 'ultimate-multisite'), + '' . __('Purelymail account settings', 'ultimate-multisite') . '' + ), + ], + ]; + } + + /** + * Makes an API request to Purelymail. + * + * @since 2.3.0 + * + * @param string $endpoint The API endpoint. + * @param array $data The data to send. + * @param string $method The HTTP method. + * @return object|\WP_Error + */ + protected function api_request($endpoint, $data = [], $method = 'POST') { + + $api_key = defined('WU_PURELYMAIL_API_KEY') ? WU_PURELYMAIL_API_KEY : ''; + + if (empty($api_key)) { + return new \WP_Error('no_api_key', __('Purelymail API key is not configured.', 'ultimate-multisite')); + } + + $url = self::API_BASE_URL . '/' . ltrim($endpoint, '/'); + + $args = [ + 'method' => $method, + 'headers' => [ + 'Content-Type' => 'application/json', + 'Purelymail-Api-Key' => $api_key, + ], + 'timeout' => 30, + ]; + + if (! empty($data)) { + $args['body'] = wp_json_encode($data); + } + + $response = wp_remote_request($url, $args); + + if (is_wp_error($response)) { + $this->log('API request error: ' . $response->get_error_message(), 'error'); + return $response; + } + + $body = wp_remote_retrieve_body($response); + $code = wp_remote_retrieve_response_code($response); + + $result = json_decode($body); + + if (JSON_ERROR_NONE !== json_last_error()) { + return new \WP_Error('json_error', __('Failed to parse API response.', 'ultimate-multisite')); + } + + // Purelymail returns {success: true/false, message: "...", result: {...}} + if (isset($result->success) && ! $result->success) { + $error_message = isset($result->message) ? $result->message : __('Unknown API error.', 'ultimate-multisite'); + $this->log('API error: ' . $error_message, 'error'); + return new \WP_Error('api_error', $error_message); + } + + if ($code >= 400) { + $error_message = isset($result->message) ? $result->message : __('API request failed.', 'ultimate-multisite'); + return new \WP_Error('api_http_error', $error_message); + } + + return $result; + } + + /** + * Creates an email account. + * + * @since 2.3.0 + * + * @param array $params The account parameters. + * @return array|\WP_Error + */ + public function create_email_account(array $params) { + + $defaults = [ + 'username' => '', + 'domain' => '', + 'password' => '', + 'quota_mb' => 0, // Purelymail doesn't use per-account quotas in the same way + ]; + + $params = wp_parse_args($params, $defaults); + + if (empty($params['username']) || empty($params['domain']) || empty($params['password'])) { + return new \WP_Error( + 'missing_params', + __('Username, domain, and password are required.', 'ultimate-multisite') + ); + } + + $email_address = $params['username'] . '@' . $params['domain']; + + // First, ensure the domain is added to Purelymail + $domain_result = $this->ensure_domain_exists($params['domain']); + if (is_wp_error($domain_result)) { + // Domain might already exist, which is fine + $this->log('Domain check result: ' . $domain_result->get_error_message()); + } + + // Create the user + $result = $this->api_request( + 'createUser', + [ + 'userName' => $email_address, + 'password' => $params['password'], + 'enablePasswordReset' => false, + ] + ); + + if (is_wp_error($result)) { + return $result; + } + + $this->log(sprintf('Email account created: %s', $email_address)); + + return [ + 'email_address' => $email_address, + 'external_id' => $email_address, // Purelymail uses email as ID + 'quota_mb' => 0, // Purelymail uses account-level storage + ]; + } + + /** + * Ensures a domain exists in Purelymail. + * + * @since 2.3.0 + * + * @param string $domain The domain name. + * @return true|\WP_Error + */ + protected function ensure_domain_exists($domain) { + + $result = $this->api_request( + 'addDomainName', + [ + 'domainName' => $domain, + ] + ); + + if (is_wp_error($result)) { + // Check if error is because domain already exists + if (false !== strpos($result->get_error_message(), 'already')) { + return true; + } + return $result; + } + + return true; + } + + /** + * Deletes an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address to delete. + * @return bool|\WP_Error + */ + public function delete_email_account($email_address) { + + $result = $this->api_request( + 'deleteUser', + [ + 'userName' => $email_address, + ] + ); + + if (is_wp_error($result)) { + return $result; + } + + $this->log(sprintf('Email account deleted: %s', $email_address)); + + return true; + } + + /** + * Changes the password for an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address. + * @param string $new_password The new password. + * @return bool|\WP_Error + */ + public function change_password($email_address, $new_password) { + + $result = $this->api_request( + 'modifyUser', + [ + 'userName' => $email_address, + 'password' => $new_password, + ] + ); + + if (is_wp_error($result)) { + return $result; + } + + $this->log(sprintf('Password changed for: %s', $email_address)); + + return true; + } + + /** + * Gets information about an email account. + * + * @since 2.3.0 + * + * @param string $email_address The email address. + * @return array|\WP_Error + */ + public function get_account_info($email_address) { + + $result = $this->api_request( + 'getUser', + [ + 'userName' => $email_address, + ] + ); + + if (is_wp_error($result)) { + return $result; + } + + // Handle nested result structure + $user_data = isset($result->result) ? $result->result : $result; + + return [ + 'email_address' => $email_address, + 'quota_mb' => 0, // Purelymail uses account-level storage + 'disk_used_mb' => 0, + ]; + } + + /** + * Gets the webmail URL for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return string + */ + public function get_webmail_url(Email_Account $account) { + + return 'https://app.purelymail.com/'; + } + + /** + * Gets the DNS instructions for a domain. + * + * @since 2.3.0 + * + * @param string $domain The domain. + * @return array + */ + public function get_dns_instructions($domain) { + + return [ + [ + 'type' => 'MX', + 'name' => '@', + 'value' => 'mailserver.purelymail.com', + 'priority' => 10, + 'description' => __('Mail exchanger record for receiving email.', 'ultimate-multisite'), + ], + [ + 'type' => 'TXT', + 'name' => '@', + 'value' => 'v=spf1 include:_spf.purelymail.com ~all', + 'description' => __('SPF record to authorize Purelymail to send email on your behalf.', 'ultimate-multisite'), + ], + [ + 'type' => 'CNAME', + 'name' => 'purelymail._domainkey', + 'value' => 'key1._domainkey.purelymail.com', + 'description' => __('DKIM record for email authentication.', 'ultimate-multisite'), + ], + [ + 'type' => 'TXT', + 'name' => '_dmarc', + 'value' => 'v=DMARC1; p=quarantine; rua=mailto:dmarc@' . $domain, + 'description' => __('DMARC policy for handling unauthenticated email.', 'ultimate-multisite'), + ], + ]; + } + + /** + * Gets the IMAP settings for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return array + */ + public function get_imap_settings(Email_Account $account) { + + return [ + 'server' => 'imap.purelymail.com', + 'port' => 993, + 'security' => 'SSL/TLS', + 'username' => $account->get_email_address(), + ]; + } + + /** + * Gets the SMTP settings for an email account. + * + * @since 2.3.0 + * + * @param Email_Account $account The email account. + * @return array + */ + public function get_smtp_settings(Email_Account $account) { + + return [ + 'server' => 'smtp.purelymail.com', + 'port' => 587, + 'security' => 'STARTTLS', + 'username' => $account->get_email_address(), + ]; + } + + /** + * Tests the connection with Purelymail. + * + * @since 2.3.0 + * @return bool|\WP_Error + */ + public function test_connection() { + + // Try to list domains - this will test the connection + $result = $this->api_request('listDomainNames', []); + + if (is_wp_error($result)) { + return $result; + } + + return true; + } +} diff --git a/inc/limitations/class-limit-email-accounts.php b/inc/limitations/class-limit-email-accounts.php new file mode 100644 index 00000000..24f5cdd0 --- /dev/null +++ b/inc/limitations/class-limit-email-accounts.php @@ -0,0 +1,166 @@ +is_enabled()) { + return false; + } + + // For simple boolean limits (enabled/disabled) + if (is_bool($limit)) { + return $limit; + } + + // For numeric limits + if (is_numeric($limit)) { + // 0 means unlimited + if (0 === (int) $limit) { + return true; + } + + return (int) $value_to_check < (int) $limit; + } + + // Default to enabled + return true; + } + + /** + * Check if more email accounts can be created. + * + * @since 2.3.0 + * + * @param int $customer_id The customer ID. + * @param int $membership_id The membership ID. + * @return bool + */ + public function can_create_more($customer_id, $membership_id) { + + if ( ! $this->is_enabled()) { + return false; + } + + $limit = $this->get_limit(); + + // Boolean true means unlimited + if (true === $limit) { + return true; + } + + // Boolean false means none allowed + if (false === $limit) { + return false; + } + + // 0 means unlimited + if (0 === (int) $limit) { + return true; + } + + $current_count = $this->get_current_account_count($customer_id, $membership_id); + + return $current_count < (int) $limit; + } + + /** + * Get the current count of email accounts. + * + * @since 2.3.0 + * + * @param int $customer_id The customer ID. + * @param int|null $membership_id Optional membership ID. + * @return int + */ + public function get_current_account_count($customer_id, $membership_id = null) { + + return wu_count_email_accounts($customer_id, $membership_id); + } + + /** + * Get remaining email account slots. + * + * @since 2.3.0 + * + * @param int $customer_id The customer ID. + * @param int $membership_id The membership ID. + * @return int|string Returns 'unlimited' if no limit, or the number of remaining slots. + */ + public function get_remaining_slots($customer_id, $membership_id) { + + if ( ! $this->is_enabled()) { + return 0; + } + + $limit = $this->get_limit(); + + // Boolean false means none allowed + if (false === $limit) { + return 0; + } + + // Boolean true or numeric 0 means unlimited + if (true === $limit || 0 === (int) $limit) { + return 'unlimited'; + } + + $current_count = $this->get_current_account_count($customer_id, $membership_id); + + return max(0, (int) $limit - $current_count); + } + + /** + * Returns a default state. + * + * @since 2.3.0 + * @return array + */ + public static function default_state() { + + return [ + 'enabled' => false, + 'limit' => 0, // 0 = unlimited when enabled + ]; + } +} diff --git a/inc/loaders/class-table-loader.php b/inc/loaders/class-table-loader.php index f31a3cd4..ccb099f7 100644 --- a/inc/loaders/class-table-loader.php +++ b/inc/loaders/class-table-loader.php @@ -177,6 +177,14 @@ class Table_Loader { */ public $checkout_formmeta_table; + /** + * The Email Accounts Table + * + * @since 2.3.0 + * @var \WP_Ultimo\Database\Email_Accounts\Email_Accounts_Table + */ + public $email_account_table; + /** * Loads the table objects for our custom tables. * @@ -246,6 +254,11 @@ public function init(): void { */ $this->checkout_form_table = new \WP_Ultimo\Database\Checkout_Forms\Checkout_Forms_Table(); $this->checkout_formmeta_table = new \WP_Ultimo\Database\Checkout_Forms\Checkout_Forms_Meta_Table(); + + /** + * Loads the Email Accounts Table + */ + $this->email_account_table = new \WP_Ultimo\Database\Email_Accounts\Email_Accounts_Table(); } /** diff --git a/inc/managers/class-email-account-manager.php b/inc/managers/class-email-account-manager.php new file mode 100644 index 00000000..9d209053 --- /dev/null +++ b/inc/managers/class-email-account-manager.php @@ -0,0 +1,706 @@ +enable_rest_api(); + + $this->enable_wp_cli(); + + // Load providers + add_action('plugins_loaded', [$this, 'load_providers'], 20); + + // Settings + add_action('wu_settings_email-accounts', [$this, 'add_email_account_settings']); + + // Async provisioning + add_action('wu_async_provision_email_account', [$this, 'async_provision_email_account']); + add_action('wu_async_delete_email_account', [$this, 'async_delete_email_account']); + + // Status transitions + add_action('wu_transition_email_account_status', [$this, 'handle_status_transition'], 10, 3); + + // Clean up when membership is deleted + add_action('wu_membership_post_delete', [$this, 'handle_membership_deleted'], 10, 2); + + // Clean up when customer is deleted + add_action('wu_customer_post_delete', [$this, 'handle_customer_deleted'], 10, 2); + + // AJAX handler for testing email integration + add_action('wp_ajax_wu_test_email_integration', [$this, 'ajax_test_email_integration']); + } + + /** + * Load email providers. + * + * @since 2.3.0 + * @return void + */ + public function load_providers(): void { + + // Only load if email accounts are enabled + if ( ! wu_get_setting('enable_email_accounts', false)) { + return; + } + + // Load built-in providers + \WP_Ultimo\Integrations\Email_Providers\CPanel_Email_Provider::get_instance(); + \WP_Ultimo\Integrations\Email_Providers\Purelymail_Provider::get_instance(); + \WP_Ultimo\Integrations\Email_Providers\Google_Workspace_Provider::get_instance(); + \WP_Ultimo\Integrations\Email_Providers\Microsoft365_Provider::get_instance(); + + // Allow additional providers to be loaded + do_action('wu_email_providers_load'); + } + + /** + * Returns the list of registered email providers. + * + * @since 2.3.0 + * @return array + */ + public function get_providers() { + + return apply_filters('wu_email_manager_get_providers', $this->providers, $this); + } + + /** + * Returns the list of enabled email providers. + * + * @since 2.3.0 + * @return array Array of provider instances. + */ + public function get_enabled_providers() { + + $providers = $this->get_providers(); + $enabled = []; + + foreach ($providers as $id => $class_name) { + $instance = $class_name::get_instance(); + + if ($instance->is_enabled() && $instance->is_setup()) { + $enabled[ $id ] = $instance; + } + } + + return $enabled; + } + + /** + * Get a specific provider instance. + * + * @since 2.3.0 + * + * @param string $id The provider ID. + * @return \WP_Ultimo\Integrations\Email_Providers\Base_Email_Provider|null + */ + public function get_provider($id) { + + $providers = $this->get_providers(); + + if (isset($providers[ $id ])) { + $class_name = $providers[ $id ]; + + return $class_name::get_instance(); + } + + return null; + } + + /** + * Alias for get_provider for consistency with hosting integration wizard. + * + * @since 2.3.0 + * + * @param string $id The provider ID. + * @return \WP_Ultimo\Integrations\Email_Providers\Base_Email_Provider|null + */ + public function get_provider_instance($id) { + + return $this->get_provider($id); + } + + /** + * Add email account settings. + * + * @since 2.3.0 + * @return void + */ + public function add_email_account_settings(): void { + + wu_register_settings_field( + 'email-accounts', + 'email_accounts_header', + [ + 'title' => __('Email Accounts Settings', 'ultimate-multisite'), + 'desc' => __('Configure email account management for your network.', 'ultimate-multisite'), + 'type' => 'header', + ] + ); + + wu_register_settings_field( + 'email-accounts', + 'enable_email_accounts', + [ + 'title' => __('Enable Email Accounts', 'ultimate-multisite'), + 'desc' => __('Allow customers to create and manage email accounts.', 'ultimate-multisite'), + 'type' => 'toggle', + 'default' => false, + ] + ); + + wu_register_settings_field( + 'email-accounts', + 'email_default_quota_mb', + [ + 'title' => __('Default Mailbox Quota (MB)', 'ultimate-multisite'), + 'desc' => __('Default storage quota for new email accounts. Set to 0 for unlimited.', 'ultimate-multisite'), + 'type' => 'number', + 'default' => 1024, + 'min' => 0, + 'require' => [ + 'enable_email_accounts' => true, + ], + ] + ); + + wu_register_settings_field( + 'email-accounts', + 'email_per_account_header', + [ + 'title' => __('Per-Account Purchases', 'ultimate-multisite'), + 'desc' => __('Allow customers to purchase additional email accounts beyond their membership quota.', 'ultimate-multisite'), + 'type' => 'header', + 'require' => [ + 'enable_email_accounts' => true, + ], + ] + ); + + wu_register_settings_field( + 'email-accounts', + 'enable_email_per_account_purchase', + [ + 'title' => __('Enable Per-Account Purchases', 'ultimate-multisite'), + 'desc' => __('Allow customers to buy additional email accounts separately.', 'ultimate-multisite'), + 'type' => 'toggle', + 'default' => false, + 'require' => [ + 'enable_email_accounts' => true, + ], + ] + ); + + wu_register_settings_field( + 'email-accounts', + 'email_account_price', + [ + 'title' => __('Price Per Email Account', 'ultimate-multisite'), + 'desc' => __('One-time or monthly price for purchasing an additional email account.', 'ultimate-multisite'), + 'type' => 'number', + 'default' => 5.00, + 'min' => 0, + 'step' => 0.01, + 'require' => [ + 'enable_email_accounts' => true, + 'enable_email_per_account_purchase' => true, + ], + ] + ); + + wu_register_settings_field( + 'email-accounts', + 'email_providers_header', + [ + 'title' => __('Email Providers', 'ultimate-multisite'), + 'desc' => __('Configure which email providers are available.', 'ultimate-multisite'), + 'type' => 'header', + 'require' => [ + 'enable_email_accounts' => true, + ], + ] + ); + + // Provider-specific settings are added by each provider via add_to_integration_list() + } + + /** + * Async provision an email account. + * + * @since 2.3.0 + * + * @param int $email_account_id The email account ID. + * @return void + */ + public function async_provision_email_account($email_account_id): void { + + $email_account = wu_get_email_account($email_account_id); + + if ( ! $email_account) { + wu_log_add('email-accounts', sprintf('Provisioning failed: Account %d not found.', $email_account_id), LogLevel::ERROR); + return; + } + + // Get the provider + $provider = $email_account->get_provider_instance(); + + if ( ! $provider) { + $email_account->set_status('failed'); + $email_account->save(); + wu_log_add('email-accounts', sprintf('Provisioning failed: Provider %s not found.', $email_account->get_provider()), LogLevel::ERROR); + return; + } + + // Update status to provisioning + $email_account->set_status('provisioning'); + $email_account->save(); + + // Get the password from transient if available + $password_token = get_transient('wu_email_provision_pwd_' . $email_account_id); + $password = ''; + + if ($password_token) { + $password = wu_get_email_password_from_token($password_token, $email_account_id); + delete_transient('wu_email_provision_pwd_' . $email_account_id); + } + + // If no password in transient, generate a new one + if (empty($password)) { + $password = wu_generate_email_password(); + } + + // Attempt to create the account + $result = $provider->create_email_account( + [ + 'username' => $email_account->get_username(), + 'domain' => $email_account->get_domain(), + 'password' => $password, + 'quota_mb' => $email_account->get_quota_mb(), + ] + ); + + if (is_wp_error($result)) { + $email_account->set_status('failed'); + $email_account->save(); + + wu_log_add( + 'email-accounts', + sprintf('Provisioning failed for %s: %s', $email_account->get_email_address(), $result->get_error_message()), + LogLevel::ERROR + ); + + /** + * Fires when email account provisioning fails. + * + * @since 2.3.0 + * + * @param Email_Account $email_account The email account. + * @param \WP_Error $result The error. + */ + do_action('wu_email_account_provisioning_failed', $email_account, $result); + + return; + } + + // Update the account with external ID if provided + if (isset($result['external_id'])) { + $email_account->set_external_id($result['external_id']); + } + + // Store encrypted password temporarily for one-time display + $password_display_token = wu_store_email_password_token($email_account_id, $password); + $email_account->update_meta('password_display_token', $password_display_token); + + $email_account->set_status('active'); + $email_account->save(); + + wu_log_add( + 'email-accounts', + sprintf('Email account provisioned successfully: %s', $email_account->get_email_address()) + ); + + /** + * Fires when email account is successfully provisioned. + * + * @since 2.3.0 + * + * @param Email_Account $email_account The email account. + * @param string $password The password (for sending welcome email). + */ + do_action('wu_email_account_provisioned', $email_account, $password); + } + + /** + * Async delete an email account from provider. + * + * @since 2.3.0 + * + * @param array $data Data containing email_address and provider. + * @return void + */ + public function async_delete_email_account($data): void { + + if (empty($data['email_address']) || empty($data['provider'])) { + return; + } + + $provider = $this->get_provider($data['provider']); + + if ( ! $provider) { + wu_log_add('email-accounts', sprintf('Delete failed: Provider %s not found.', $data['provider']), LogLevel::ERROR); + return; + } + + $result = $provider->delete_email_account($data['email_address']); + + if (is_wp_error($result)) { + wu_log_add( + 'email-accounts', + sprintf('Failed to delete %s from provider: %s', $data['email_address'], $result->get_error_message()), + LogLevel::ERROR + ); + return; + } + + wu_log_add('email-accounts', sprintf('Email account deleted from provider: %s', $data['email_address'])); + } + + /** + * Handle status transitions. + * + * @since 2.3.0 + * + * @param string $old_status The old status. + * @param string $new_status The new status. + * @param Email_Account $email_account The email account. + * @return void + */ + public function handle_status_transition($old_status, $new_status, $email_account): void { + + if ('suspended' === $new_status && 'active' === $old_status) { + /** + * Fires when an email account is suspended. + * + * @since 2.3.0 + * + * @param Email_Account $email_account The email account. + */ + do_action('wu_email_account_suspended', $email_account); + } + + if ('active' === $new_status && 'suspended' === $old_status) { + /** + * Fires when an email account is reactivated. + * + * @since 2.3.0 + * + * @param Email_Account $email_account The email account. + */ + do_action('wu_email_account_reactivated', $email_account); + } + } + + /** + * Handle membership deletion - delete associated email accounts. + * + * @since 2.3.0 + * + * @param bool $result Whether the delete was successful. + * @param \WP_Ultimo\Models\Membership $membership The deleted membership. + * @return void + */ + public function handle_membership_deleted($result, $membership): void { + + if ( ! $result || ! $membership instanceof \WP_Ultimo\Models\Membership) { + return; + } + + $email_accounts = wu_get_email_accounts( + [ + 'membership_id' => $membership->get_id(), + ] + ); + + foreach ($email_accounts as $email_account) { + // Queue deletion from provider + wu_enqueue_async_action( + 'wu_async_delete_email_account', + [ + 'email_address' => $email_account->get_email_address(), + 'provider' => $email_account->get_provider(), + ], + 'email_account' + ); + + // Delete from database + $email_account->delete(); + } + } + + /** + * Handle customer deletion - delete associated email accounts. + * + * @since 2.3.0 + * + * @param bool $result Whether the delete was successful. + * @param \WP_Ultimo\Models\Customer $customer The deleted customer. + * @return void + */ + public function handle_customer_deleted($result, $customer): void { + + if ( ! $result || ! $customer instanceof \WP_Ultimo\Models\Customer) { + return; + } + + $email_accounts = wu_get_email_accounts( + [ + 'customer_id' => $customer->get_id(), + ] + ); + + foreach ($email_accounts as $email_account) { + // Queue deletion from provider + wu_enqueue_async_action( + 'wu_async_delete_email_account', + [ + 'email_address' => $email_account->get_email_address(), + 'provider' => $email_account->get_provider(), + ], + 'email_account' + ); + + // Delete from database + $email_account->delete(); + } + } + + /** + * AJAX handler for testing email provider integration. + * + * @since 2.3.0 + * @return void + */ + public function ajax_test_email_integration(): void { + + check_ajax_referer('wu_test_email_integration'); + + if ( ! current_user_can('manage_network')) { + wp_send_json_error( + [ + 'message' => __('You do not have permission to perform this action.', 'ultimate-multisite'), + ] + ); + } + + $integration_id = isset($_POST['integration_id']) ? sanitize_text_field(wp_unslash($_POST['integration_id'])) : ''; + + if (empty($integration_id)) { + wp_send_json_error( + [ + 'message' => __('No integration ID provided.', 'ultimate-multisite'), + ] + ); + } + + $provider = $this->get_provider($integration_id); + + if ( ! $provider) { + wp_send_json_error( + [ + 'message' => __('Invalid integration ID.', 'ultimate-multisite'), + ] + ); + } + + if ( ! $provider->is_setup()) { + wp_send_json_error( + [ + 'message' => sprintf( + /* translators: %s is the list of missing constants */ + __('Missing required configuration: %s', 'ultimate-multisite'), + implode(', ', $provider->get_missing_constants()) + ), + ] + ); + } + + $result = $provider->test_connection(); + + if (is_wp_error($result)) { + wp_send_json_error( + [ + 'message' => $result->get_error_message(), + ] + ); + } + + wp_send_json_success( + [ + 'message' => __('Connection test passed successfully.', 'ultimate-multisite'), + ] + ); + } + + /** + * Create an email account with validation. + * + * @since 2.3.0 + * + * @param array $params Account parameters. + * @return Email_Account|\WP_Error + */ + public function create_account($params) { + + $defaults = [ + 'customer_id' => 0, + 'membership_id' => null, + 'site_id' => null, + 'email_address' => '', + 'provider' => '', + 'password' => '', + 'quota_mb' => wu_get_setting('email_default_quota_mb', 1024), + 'purchase_type' => 'membership_included', + ]; + + $params = wp_parse_args($params, $defaults); + + // Validate customer + $customer = wu_get_customer($params['customer_id']); + + if ( ! $customer) { + return new \WP_Error('invalid_customer', __('Invalid customer.', 'ultimate-multisite')); + } + + // Validate provider + $provider = $this->get_provider($params['provider']); + + if ( ! $provider || ! $provider->is_enabled() || ! $provider->is_setup()) { + return new \WP_Error('invalid_provider', __('Invalid or unconfigured email provider.', 'ultimate-multisite')); + } + + // Check quota if membership included + if ('membership_included' === $params['purchase_type'] && $params['membership_id']) { + if ( ! wu_can_create_email_account($params['customer_id'], $params['membership_id'])) { + return new \WP_Error('quota_exceeded', __('Email account quota exceeded.', 'ultimate-multisite')); + } + } + + // Validate email address + if (empty($params['email_address']) || ! is_email($params['email_address'])) { + return new \WP_Error('invalid_email', __('Invalid email address.', 'ultimate-multisite')); + } + + // Check if email already exists + $existing = wu_get_email_account_by_email($params['email_address']); + + if ($existing) { + return new \WP_Error('email_exists', __('This email address already exists.', 'ultimate-multisite')); + } + + // Generate password if not provided + $password = ! empty($params['password']) ? $params['password'] : wu_generate_email_password(); + + // Create the account record + $email_account = wu_create_email_account( + [ + 'customer_id' => $params['customer_id'], + 'membership_id' => $params['membership_id'], + 'site_id' => $params['site_id'], + 'email_address' => $params['email_address'], + 'provider' => $params['provider'], + 'quota_mb' => $params['quota_mb'], + 'purchase_type' => $params['purchase_type'], + 'payment_id' => $params['payment_id'] ?? null, + 'status' => 'pending', + ] + ); + + if (is_wp_error($email_account)) { + return $email_account; + } + + // Store password token for provisioning + $token = wu_store_email_password_token($email_account->get_id(), $password); + set_transient('wu_email_provision_pwd_' . $email_account->get_id(), $token, 3600); + + return $email_account; + } + + /** + * Get DNS instructions for a provider and domain. + * + * @since 2.3.0 + * + * @param string $provider_id The provider ID. + * @param string $domain The domain. + * @return array|\WP_Error + */ + public function get_dns_instructions($provider_id, $domain) { + + $provider = $this->get_provider($provider_id); + + if ( ! $provider) { + return new \WP_Error('invalid_provider', __('Invalid provider.', 'ultimate-multisite')); + } + + return $provider->get_dns_instructions($domain); + } +} diff --git a/inc/models/class-email-account.php b/inc/models/class-email-account.php new file mode 100644 index 00000000..f4637e86 --- /dev/null +++ b/inc/models/class-email-account.php @@ -0,0 +1,727 @@ +get_id(); + + return [ + 'customer_id' => 'required|integer', + 'email_address' => "required|email|unique:\WP_Ultimo\Models\Email_Account,email_address,{$id}", + 'domain' => 'required', + 'provider' => 'required', + 'status' => 'required|in:pending,provisioning,active,suspended,failed|default:pending', + 'purchase_type' => 'in:membership_included,per_account|default:membership_included', + 'quota_mb' => 'integer|default:0', + ]; + } + + /** + * Get the customer ID. + * + * @since 2.3.0 + * @return int + */ + public function get_customer_id(): int { + + return (int) $this->customer_id; + } + + /** + * Set the customer ID. + * + * @since 2.3.0 + * @param int $customer_id The customer ID. + * @return void + */ + public function set_customer_id($customer_id): void { + + $this->customer_id = (int) $customer_id; + } + + /** + * Get the customer object. + * + * @since 2.3.0 + * @return Customer|null + */ + public function get_customer() { + + return wu_get_customer($this->get_customer_id()); + } + + /** + * Get the membership ID. + * + * @since 2.3.0 + * @return int|null + */ + public function get_membership_id() { + + return $this->membership_id ? (int) $this->membership_id : null; + } + + /** + * Set the membership ID. + * + * @since 2.3.0 + * @param int|null $membership_id The membership ID. + * @return void + */ + public function set_membership_id($membership_id): void { + + $this->membership_id = $membership_id ? (int) $membership_id : null; + } + + /** + * Get the membership object. + * + * @since 2.3.0 + * @return Membership|null + */ + public function get_membership() { + + $membership_id = $this->get_membership_id(); + + return $membership_id ? wu_get_membership($membership_id) : null; + } + + /** + * Get the site ID. + * + * @since 2.3.0 + * @return int|null + */ + public function get_site_id() { + + return $this->site_id ? (int) $this->site_id : null; + } + + /** + * Set the site ID. + * + * @since 2.3.0 + * @param int|null $site_id The site ID. + * @return void + */ + public function set_site_id($site_id): void { + + $this->site_id = $site_id ? (int) $site_id : null; + } + + /** + * Get the site object. + * + * @since 2.3.0 + * @return Site|null + */ + public function get_site() { + + $site_id = $this->get_site_id(); + + return $site_id ? wu_get_site($site_id) : null; + } + + /** + * Get the full email address. + * + * @since 2.3.0 + * @return string + */ + public function get_email_address(): string { + + return $this->email_address; + } + + /** + * Set the email address. + * + * @since 2.3.0 + * @param string $email_address The email address. + * @return void + */ + public function set_email_address($email_address): void { + + $this->email_address = strtolower(sanitize_email($email_address)); + + // Auto-extract domain from email address + $parts = explode('@', $this->email_address); + if (count($parts) === 2) { + $this->domain = $parts[1]; + } + } + + /** + * Get the username portion of the email. + * + * @since 2.3.0 + * @return string + */ + public function get_username(): string { + + $parts = explode('@', $this->get_email_address()); + + return $parts[0] ?? ''; + } + + /** + * Get the domain. + * + * @since 2.3.0 + * @return string + */ + public function get_domain(): string { + + return $this->domain; + } + + /** + * Set the domain. + * + * @since 2.3.0 + * @param string $domain The domain. + * @return void + */ + public function set_domain($domain): void { + + $this->domain = strtolower($domain); + } + + /** + * Get the provider identifier. + * + * @since 2.3.0 + * @return string + */ + public function get_provider(): string { + + return $this->provider; + } + + /** + * Set the provider. + * + * @since 2.3.0 + * @param string $provider The provider identifier. + * @return void + */ + public function set_provider($provider): void { + + $this->provider = $provider; + } + + /** + * Get the provider instance. + * + * @since 2.3.0 + * @return \WP_Ultimo\Integrations\Email_Providers\Base_Email_Provider|null + */ + public function get_provider_instance() { + + $manager = \WP_Ultimo\Managers\Email_Account_Manager::get_instance(); + + return $manager->get_provider($this->get_provider()); + } + + /** + * Get the status. + * + * @since 2.3.0 + * @return string + */ + public function get_status(): string { + + return $this->status; + } + + /** + * Set the status. + * + * @since 2.3.0 + * @param string $status The status. + * @return void + */ + public function set_status($status): void { + + $this->status = $status; + } + + /** + * Check if the email account is active. + * + * @since 2.3.0 + * @return bool + */ + public function is_active(): bool { + + return $this->get_status() === 'active'; + } + + /** + * Returns the status label. + * + * @since 2.3.0 + * @return string + */ + public function get_status_label(): string { + + $status = new Email_Account_Status($this->get_status()); + + return $status->get_label(); + } + + /** + * Gets the CSS classes for the status. + * + * @since 2.3.0 + * @return string + */ + public function get_status_class(): string { + + $status = new Email_Account_Status($this->get_status()); + + return $status->get_classes(); + } + + /** + * Get the quota in megabytes. + * + * @since 2.3.0 + * @return int + */ + public function get_quota_mb(): int { + + return (int) $this->quota_mb; + } + + /** + * Set the quota in megabytes. + * + * @since 2.3.0 + * @param int $quota_mb The quota in MB. + * @return void + */ + public function set_quota_mb($quota_mb): void { + + $this->quota_mb = (int) $quota_mb; + } + + /** + * Get the external ID from the provider. + * + * @since 2.3.0 + * @return string|null + */ + public function get_external_id() { + + return $this->external_id; + } + + /** + * Set the external ID. + * + * @since 2.3.0 + * @param string|null $external_id The external ID. + * @return void + */ + public function set_external_id($external_id): void { + + $this->external_id = $external_id; + } + + /** + * Get the encrypted password hash. + * + * @since 2.3.0 + * @return string|null + */ + public function get_password_hash() { + + return $this->password_hash; + } + + /** + * Set the encrypted password hash. + * + * @since 2.3.0 + * @param string|null $password_hash The encrypted password hash. + * @return void + */ + public function set_password_hash($password_hash): void { + + $this->password_hash = $password_hash; + } + + /** + * Get the purchase type. + * + * @since 2.3.0 + * @return string + */ + public function get_purchase_type(): string { + + return $this->purchase_type; + } + + /** + * Set the purchase type. + * + * @since 2.3.0 + * @param string $purchase_type The purchase type. + * @return void + */ + public function set_purchase_type($purchase_type): void { + + $this->purchase_type = $purchase_type; + } + + /** + * Check if this is a per-account purchase. + * + * @since 2.3.0 + * @return bool + */ + public function is_per_account_purchase(): bool { + + return $this->get_purchase_type() === 'per_account'; + } + + /** + * Get the payment ID. + * + * @since 2.3.0 + * @return int|null + */ + public function get_payment_id() { + + return $this->payment_id ? (int) $this->payment_id : null; + } + + /** + * Set the payment ID. + * + * @since 2.3.0 + * @param int|null $payment_id The payment ID. + * @return void + */ + public function set_payment_id($payment_id): void { + + $this->payment_id = $payment_id ? (int) $payment_id : null; + } + + /** + * Get the payment object. + * + * @since 2.3.0 + * @return Payment|null + */ + public function get_payment() { + + $payment_id = $this->get_payment_id(); + + return $payment_id ? wu_get_payment($payment_id) : null; + } + + /** + * Get date when this was created. + * + * @since 2.3.0 + * @return string + */ + public function get_date_created() { + + return $this->date_created; + } + + /** + * Set date when this was created. + * + * @since 2.3.0 + * @param string $date_created Date when the email account was created. + * @return void + */ + public function set_date_created($date_created): void { + + $this->date_created = $date_created; + } + + /** + * Get the webmail URL for this email account. + * + * @since 2.3.0 + * @return string + */ + public function get_webmail_url(): string { + + $provider = $this->get_provider_instance(); + + if ($provider) { + return $provider->get_webmail_url($this); + } + + return ''; + } + + /** + * Get the IMAP settings for this email account. + * + * @since 2.3.0 + * @return array + */ + public function get_imap_settings(): array { + + $provider = $this->get_provider_instance(); + + if ($provider) { + return $provider->get_imap_settings($this); + } + + return []; + } + + /** + * Get the SMTP settings for this email account. + * + * @since 2.3.0 + * @return array + */ + public function get_smtp_settings(): array { + + $provider = $this->get_provider_instance(); + + if ($provider) { + return $provider->get_smtp_settings($this); + } + + return []; + } + + /** + * Get all email accounts by customer. + * + * @since 2.3.0 + * @param int $customer_id The customer ID. + * @return Email_Account[] + */ + public static function get_by_customer($customer_id) { + + return self::get_items( + [ + 'customer_id' => $customer_id, + ] + ); + } + + /** + * Get all email accounts by membership. + * + * @since 2.3.0 + * @param int $membership_id The membership ID. + * @return Email_Account[] + */ + public static function get_by_membership($membership_id) { + + return self::get_items( + [ + 'membership_id' => $membership_id, + ] + ); + } + + /** + * Get all email accounts by site. + * + * @since 2.3.0 + * @param int $site_id The site ID. + * @return Email_Account[] + */ + public static function get_by_site($site_id) { + + return self::get_items( + [ + 'site_id' => $site_id, + ] + ); + } + + /** + * Get an email account by email address. + * + * @since 2.3.0 + * @param string $email_address The email address. + * @return Email_Account|null + */ + public static function get_by_email_address($email_address) { + + return self::get_by('email_address', strtolower($email_address)); + } + + /** + * Count email accounts for a customer. + * + * @since 2.3.0 + * @param int $customer_id The customer ID. + * @param int|null $membership_id Optional membership ID filter. + * @return int + */ + public static function count_by_customer($customer_id, $membership_id = null): int { + + $args = [ + 'customer_id' => $customer_id, + 'count' => true, + ]; + + if ($membership_id) { + $args['membership_id'] = $membership_id; + } + + return (int) self::get_items($args); + } +} diff --git a/inc/objects/class-limitations.php b/inc/objects/class-limitations.php index 8172e7a5..7ada2590 100644 --- a/inc/objects/class-limitations.php +++ b/inc/objects/class-limitations.php @@ -32,6 +32,7 @@ * @property-read \WP_Ultimo\Limitations\Limit_Domain_Mapping $domain_mapping * @property-read \WP_Ultimo\Limitations\Limit_Customer_User_Role $customer_user_role * @property-read \WP_Ultimo\Limitations\Limit_Hide_Footer_Credits $hide_credits + * @property-read \WP_Ultimo\Limitations\Limit_Email_Accounts $email_accounts */ class Limitations { @@ -494,6 +495,7 @@ public static function repository() { 'domain_mapping' => \WP_Ultimo\Limitations\Limit_Domain_Mapping::class, 'customer_user_role' => \WP_Ultimo\Limitations\Limit_Customer_User_Role::class, 'hide_credits' => \WP_Ultimo\Limitations\Limit_Hide_Footer_Credits::class, + 'email_accounts' => \WP_Ultimo\Limitations\Limit_Email_Accounts::class, ]; return apply_filters('wu_limit_classes', $classes); diff --git a/inc/ui/class-email-accounts-element.php b/inc/ui/class-email-accounts-element.php new file mode 100644 index 00000000..c39080cf --- /dev/null +++ b/inc/ui/class-email-accounts-element.php @@ -0,0 +1,824 @@ + __('General', 'ultimate-multisite'), + 'desc' => __('General', 'ultimate-multisite'), + 'type' => 'header', + ]; + + $fields['title'] = [ + 'type' => 'text', + 'title' => __('Title', 'ultimate-multisite'), + 'value' => __('Email Accounts', 'ultimate-multisite'), + 'desc' => __('Leave blank to hide the title completely.', 'ultimate-multisite'), + 'tooltip' => '', + ]; + + return $fields; + } + + /** + * The list of keywords for this element. + * + * @since 2.3.0 + * @return array + */ + public function keywords() { + + return [ + 'WP Ultimo', + 'Ultimate Multisite', + 'Email', + 'Mail', + ]; + } + + /** + * List of default parameters for the element. + * + * @since 2.3.0 + * @return array + */ + public function defaults() { + + return [ + 'title' => __('Email Accounts', 'ultimate-multisite'), + ]; + } + + /** + * Initializes the singleton. + * + * @since 2.3.0 + * @return void + */ + public function init(): void { + + parent::init(); + + // Check if email accounts feature is enabled + if ( ! wu_get_setting('enable_email_accounts', false)) { + $this->set_display(false); + return; + } + + if ($this->is_preview()) { + $this->site = wu_mock_site(); + $this->customer = wu_mock_customer(); + return; + } + + $this->site = wu_get_current_site(); + $this->customer = wu_get_current_customer(); + + if ( ! $this->site || ! $this->customer) { + $this->set_display(false); + return; + } + + // Check if membership has email accounts enabled + $this->membership = $this->site->get_membership(); + + if ($this->membership && $this->membership->has_limitations()) { + $limitations = $this->membership->get_limitations(); + + if ( ! isset($limitations->email_accounts) || ! $limitations->email_accounts->is_enabled()) { + $this->set_display(false); + return; + } + } + + add_action('plugins_loaded', [$this, 'register_forms']); + } + + /** + * Loads the required scripts. + * + * @since 2.3.0 + * @return void + */ + public function register_scripts(): void { + + add_wubox(); + } + + /** + * Register ajax forms. + * + * @since 2.3.0 + * @return void + */ + public function register_forms(): void { + + wu_register_form( + 'user_create_email_account', + [ + 'render' => [$this, 'render_create_email_form'], + 'handler' => [$this, 'handle_create_email_form'], + 'capability' => 'exist', + ] + ); + + wu_register_form( + 'user_view_email_credentials', + [ + 'render' => [$this, 'render_credentials_modal'], + 'handler' => '__return_empty_string', + 'capability' => 'exist', + ] + ); + + wu_register_form( + 'user_delete_email_account', + [ + 'render' => [$this, 'render_delete_email_form'], + 'handler' => [$this, 'handle_delete_email_form'], + 'capability' => 'exist', + ] + ); + + wu_register_form( + 'user_view_dns_instructions', + [ + 'render' => [$this, 'render_dns_instructions_modal'], + 'handler' => '__return_empty_string', + 'capability' => 'exist', + ] + ); + } + + /** + * Renders the create email account form. + * + * @since 2.3.0 + * @return void + */ + public function render_create_email_form(): void { + + $providers = wu_get_enabled_email_providers(); + $provider_options = []; + + foreach ($providers as $id => $provider) { + $provider_options[ $id ] = $provider->get_title(); + } + + // Get available domains for this customer + $site = wu_get_site(wu_request('current_site')); + $domains = []; + + if ($site) { + // Add the site's primary domain + $site_domain = wp_parse_url(get_site_url($site->get_id()), PHP_URL_HOST); + $domains[ $site_domain ] = $site_domain; + + // Add any mapped domains + $mapped_domains = wu_get_domains( + [ + 'blog_id' => $site->get_id(), + 'active' => true, + ] + ); + foreach ($mapped_domains as $domain) { + $domains[ $domain->get_domain() ] = $domain->get_domain(); + } + } + + $fields = [ + 'provider' => [ + 'type' => 'select', + 'title' => __('Email Provider', 'ultimate-multisite'), + 'placeholder' => __('Select a provider', 'ultimate-multisite'), + 'options' => $provider_options, + 'html_attr' => [ + 'v-model' => 'provider', + ], + ], + 'domain' => [ + 'type' => 'select', + 'title' => __('Domain', 'ultimate-multisite'), + 'placeholder' => __('Select a domain', 'ultimate-multisite'), + 'options' => $domains, + 'html_attr' => [ + 'v-model' => 'domain', + ], + ], + 'username' => [ + 'type' => 'text', + 'title' => __('Username', 'ultimate-multisite'), + 'placeholder' => __('e.g. info, support, admin', 'ultimate-multisite'), + 'html_attr' => [ + 'v-model' => 'username', + ], + ], + 'email_preview' => [ + 'type' => 'note', + 'desc' => sprintf( + '%s: {{ username }}@{{ domain }}', + __('Email Address', 'ultimate-multisite') + ), + 'wrapper_html_attr' => [ + 'v-show' => 'username && domain', + 'v-cloak' => 1, + ], + ], + 'current_site' => [ + 'type' => 'hidden', + 'value' => wu_request('current_site'), + ], + 'submit_button' => [ + 'type' => 'submit', + 'title' => __('Create Email Account', 'ultimate-multisite'), + 'value' => 'save', + 'classes' => 'button button-primary wu-w-full', + 'wrapper_classes' => 'wu-items-end', + 'html_attr' => [ + 'v-bind:disabled' => '!provider || !domain || !username', + ], + ], + ]; + + $form = new Form( + 'create_email_account', + $fields, + [ + 'views' => 'admin-pages/fields', + 'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0', + 'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid', + 'html_attr' => [ + 'data-wu-app' => 'create_email_account', + 'data-state' => wp_json_encode( + [ + 'provider' => '', + 'domain' => '', + 'username' => '', + ] + ), + ], + ] + ); + + $form->render(); + } + + /** + * Handles creation of a new email account. + * + * @since 2.3.0 + * @return void + */ + public function handle_create_email_form(): void { + + $current_user_id = get_current_user_id(); + $current_site_id = wu_request('current_site'); + $current_site = wu_get_site($current_site_id); + + if ( ! $current_site) { + wp_send_json_error(new \WP_Error('invalid_site', __('Invalid site.', 'ultimate-multisite'))); + exit; + } + + $customer = $current_site->get_customer(); + + if ( ! is_super_admin() && (! $customer || $customer->get_user_id() !== $current_user_id)) { + wp_send_json_error(new \WP_Error('no_permissions', __('You do not have permissions to perform this action.', 'ultimate-multisite'))); + exit; + } + + $membership = $current_site->get_membership(); + + // Build email address + $username = sanitize_user(wu_request('username'), true); + $domain = sanitize_text_field(wu_request('domain')); + $provider = sanitize_text_field(wu_request('provider')); + $email_address = $username . '@' . $domain; + + // Validate + if (empty($username) || empty($domain) || empty($provider)) { + wp_send_json_error(new \WP_Error('missing_fields', __('All fields are required.', 'ultimate-multisite'))); + exit; + } + + // Create the account + $manager = \WP_Ultimo\Managers\Email_Account_Manager::get_instance(); + + $email_account = $manager->create_account( + [ + 'customer_id' => $customer->get_id(), + 'membership_id' => $membership ? $membership->get_id() : null, + 'site_id' => $current_site_id, + 'email_address' => $email_address, + 'provider' => $provider, + 'purchase_type' => 'membership_included', + ] + ); + + if (is_wp_error($email_account)) { + wp_send_json_error($email_account); + exit; + } + + wp_send_json_success( + [ + 'redirect_url' => wu_get_current_url(), + ] + ); + + exit; + } + + /** + * Renders the credentials modal. + * + * @since 2.3.0 + * @return void + */ + public function render_credentials_modal(): void { + + $email_account_id = wu_request('email_account_id'); + $email_account = wu_get_email_account($email_account_id); + + if ( ! $email_account) { + echo '

' . esc_html__('Email account not found.', 'ultimate-multisite') . '

'; + return; + } + + // Get password from token if available + $password_token = $email_account->get_meta('password_display_token'); + $password = ''; + + if ($password_token) { + $password = wu_get_email_password_from_token($password_token, $email_account_id); + + if ($password) { + // Clear the token after retrieval + $email_account->delete_meta('password_display_token'); + } + } + + $imap_settings = $email_account->get_imap_settings(); + $smtp_settings = $email_account->get_smtp_settings(); + + $fields = [ + 'email_address' => [ + 'type' => 'text-display', + 'title' => __('Email Address', 'ultimate-multisite'), + 'display_value' => $email_account->get_email_address(), + 'copy' => true, + ], + ]; + + if ($password) { + $fields['password'] = [ + 'type' => 'text-display', + 'title' => __('Password', 'ultimate-multisite'), + 'display_value' => $password, + 'copy' => true, + ]; + + $fields['password_note'] = [ + 'type' => 'note', + 'desc' => '' . __('Please save this password now. For security, it will not be shown again.', 'ultimate-multisite') . '', + ]; + } + + $fields['webmail_url'] = [ + 'type' => 'text-display', + 'title' => __('Webmail URL', 'ultimate-multisite'), + 'display_value' => sprintf('%s', esc_url($email_account->get_webmail_url()), esc_html($email_account->get_webmail_url())), + ]; + + $fields['imap_header'] = [ + 'type' => 'header', + 'title' => __('IMAP Settings', 'ultimate-multisite'), + ]; + + $fields['imap_server'] = [ + 'type' => 'text-display', + 'title' => __('Server', 'ultimate-multisite'), + 'display_value' => $imap_settings['server'] ?? '', + 'copy' => true, + ]; + + $fields['imap_port'] = [ + 'type' => 'text-display', + 'title' => __('Port', 'ultimate-multisite'), + 'display_value' => $imap_settings['port'] ?? '', + ]; + + $fields['smtp_header'] = [ + 'type' => 'header', + 'title' => __('SMTP Settings', 'ultimate-multisite'), + ]; + + $fields['smtp_server'] = [ + 'type' => 'text-display', + 'title' => __('Server', 'ultimate-multisite'), + 'display_value' => $smtp_settings['server'] ?? '', + 'copy' => true, + ]; + + $fields['smtp_port'] = [ + 'type' => 'text-display', + 'title' => __('Port', 'ultimate-multisite'), + 'display_value' => $smtp_settings['port'] ?? '', + ]; + + $form = new Form( + 'view_email_credentials', + $fields, + [ + 'views' => 'admin-pages/fields', + 'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0', + 'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid', + ] + ); + + $form->render(); + } + + /** + * Renders the delete confirmation form. + * + * @since 2.3.0 + * @return void + */ + public function render_delete_email_form(): void { + + $email_account_id = wu_request('email_account_id'); + $email_account = wu_get_email_account($email_account_id); + + $fields = [ + 'warning' => [ + 'type' => 'note', + 'desc' => sprintf( + '%s %s', + __('Warning:', 'ultimate-multisite'), + __('This will permanently delete the email account and all its data from the email provider.', 'ultimate-multisite') + ), + ], + 'confirm' => [ + 'type' => 'toggle', + 'title' => __('Confirm Deletion', 'ultimate-multisite'), + 'desc' => __('I understand this action cannot be undone.', 'ultimate-multisite'), + 'html_attr' => [ + 'v-model' => 'confirmed', + ], + ], + 'email_account_id' => [ + 'type' => 'hidden', + 'value' => $email_account_id, + ], + 'submit_button' => [ + 'type' => 'submit', + 'title' => __('Delete Email Account', 'ultimate-multisite'), + 'value' => 'save', + 'classes' => 'button button-primary wu-w-full', + 'wrapper_classes' => 'wu-items-end', + 'html_attr' => [ + 'v-bind:disabled' => '!confirmed', + ], + ], + ]; + + $form = new Form( + 'delete_email_account', + $fields, + [ + 'views' => 'admin-pages/fields', + 'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0', + 'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid', + 'html_attr' => [ + 'data-wu-app' => 'delete_email_account', + 'data-state' => wp_json_encode( + [ + 'confirmed' => false, + ] + ), + ], + ] + ); + + $form->render(); + } + + /** + * Handles email account deletion. + * + * @since 2.3.0 + * @return void + */ + public function handle_delete_email_form(): void { + + $email_account_id = wu_request('email_account_id'); + $email_account = wu_get_email_account($email_account_id); + + if ( ! $email_account) { + wp_send_json_error(new \WP_Error('not_found', __('Email account not found.', 'ultimate-multisite'))); + exit; + } + + // Verify ownership + $current_user_id = get_current_user_id(); + $customer = $email_account->get_customer(); + + if ( ! is_super_admin() && (! $customer || $customer->get_user_id() !== $current_user_id)) { + wp_send_json_error(new \WP_Error('no_permissions', __('You do not have permissions to perform this action.', 'ultimate-multisite'))); + exit; + } + + // Queue deletion from provider + wu_enqueue_async_action( + 'wu_async_delete_email_account', + [ + 'email_address' => $email_account->get_email_address(), + 'provider' => $email_account->get_provider(), + ], + 'email_account' + ); + + // Delete from database + $email_account->delete(); + + wp_send_json_success( + [ + 'redirect_url' => wu_get_current_url(), + ] + ); + + exit; + } + + /** + * Renders the DNS instructions modal. + * + * @since 2.3.0 + * @return void + */ + public function render_dns_instructions_modal(): void { + + $provider_id = wu_request('provider'); + $domain = wu_request('domain'); + + $provider = wu_get_email_provider($provider_id); + + if ( ! $provider) { + echo '

' . esc_html__('Provider not found.', 'ultimate-multisite') . '

'; + return; + } + + $dns_records = $provider->get_dns_instructions($domain); + + echo '
'; + echo '

' . esc_html__('Add the following DNS records to your domain to enable email:', 'ultimate-multisite') . '

'; + echo ''; + echo ''; + echo ''; + + foreach ($dns_records as $record) { + $priority = isset($record['priority']) ? ' (' . esc_html__('Priority:', 'ultimate-multisite') . ' ' . esc_html($record['priority']) . ')' : ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + if ( ! empty($record['description'])) { + echo ''; + } + } + + echo '
' . esc_html__('Type', 'ultimate-multisite') . '' . esc_html__('Name', 'ultimate-multisite') . '' . esc_html__('Value', 'ultimate-multisite') . '
' . esc_html($record['type']) . '' . esc_html($record['name']) . '' . esc_html($record['value']) . '' . esc_html($priority) . '
' . esc_html($record['description']) . '
'; + echo '
'; + } + + /** + * Runs early on the request lifecycle. + * + * @since 2.3.0 + * @return void + */ + public function setup(): void { + + $this->site = WP_Ultimo()->currents->get_site(); + $this->customer = WP_Ultimo()->currents->get_customer(); + + if ( ! $this->site || ! $this->customer || ! $this->site->is_customer_allowed()) { + $this->set_display(false); + return; + } + + $this->membership = $this->site->get_membership(); + + // Load admin.php for helper functions + require_once wu_path('inc/functions/admin.php'); + } + + /** + * Allows the setup in the context of previews. + * + * @since 2.3.0 + * @return void + */ + public function setup_preview(): void { + + $this->site = wu_mock_site(); + $this->customer = wu_mock_customer(); + $this->membership = wu_mock_membership(); + } + + /** + * The content to be output on the screen. + * + * @since 2.3.0 + * + * @param array $atts Parameters of the block/shortcode. + * @param string|null $content The content inside the shortcode. + * @return void + */ + public function output($atts, $content = null): void { + + $current_site = $this->site; + $customer = $this->customer; + + if ( ! $current_site || ! $customer) { + return; + } + + // Get email accounts for this site + $email_accounts = wu_get_email_accounts( + [ + 'customer_id' => $customer->get_id(), + 'site_id' => $current_site->get_id(), + 'orderby' => 'date_created', + 'order' => 'DESC', + ] + ); + + $accounts = []; + + foreach ($email_accounts as $account) { + $status = new Email_Account_Status($account->get_status()); + + $url_atts = [ + 'current_site' => $current_site->get_id(), + 'email_account_id' => $account->get_id(), + ]; + + $accounts[] = [ + 'id' => $account->get_id(), + 'email_object' => $account, + 'email_address' => $account->get_email_address(), + 'provider' => $account->get_provider(), + 'provider_title' => $account->get_provider_instance() ? $account->get_provider_instance()->get_title() : $account->get_provider(), + 'status' => $status->get_label(), + 'status_class' => $status->get_classes(), + 'webmail_url' => $account->get_webmail_url(), + 'credentials_url' => wu_get_form_url('user_view_email_credentials', $url_atts), + 'delete_url' => wu_get_form_url('user_delete_email_account', $url_atts), + ]; + } + + // Check if can create more + $can_create = false; + + if ($this->membership) { + $can_create = wu_can_create_email_account($customer->get_id(), $this->membership->get_id()); + } + + // Get enabled providers + $providers = wu_get_enabled_email_providers(); + + $url_atts = [ + 'current_site' => $current_site->get_id(), + ]; + + $other_atts = [ + 'email_accounts' => $accounts, + 'can_create' => $can_create && ! empty($providers), + 'modal' => [ + 'label' => __('Add Email Account', 'ultimate-multisite'), + 'icon' => 'wu-circle-with-plus', + 'classes' => 'wubox', + 'url' => wu_get_form_url('user_create_email_account', $url_atts), + ], + ]; + + $atts = array_merge($other_atts, $atts); + + wu_get_template('dashboard-widgets/email-accounts', $atts); + } +} diff --git a/tests/WP_Ultimo/Functions/Email_Account_Functions_Test.php b/tests/WP_Ultimo/Functions/Email_Account_Functions_Test.php new file mode 100644 index 00000000..40160803 --- /dev/null +++ b/tests/WP_Ultimo/Functions/Email_Account_Functions_Test.php @@ -0,0 +1,182 @@ +assertIsString($password); + $this->assertGreaterThanOrEqual(12, strlen($password)); + } + + /** + * Test wu_generate_email_password generates unique passwords. + */ + public function test_wu_generate_email_password_unique(): void { + $passwords = []; + + for ($i = 0; $i < 10; $i++) { + $passwords[] = wu_generate_email_password(); + } + + // All passwords should be unique + $unique = array_unique($passwords); + $this->assertCount(10, $unique); + } + + /** + * Test wu_get_email_account returns false for invalid ID. + * + * Note: This test is skipped as it requires the email accounts table to exist. + */ + public function test_wu_get_email_account_invalid_id(): void { + $this->markTestSkipped('Requires email accounts database table'); + } + + /** + * Test wu_get_email_accounts returns empty array when no accounts. + * + * Note: This test is skipped as it requires the email accounts table to exist. + */ + public function test_wu_get_email_accounts_empty(): void { + $this->markTestSkipped('Requires email accounts database table'); + } + + /** + * Test wu_encrypt_email_password and wu_decrypt_email_password. + */ + public function test_password_encryption_decryption(): void { + $original_password = 'MySecureP@ssw0rd!'; + + $encrypted = wu_encrypt_email_password($original_password); + $this->assertNotEquals($original_password, $encrypted); + $this->assertNotEmpty($encrypted); + + $decrypted = wu_decrypt_email_password($encrypted); + $this->assertEquals($original_password, $decrypted); + } + + /** + * Test wu_store_email_password_token and wu_get_email_password_from_token. + */ + public function test_password_token_storage(): void { + $password = 'TokenTestP@ssword'; + $account_id = 12345; + + $token = wu_store_email_password_token($account_id, $password); + $this->assertNotEmpty($token); + $this->assertIsString($token); + + $retrieved = wu_get_email_password_from_token($token, $account_id); + $this->assertEquals($password, $retrieved); + + // Token should be deleted after retrieval + $second_try = wu_get_email_password_from_token($token, $account_id); + $this->assertFalse($second_try); + } + + /** + * Test wu_get_email_password_from_token with wrong account ID. + */ + public function test_password_token_wrong_account(): void { + $password = 'WrongAccountTest'; + $account_id = 11111; + + $token = wu_store_email_password_token($account_id, $password); + + // Try to retrieve with wrong account ID + $retrieved = wu_get_email_password_from_token($token, 99999); + $this->assertFalse($retrieved); + } + + /** + * Test wu_get_email_account_by_email returns false for non-existent. + * + * Note: This test is skipped as it requires the email accounts table to exist. + */ + public function test_wu_get_email_account_by_email_not_found(): void { + $this->markTestSkipped('Requires email accounts database table'); + } + + /** + * Test that wu_get_enabled_email_providers returns array. + */ + public function test_wu_get_enabled_email_providers_returns_array(): void { + $providers = wu_get_enabled_email_providers(); + + $this->assertIsArray($providers); + } + + /** + * Test wu_count_email_accounts returns zero when no accounts. + * + * Note: This test is skipped as it requires the email accounts table to exist. + */ + public function test_wu_count_email_accounts_returns_zero(): void { + $this->markTestSkipped('Requires email accounts database table'); + } + + /** + * Test password encryption with special characters. + */ + public function test_password_encryption_special_chars(): void { + $password = 'P@$$w0rd!#%^&*()_+-=[]{}|;:,.<>?~`'; + + $encrypted = wu_encrypt_email_password($password); + $decrypted = wu_decrypt_email_password($encrypted); + + $this->assertEquals($password, $decrypted); + } + + /** + * Test password encryption with empty string. + */ + public function test_password_encryption_empty(): void { + $password = ''; + + $encrypted = wu_encrypt_email_password($password); + $decrypted = wu_decrypt_email_password($encrypted); + + $this->assertEquals($password, $decrypted); + } + + /** + * Test password encryption with long password. + */ + public function test_password_encryption_long(): void { + $password = str_repeat('a', 1000); + + $encrypted = wu_encrypt_email_password($password); + $decrypted = wu_decrypt_email_password($encrypted); + + $this->assertEquals($password, $decrypted); + } + + /** + * Test wu_generate_email_password with custom length. + */ + public function test_wu_generate_email_password_custom_length(): void { + $password = wu_generate_email_password(24); + + $this->assertIsString($password); + $this->assertEquals(24, strlen($password)); + } +} diff --git a/tests/WP_Ultimo/Limitations/Limit_Email_Accounts_Test.php b/tests/WP_Ultimo/Limitations/Limit_Email_Accounts_Test.php new file mode 100644 index 00000000..9cfbd041 --- /dev/null +++ b/tests/WP_Ultimo/Limitations/Limit_Email_Accounts_Test.php @@ -0,0 +1,399 @@ + true, + 'limit' => 5, + ] + ); + + $this->assertTrue($limit->is_enabled()); + $this->assertEquals(5, $limit->get_limit()); + } + + /** + * Test limit initialization with disabled. + */ + public function test_limit_initialization_disabled(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => false, + 'limit' => 10, + ] + ); + + $this->assertFalse($limit->is_enabled()); + } + + /** + * Test limit initialization with zero (unlimited). + */ + public function test_limit_initialization_zero_unlimited(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => 0, + ] + ); + + $this->assertTrue($limit->is_enabled()); + $this->assertEquals(0, $limit->get_limit()); + } + + /** + * Test limit initialization with boolean true (unlimited). + */ + public function test_limit_initialization_boolean_true(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => true, + ] + ); + + $this->assertTrue($limit->is_enabled()); + $this->assertTrue($limit->get_limit()); + } + + /** + * Test limit initialization with boolean false (none allowed). + */ + public function test_limit_initialization_boolean_false(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => false, + ] + ); + + $this->assertTrue($limit->is_enabled()); + $this->assertFalse($limit->get_limit()); + } + + /** + * Test check method with boolean true limit (unlimited). + */ + public function test_check_with_boolean_true_limit(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => true, + ] + ); + + $this->assertTrue($limit->check(0, true)); + $this->assertTrue($limit->check(100, true)); + } + + /** + * Test check method with boolean false limit (none allowed). + */ + public function test_check_with_boolean_false_limit(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => false, + ] + ); + + $this->assertFalse($limit->check(0, false)); + $this->assertFalse($limit->check(1, false)); + } + + /** + * Test check method with zero limit (unlimited). + */ + public function test_check_with_zero_limit_unlimited(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => 0, + ] + ); + + $this->assertTrue($limit->check(0, 0)); + $this->assertTrue($limit->check(100, 0)); + $this->assertTrue($limit->check(9999, 0)); + } + + /** + * Test check method with numeric limit when under limit. + */ + public function test_check_with_numeric_limit_under_limit(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => 5, + ] + ); + + $this->assertTrue($limit->check(0, 5)); // 0 < 5 + $this->assertTrue($limit->check(3, 5)); // 3 < 5 + $this->assertTrue($limit->check(4, 5)); // 4 < 5 + } + + /** + * Test check method with numeric limit when at limit. + */ + public function test_check_with_numeric_limit_at_limit(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => 5, + ] + ); + + $this->assertFalse($limit->check(5, 5)); // 5 is not < 5 + } + + /** + * Test check method with numeric limit when over limit. + */ + public function test_check_with_numeric_limit_over_limit(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => 5, + ] + ); + + $this->assertFalse($limit->check(6, 5)); + $this->assertFalse($limit->check(100, 5)); + } + + /** + * Test check method when limit is disabled. + */ + public function test_check_with_disabled_limit(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => false, + 'limit' => 5, + ] + ); + + $this->assertFalse($limit->check(0, 5)); + $this->assertFalse($limit->check(3, 5)); + } + + /** + * Test can_create_more with unlimited (true). + */ + public function test_can_create_more_unlimited_true(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => true, + ] + ); + + // With unlimited, should always return true + $result = $limit->can_create_more(1, 1); + $this->assertTrue($result); + } + + /** + * Test can_create_more with zero (unlimited). + */ + public function test_can_create_more_zero_unlimited(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => 0, + ] + ); + + $result = $limit->can_create_more(1, 1); + $this->assertTrue($result); + } + + /** + * Test can_create_more when disabled. + */ + public function test_can_create_more_disabled(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => false, + 'limit' => 5, + ] + ); + + $result = $limit->can_create_more(1, 1); + $this->assertFalse($result); + } + + /** + * Test can_create_more with false limit. + */ + public function test_can_create_more_false_limit(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => false, + ] + ); + + $result = $limit->can_create_more(1, 1); + $this->assertFalse($result); + } + + /** + * Test get_remaining_slots with unlimited (true). + */ + public function test_get_remaining_slots_unlimited_true(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => true, + ] + ); + + $result = $limit->get_remaining_slots(1, 1); + $this->assertEquals('unlimited', $result); + } + + /** + * Test get_remaining_slots with zero (unlimited). + */ + public function test_get_remaining_slots_zero_unlimited(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => 0, + ] + ); + + $result = $limit->get_remaining_slots(1, 1); + $this->assertEquals('unlimited', $result); + } + + /** + * Test get_remaining_slots when disabled. + */ + public function test_get_remaining_slots_disabled(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => false, + 'limit' => 5, + ] + ); + + $result = $limit->get_remaining_slots(1, 1); + $this->assertEquals(0, $result); + } + + /** + * Test get_remaining_slots with false limit. + */ + public function test_get_remaining_slots_false_limit(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => false, + ] + ); + + $result = $limit->get_remaining_slots(1, 1); + $this->assertEquals(0, $result); + } + + /** + * Test get_remaining_slots with numeric limit using mock. + */ + public function test_get_remaining_slots_numeric(): void { + $limit_mock = $this->getMockBuilder(Limit_Email_Accounts::class) + ->setConstructorArgs( + [ + [ + 'enabled' => true, + 'limit' => 5, + ], + ] + ) + ->onlyMethods(['get_current_account_count']) + ->getMock(); + + $limit_mock->expects($this->once()) + ->method('get_current_account_count') + ->willReturn(2); + + $result = $limit_mock->get_remaining_slots(1, 1); + $this->assertEquals(3, $result); // 5 - 2 = 3 + } + + /** + * Test get_remaining_slots returns zero when over limit. + */ + public function test_get_remaining_slots_over_limit(): void { + $limit_mock = $this->getMockBuilder(Limit_Email_Accounts::class) + ->setConstructorArgs( + [ + [ + 'enabled' => true, + 'limit' => 2, + ], + ] + ) + ->onlyMethods(['get_current_account_count']) + ->getMock(); + + $limit_mock->expects($this->once()) + ->method('get_current_account_count') + ->willReturn(5); + + $result = $limit_mock->get_remaining_slots(1, 1); + $this->assertEquals(0, $result); // max(0, 2 - 5) = 0 + } + + /** + * Test default state. + */ + public function test_default_state(): void { + $default = Limit_Email_Accounts::default_state(); + + $this->assertIsArray($default); + $this->assertArrayHasKey('enabled', $default); + $this->assertArrayHasKey('limit', $default); + $this->assertFalse($default['enabled']); + $this->assertEquals(0, $default['limit']); + } + + /** + * Test module ID. + */ + public function test_module_id(): void { + $limit = new Limit_Email_Accounts( + [ + 'enabled' => true, + 'limit' => 5, + ] + ); + + // Access the protected id property via reflection + $reflection = new \ReflectionClass($limit); + $property = $reflection->getProperty('id'); + $property->setAccessible(true); + + $this->assertEquals('email_accounts', $property->getValue($limit)); + } +} diff --git a/tests/WP_Ultimo/Models/Email_Account_Test.php b/tests/WP_Ultimo/Models/Email_Account_Test.php new file mode 100644 index 00000000..d1b7b933 --- /dev/null +++ b/tests/WP_Ultimo/Models/Email_Account_Test.php @@ -0,0 +1,345 @@ +set_customer_id(1); + $email_account->set_membership_id(1); + $email_account->set_email_address('user@example.com'); + $email_account->set_provider('cpanel'); + $email_account->set_status('active'); + $email_account->set_quota_mb(1024); + + $this->assertEquals(1, $email_account->get_customer_id()); + $this->assertEquals(1, $email_account->get_membership_id()); + $this->assertEquals('user@example.com', $email_account->get_email_address()); + $this->assertEquals('cpanel', $email_account->get_provider()); + $this->assertEquals('active', $email_account->get_status()); + $this->assertEquals(1024, $email_account->get_quota_mb()); + } + + /** + * Test email address parsing. + */ + public function test_email_address_parsing(): void { + $email_account = new Email_Account(); + $email_account->set_email_address('testuser@mydomain.com'); + + $this->assertEquals('testuser@mydomain.com', $email_account->get_email_address()); + $this->assertEquals('testuser', $email_account->get_username()); + $this->assertEquals('mydomain.com', $email_account->get_domain()); + } + + /** + * Test domain extraction from email address. + */ + public function test_domain_extraction(): void { + $email_account = new Email_Account(); + $email_account->set_email_address('info@subdomain.example.org'); + + $this->assertEquals('subdomain.example.org', $email_account->get_domain()); + } + + /** + * Test status setters and getters. + */ + public function test_status_functionality(): void { + $email_account = new Email_Account(); + + // Test all valid statuses + $statuses = ['pending', 'provisioning', 'active', 'suspended', 'failed']; + + foreach ($statuses as $status) { + $email_account->set_status($status); + $this->assertEquals($status, $email_account->get_status()); + } + } + + /** + * Test status label retrieval. + */ + public function test_status_label(): void { + $email_account = new Email_Account(); + + $email_account->set_status('active'); + $this->assertNotEmpty($email_account->get_status_label()); + + $email_account->set_status('pending'); + $this->assertNotEmpty($email_account->get_status_label()); + + $email_account->set_status('failed'); + $this->assertNotEmpty($email_account->get_status_label()); + } + + /** + * Test status class retrieval. + */ + public function test_status_class(): void { + $email_account = new Email_Account(); + + $email_account->set_status('active'); + $this->assertNotEmpty($email_account->get_status_class()); + + $email_account->set_status('failed'); + $this->assertNotEmpty($email_account->get_status_class()); + } + + /** + * Test purchase type functionality. + */ + public function test_purchase_type_functionality(): void { + $email_account = new Email_Account(); + + // Default should be membership_included + $email_account->set_purchase_type('membership_included'); + $this->assertEquals('membership_included', $email_account->get_purchase_type()); + + $email_account->set_purchase_type('per_account'); + $this->assertEquals('per_account', $email_account->get_purchase_type()); + } + + /** + * Test quota functionality. + */ + public function test_quota_functionality(): void { + $email_account = new Email_Account(); + + // Test zero quota (unlimited) + $email_account->set_quota_mb(0); + $this->assertEquals(0, $email_account->get_quota_mb()); + + // Test positive quota + $email_account->set_quota_mb(2048); + $this->assertEquals(2048, $email_account->get_quota_mb()); + } + + /** + * Test external ID functionality. + */ + public function test_external_id_functionality(): void { + $email_account = new Email_Account(); + + $external_id = 'provider-account-12345'; + $email_account->set_external_id($external_id); + + $this->assertEquals($external_id, $email_account->get_external_id()); + } + + /** + * Test site ID functionality. + */ + public function test_site_id_functionality(): void { + $email_account = new Email_Account(); + + $email_account->set_site_id(123); + $this->assertEquals(123, $email_account->get_site_id()); + + $email_account->set_site_id(null); + $this->assertNull($email_account->get_site_id()); + } + + /** + * Test payment ID functionality. + */ + public function test_payment_id_functionality(): void { + $email_account = new Email_Account(); + + $email_account->set_payment_id(456); + $this->assertEquals(456, $email_account->get_payment_id()); + + $email_account->set_payment_id(null); + $this->assertNull($email_account->get_payment_id()); + } + + /** + * Test validation rules exist. + */ + public function test_validation_rules_exist(): void { + $email_account = new Email_Account(); + $rules = $email_account->validation_rules(); + + $this->assertIsArray($rules); + $this->assertArrayHasKey('customer_id', $rules); + $this->assertArrayHasKey('email_address', $rules); + $this->assertArrayHasKey('provider', $rules); + $this->assertArrayHasKey('status', $rules); + } + + /** + * Test default status value. + */ + public function test_default_status(): void { + $email_account = new Email_Account(); + + // Default status should be 'pending' + $this->assertEquals('pending', $email_account->get_status()); + } + + /** + * Test to_array method. + */ + public function test_to_array(): void { + $email_account = new Email_Account(); + $email_account->set_customer_id(1); + $email_account->set_email_address('test@example.com'); + $email_account->set_provider('cpanel'); + $email_account->set_status('active'); + + $array = $email_account->to_array(); + + $this->assertIsArray($array); + $this->assertEquals(1, $array['customer_id']); + $this->assertEquals('test@example.com', $array['email_address']); + $this->assertEquals('cpanel', $array['provider']); + $this->assertEquals('active', $array['status']); + } + + /** + * Test provider values. + */ + public function test_provider_values(): void { + $email_account = new Email_Account(); + + $providers = ['cpanel', 'purelymail', 'google_workspace', 'microsoft365']; + + foreach ($providers as $provider) { + $email_account->set_provider($provider); + $this->assertEquals($provider, $email_account->get_provider()); + } + } + + /** + * Test empty email address handling. + */ + public function test_empty_email_address(): void { + $email_account = new Email_Account(); + $email_account->set_email_address(''); + + $this->assertEquals('', $email_account->get_email_address()); + $this->assertEquals('', $email_account->get_username()); + $this->assertEquals('', $email_account->get_domain()); + } + + /** + * Test get_customer returns false for invalid customer. + */ + public function test_invalid_customer_returns_false(): void { + $email_account = new Email_Account(); + $email_account->set_customer_id(99999); + + $this->assertFalse($email_account->get_customer()); + } + + /** + * Test get_membership returns false for invalid membership. + */ + public function test_invalid_membership_returns_false(): void { + $email_account = new Email_Account(); + $email_account->set_membership_id(99999); + + $this->assertFalse($email_account->get_membership()); + } + + /** + * Test get_customer relationship with real customer. + */ + public function test_get_customer_relationship(): void { + $user_id = self::factory()->user->create( + [ + 'user_login' => 'emailcustomertest', + 'user_email' => 'emailcustomertest@example.com', + ] + ); + + $customer = new Customer(); + $customer->set_user_id($user_id); + $customer->set_type('customer'); + $customer->set_email_verification('none'); + $result = $customer->save(); + + if (is_wp_error($result)) { + $this->markTestSkipped('Could not create test customer: ' . $result->get_error_message()); + } + + $email_account = new Email_Account(); + $email_account->set_customer_id($customer->get_id()); + + $retrieved = $email_account->get_customer(); + $this->assertInstanceOf(Customer::class, $retrieved); + $this->assertEquals($customer->get_id(), $retrieved->get_id()); + + // Remove hook to avoid query to non-existent table during cleanup + remove_action('wu_customer_post_delete', [\WP_Ultimo\Managers\Email_Account_Manager::get_instance(), 'handle_customer_deleted'], 10); + + // Clean up + $customer->delete(); + } + + /** + * Test get_membership relationship with real membership. + */ + public function test_get_membership_relationship(): void { + $user_id = self::factory()->user->create( + [ + 'user_login' => 'emailmembershiptest', + 'user_email' => 'emailmembershiptest@example.com', + ] + ); + + $customer = new Customer(); + $customer->set_user_id($user_id); + $customer->set_type('customer'); + $customer->set_email_verification('none'); + $result = $customer->save(); + + if (is_wp_error($result)) { + $this->markTestSkipped('Could not create test customer: ' . $result->get_error_message()); + } + + $membership = new Membership(); + $membership->set_customer_id($customer->get_id()); + $membership->set_status('active'); + $result = $membership->save(); + + if (is_wp_error($result)) { + // Remove hook to avoid query to non-existent table during cleanup + remove_action('wu_customer_post_delete', [\WP_Ultimo\Managers\Email_Account_Manager::get_instance(), 'handle_customer_deleted'], 10); + $customer->delete(); + $this->markTestSkipped('Could not create test membership: ' . $result->get_error_message()); + } + + $email_account = new Email_Account(); + $email_account->set_membership_id($membership->get_id()); + + $retrieved = $email_account->get_membership(); + $this->assertInstanceOf(Membership::class, $retrieved); + $this->assertEquals($membership->get_id(), $retrieved->get_id()); + + // Remove hooks to avoid query to non-existent table during cleanup + remove_action('wu_membership_post_delete', [\WP_Ultimo\Managers\Email_Account_Manager::get_instance(), 'handle_membership_deleted'], 10); + remove_action('wu_customer_post_delete', [\WP_Ultimo\Managers\Email_Account_Manager::get_instance(), 'handle_customer_deleted'], 10); + + // Clean up + $membership->delete(); + $customer->delete(); + } +} diff --git a/views/dashboard-widgets/email-accounts.php b/views/dashboard-widgets/email-accounts.php new file mode 100644 index 00000000..6af6cc9a --- /dev/null +++ b/views/dashboard-widgets/email-accounts.php @@ -0,0 +1,250 @@ + +
+ +
+ + +
+ + + +

+ + + +

+ + + +
+ + + + 0 ) : ?> + + + + + + + + + +
+ +
+ + + + +
+ + + + + 0) : ?> + + + 0) : ?> + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + +
+ 0)) : ?> + + +
+ + + + + + + + + + +
+ + + + + +
+ + get_status_label(); + $status_class = $account->get_status_class(); + + $status = "$status_label"; + + $second_row_actions = []; + + // Webmail link (only for active accounts) + if ($account->get_status() === 'active' && ! empty($account_data['webmail_url'])) { + $second_row_actions['webmail'] = [ + 'wrapper_classes' => '', + 'icon' => 'dashicons-wu-globe wu-align-middle wu-mr-1', + 'label' => '', + 'url' => $account_data['webmail_url'], + 'value' => __('Open Webmail', 'ultimate-multisite'), + 'attrs' => 'target="_blank" rel="noopener"', + ]; + } + + // View credentials (only for active accounts) + if ($account->get_status() === 'active') { + $second_row_actions['credentials'] = [ + 'wrapper_classes' => 'wubox', + 'icon' => 'dashicons-wu-key wu-align-middle wu-mr-1', + 'label' => '', + 'url' => $account_data['credentials_link'], + 'value' => __('View Settings', 'ultimate-multisite'), + ]; + } + + // DNS instructions link + if (! empty($account_data['dns_link'])) { + $second_row_actions['dns'] = [ + 'wrapper_classes' => 'wubox', + 'icon' => 'dashicons-wu-server wu-align-middle wu-mr-1', + 'label' => '', + 'url' => $account_data['dns_link'], + 'value' => __('DNS Setup', 'ultimate-multisite'), + ]; + } + + // Delete link + $second_row_actions['delete'] = [ + 'wrapper_classes' => 'wu-text-red-500 wubox', + 'icon' => 'dashicons-wu-trash-2 wu-align-middle wu-mr-1', + 'label' => '', + 'value' => __('Delete', 'ultimate-multisite'), + 'url' => $account_data['delete_link'], + ]; + + // Provider info + $provider_id = $account->get_provider(); + $provider_label = ucfirst($provider_id); + + // Get provider instance for better label + $manager = \WP_Ultimo\Managers\Email_Account_Manager::get_instance(); + if ($manager) { + $provider = $manager->get_provider($provider_id); + if ($provider) { + $provider_label = $provider->get_title(); + } + } + + wu_responsive_table_row( + [ + 'id' => false, + 'title' => strtolower($account->get_email_address()), + 'url' => false, + 'status' => $status, + ], + [ + 'provider' => [ + 'wrapper_classes' => '', + 'icon' => 'dashicons-wu-mail wu-align-text-bottom wu-mr-1', + 'label' => '', + 'value' => $provider_label, + ], + 'quota' => [ + 'wrapper_classes' => '', + 'icon' => 'dashicons-wu-database wu-align-text-bottom wu-mr-1', + 'label' => '', + 'value' => function () use ($account) { + $quota = $account->get_quota_mb(); + if ($quota > 0) { + if ($quota >= 1024) { + printf( + /* translators: %s is quota in GB */ + esc_html__('%s GB', 'ultimate-multisite'), + esc_html(number_format_i18n($quota / 1024, 1)) + ); + } else { + printf( + /* translators: %d is quota in MB */ + esc_html__('%d MB', 'ultimate-multisite'), + absint($quota) + ); + } + } else { + esc_html_e('Unlimited', 'ultimate-multisite'); + } + }, + ], + ], + $second_row_actions + ); + + ?> + +
+ +
+ +
+ +
diff --git a/views/wizards/email-integrations/activation.php b/views/wizards/email-integrations/activation.php new file mode 100644 index 00000000..a73fdb6b --- /dev/null +++ b/views/wizards/email-integrations/activation.php @@ -0,0 +1,112 @@ + +

+ + get_title())); ?> +

+ +

+ get_description(), wu_kses_allowed_html()); ?> +

+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + + + + + +
+ + + +
+ + + not:', 'ultimate-multisite'), ['strong' => []]); ?> + + + + +
+ + + +
+ + +
+ + + + + + is_enabled()) : ?> + + + + + + + + + +   + + is_enabled()) : ?> + + + + + + + +
+ diff --git a/views/wizards/email-integrations/configuration-results.php b/views/wizards/email-integrations/configuration-results.php new file mode 100644 index 00000000..ed771cde --- /dev/null +++ b/views/wizards/email-integrations/configuration-results.php @@ -0,0 +1,35 @@ + +

+ +

+ +

+ +
+
get_constants_string($post_data)); ?>
+
+ +

+ +

+ + +
+ + + + + + + +
+ diff --git a/views/wizards/email-integrations/configuration.php b/views/wizards/email-integrations/configuration.php new file mode 100644 index 00000000..a683ba8d --- /dev/null +++ b/views/wizards/email-integrations/configuration.php @@ -0,0 +1,37 @@ + +

+ +

+ +

+ +render(); ?> + + + + +
+ + + + + + + + + + + +
+ diff --git a/views/wizards/email-integrations/default-instructions.php b/views/wizards/email-integrations/default-instructions.php new file mode 100644 index 00000000..103e1340 --- /dev/null +++ b/views/wizards/email-integrations/default-instructions.php @@ -0,0 +1,81 @@ + +

+ get_title()) + ); + ?> +

+ +

+ +

+ +
+ +
    + +
  1. + +

    + get_title()) + ); + ?> +

    +
  2. + +
  3. + +

    + +

    +
  4. + +
  5. + +

    + +

    +
  6. + +
+ + get_documentation_link()) : ?> +
+

+ + %1$s documentation.', 'ultimate-multisite'); + printf( + wp_kses($instructions_text, wu_kses_allowed_html()), + esc_html($integration->get_title()), + esc_url($integration->get_documentation_link()) + ); + ?> +

+
+ + + get_affiliate_url()) : ?> +
+

+ get_signup_instructions(), wu_kses_allowed_html()); ?> +

+
+ + +
diff --git a/views/wizards/email-integrations/ready.php b/views/wizards/email-integrations/ready.php new file mode 100644 index 00000000..abc8e25e --- /dev/null +++ b/views/wizards/email-integrations/ready.php @@ -0,0 +1,49 @@ + +

+ +

+ get_title()) + ); + ?> +

+ +
+ +
+ +

+

+ +

+
+ + get_affiliate_url()) : ?> +
+

+ get_signup_instructions(), wu_kses_allowed_html()); ?> +

+
+ + +
+ + +
+ + + + + +
+ diff --git a/views/wizards/email-integrations/test.php b/views/wizards/email-integrations/test.php new file mode 100644 index 00000000..a280632f --- /dev/null +++ b/views/wizards/email-integrations/test.php @@ -0,0 +1,116 @@ + +
+ +

+ +

+ +

+ +
+ +
+ +

{{ waiting_message }}

+
+ +
+ +

+

{{ message }}

+
+ +
+ +

+

{{ message }}

+
+ +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + +
+ +