diff --git a/inc/admin-pages/class-hosting-integration-wizard-admin-page.php b/inc/admin-pages/class-hosting-integration-wizard-admin-page.php
index 80d39eb5b..a888d067e 100644
--- a/inc/admin-pages/class-hosting-integration-wizard-admin-page.php
+++ b/inc/admin-pages/class-hosting-integration-wizard-admin-page.php
@@ -241,7 +241,10 @@ public function section_instructions(): void {
*/
public function section_configuration(): void {
- $fields = $this->integration->get_fields();
+ $fields = $this->integration->get_fields();
+ $configuration_instructions = method_exists($this->integration, 'get_configuration_instructions')
+ ? call_user_func([$this->integration, 'get_configuration_instructions'])
+ : [];
// Aggregate fields from capability modules if this is a new Integration
if ($this->integration instanceof \WP_Ultimo\Integrations\Integration) {
@@ -292,10 +295,11 @@ public function section_configuration(): void {
wu_get_template(
'wizards/host-integrations/configuration',
[
- 'screen' => get_current_screen(),
- 'page' => $this,
- 'integration' => $this->integration,
- 'form' => $form,
+ 'screen' => get_current_screen(),
+ 'page' => $this,
+ 'integration' => $this->integration,
+ 'form' => $form,
+ 'configuration_instructions' => $configuration_instructions,
]
);
}
diff --git a/inc/helpers/class-oci-signer.php b/inc/helpers/class-oci-signer.php
new file mode 100644
index 000000000..a4488d99d
--- /dev/null
+++ b/inc/helpers/class-oci-signer.php
@@ -0,0 +1,174 @@
+tenancy_ocid = $tenancy_ocid;
+ $this->user_ocid = $user_ocid;
+ $this->fingerprint = $fingerprint;
+ $this->private_key = $this->normalize_private_key($private_key);
+ }
+
+ /**
+ * Sign an OCI HTTP request.
+ *
+ * @since 2.5.0
+ *
+ * @param string $method HTTP method.
+ * @param string $url Full request URL.
+ * @param string $payload Request body.
+ * @return arrayses:GetAccount, ses:CreateEmailIdentity, ses:GetEmailIdentity, ses:DeleteEmailIdentity, and ses:SendEmail.', 'ultimate-multisite'),
+ __('Enter the AWS Access Key ID and Secret Access Key. Prefer a restricted IAM user dedicated to Ultimate Multisite instead of a root or broad administrator key.', 'ultimate-multisite'),
+ __('Set the AWS region where SES is out of sandbox and where you want identities created, for example us-east-1. The region must match your approved SES sending region.', 'ultimate-multisite'),
+ __('After testing succeeds, add the SPF, DKIM, and DMARC records returned for each domain to DNS. Mail should only be considered fully configured when recipient headers show spf=pass, dkim=pass, and dmarc=pass.', 'ultimate-multisite'),
+ ],
+ ];
+ }
}
diff --git a/inc/integrations/providers/oci-email/class-oci-email-integration.php b/inc/integrations/providers/oci-email/class-oci-email-integration.php
new file mode 100644
index 000000000..8d781c46e
--- /dev/null
+++ b/inc/integrations/providers/oci-email/class-oci-email-integration.php
@@ -0,0 +1,272 @@
+set_logo(function_exists('wu_get_asset') ? wu_get_asset('oracle-oci.svg', 'img/hosts') : '');
+ $this->set_tutorial_link('https://ultimatemultisite.com/docs/user-guide/host-integrations/oracle-oci-email-delivery');
+ $this->set_constants(['WU_OCI_TENANCY_OCID', 'WU_OCI_USER_OCID', 'WU_OCI_KEY_FINGERPRINT', 'WU_OCI_PRIVATE_KEY', 'WU_OCI_COMPARTMENT_OCID']);
+ $this->set_optional_constants(['WU_OCI_REGION', 'WU_OCI_EMAIL_SPF_INCLUDE', 'WU_OCI_EMAIL_DOMAIN_ENDPOINT']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_description(): string {
+
+ return __('Oracle Cloud Infrastructure Email Delivery sends high-volume transactional email through OCI. Use it to create Email Delivery domains, request DKIM, and publish the returned SPF/DKIM/DMARC records for each mapped domain.', 'ultimate-multisite');
+ }
+
+ /**
+ * Returns the OCI region to use for Email Delivery.
+ *
+ * @since 2.5.0
+ * @return string
+ */
+ public function get_region(): string {
+
+ $region = $this->get_credential('WU_OCI_REGION');
+
+ return $region ?: 'eu-zurich-1';
+ }
+
+ /**
+ * Returns the OCI API base URL for the configured region.
+ *
+ * @since 2.5.0
+ * @return string
+ */
+ public function get_api_base(): string {
+
+ $endpoint = $this->get_credential('WU_OCI_EMAIL_DOMAIN_ENDPOINT');
+
+ if ($endpoint) {
+ return trailingslashit($endpoint);
+ }
+
+ return sprintf(self::API_BASE, $this->get_region());
+ }
+
+ /**
+ * Returns the SPF include host for OCI Email Delivery.
+ *
+ * @since 2.5.0
+ * @return string
+ */
+ public function get_spf_include(): string {
+
+ $spf_include = $this->get_credential('WU_OCI_EMAIL_SPF_INCLUDE');
+
+ return $spf_include ?: 'rp.oracleemaildelivery.com';
+ }
+
+ /**
+ * Returns the configured OCI compartment OCID.
+ *
+ * @since 2.5.0
+ * @return string
+ */
+ public function get_compartment_ocid(): string {
+
+ return $this->get_credential('WU_OCI_COMPARTMENT_OCID');
+ }
+
+ /**
+ * Returns a configured OCI signer instance.
+ *
+ * @since 2.5.0
+ * @return OCI_Signer
+ */
+ public function get_signer(): OCI_Signer {
+
+ return new OCI_Signer(
+ $this->get_credential('WU_OCI_TENANCY_OCID'),
+ $this->get_credential('WU_OCI_USER_OCID'),
+ $this->get_credential('WU_OCI_KEY_FINGERPRINT'),
+ $this->get_credential('WU_OCI_PRIVATE_KEY')
+ );
+ }
+
+ /**
+ * Makes an authenticated request to the OCI Email Delivery API.
+ *
+ * @since 2.5.0
+ *
+ * @param string $endpoint Relative endpoint path.
+ * @param string $method HTTP method. Defaults to GET.
+ * @param array $data Request body data (JSON encoded for non-GET requests).
+ * @return array|\WP_Error Decoded response array or WP_Error on failure.
+ */
+ public function oci_api_call(string $endpoint, string $method = 'GET', array $data = []) {
+
+ $url = $this->get_api_base() . ltrim($endpoint, '/');
+ $payload = ('GET' === $method || empty($data)) ? '' : wp_json_encode($data);
+ $headers = $this->get_signer()->sign($method, $url, $payload ?: '');
+
+ if (empty($headers)) {
+ return new \WP_Error('oracle-oci-signing-error', __('Unable to sign OCI API request. Check the configured private key.', 'ultimate-multisite'));
+ }
+
+ $request_args = [
+ 'method' => $method,
+ 'headers' => $headers,
+ ];
+
+ if ($payload) {
+ $request_args['body'] = $payload;
+ }
+
+ $response = wp_remote_request($url, $request_args);
+
+ if (is_wp_error($response)) {
+ return $response;
+ }
+
+ $status_code = wp_remote_retrieve_response_code($response);
+ $body = wp_remote_retrieve_body($response);
+ $decoded = json_decode($body, true);
+
+ if ($status_code >= 200 && $status_code < 300) {
+ return $decoded ?: [];
+ }
+
+ $error_message = isset($decoded['message']) ? $decoded['message'] : wp_remote_retrieve_response_message($response);
+
+ return new \WP_Error(
+ 'oracle-oci-error',
+ sprintf(
+ /* translators: 1: HTTP status code, 2: error message */
+ __('OCI Email Delivery API error (HTTP %1$d): %2$s', 'ultimate-multisite'),
+ $status_code,
+ $error_message
+ )
+ );
+ }
+
+ /**
+ * Tests the connection to the OCI Email Delivery API.
+ *
+ * @since 2.5.0
+ * @return true|\WP_Error
+ */
+ public function test_connection() {
+
+ $result = $this->oci_api_call('emailDomains?compartmentId=' . rawurlencode($this->get_compartment_ocid()) . '&limit=1');
+
+ if (is_wp_error($result)) {
+ return $result;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the credential form fields for the setup wizard.
+ *
+ * @since 2.5.0
+ * @return array
+ */
+ public function get_fields(): array {
+
+ return [
+ 'WU_OCI_TENANCY_OCID' => [
+ 'title' => __('OCI Tenancy OCID', 'ultimate-multisite'),
+ 'placeholder' => __('e.g. ocid1.tenancy.oc1..aaaa...', 'ultimate-multisite'),
+ ],
+ 'WU_OCI_USER_OCID' => [
+ 'title' => __('OCI User OCID', 'ultimate-multisite'),
+ 'placeholder' => __('e.g. ocid1.user.oc1..aaaa...', 'ultimate-multisite'),
+ ],
+ 'WU_OCI_KEY_FINGERPRINT' => [
+ 'title' => __('OCI API Key Fingerprint', 'ultimate-multisite'),
+ 'placeholder' => __('e.g. 12:34:56:...', 'ultimate-multisite'),
+ ],
+ 'WU_OCI_PRIVATE_KEY' => [
+ 'title' => __('OCI API Private Key', 'ultimate-multisite'),
+ 'type' => 'textarea',
+ 'desc' => __('Paste the PEM private key for the OCI API key. Newline escape sequences are supported.', 'ultimate-multisite'),
+ 'html_attr' => [
+ 'autocomplete' => 'new-password',
+ ],
+ ],
+ 'WU_OCI_COMPARTMENT_OCID' => [
+ 'title' => __('OCI Compartment OCID', 'ultimate-multisite'),
+ 'placeholder' => __('e.g. ocid1.compartment.oc1..aaaa...', 'ultimate-multisite'),
+ ],
+ 'WU_OCI_REGION' => [
+ 'title' => __('OCI Region', 'ultimate-multisite'),
+ 'placeholder' => __('e.g. eu-zurich-1', 'ultimate-multisite'),
+ 'desc' => __('Optional. The OCI region for Email Delivery. Defaults to eu-zurich-1.', 'ultimate-multisite'),
+ ],
+ 'WU_OCI_EMAIL_SPF_INCLUDE' => [
+ 'title' => __('OCI SPF Include Host', 'ultimate-multisite'),
+ 'placeholder' => __('rp.oracleemaildelivery.com', 'ultimate-multisite'),
+ 'desc' => __('Optional. Override only if Oracle changes the SPF include host for your tenancy or region.', 'ultimate-multisite'),
+ ],
+ 'WU_OCI_EMAIL_DOMAIN_ENDPOINT' => [
+ 'title' => __('OCI Email API Endpoint Override', 'ultimate-multisite'),
+ 'desc' => __('Optional. Advanced use only; overrides the generated Email Delivery API endpoint.', 'ultimate-multisite'),
+ ],
+ ];
+ }
+
+ /**
+ * Returns provider-specific guidance for the configuration step.
+ *
+ * @since 2.5.0
+ * @return array{title: string, description: string, steps: arrayeu-zurich-1. The API region and SMTP region should match.', 'ultimate-multisite'),
+ __('Leave the SPF include host as rp.oracleemaildelivery.com unless Oracle provides a different include host for your tenancy.', 'ultimate-multisite'),
+ __('After testing succeeds, publish the returned SPF, DKIM, and DMARC DNS records. For existing SPF records, merge the OCI include into the single existing SPF TXT record instead of creating a second SPF record.', 'ultimate-multisite'),
+ ],
+ ];
+ }
+}
diff --git a/inc/integrations/providers/oci-email/class-oci-email-transactional-email.php b/inc/integrations/providers/oci-email/class-oci-email-transactional-email.php
new file mode 100644
index 000000000..41afffbfd
--- /dev/null
+++ b/inc/integrations/providers/oci-email/class-oci-email-transactional-email.php
@@ -0,0 +1,507 @@
+ [
+ 'domain_verify' => __('Create or locate OCI Email Delivery domains when a new domain is added to the network', 'ultimate-multisite'),
+ 'dkim_records' => __('Request DKIM for verified domains and return the DNS records required by OCI', 'ultimate-multisite'),
+ 'authentication_dns' => __('Return SPF and DMARC records alongside OCI DKIM records so DNS automation can publish a complete authentication set', 'ultimate-multisite'),
+ ],
+ 'will_not' => [
+ 'mailbox_provision' => __('Provision mailboxes or IMAP/POP3 accounts (use the Email Selling capability for that)', 'ultimate-multisite'),
+ 'dns_auto_create' => __('Automatically create DNS records unless a supported DNS provider capability is configured to consume the returned records', 'ultimate-multisite'),
+ 'smtp_credentials' => __('Create OCI SMTP credentials; those remain managed in OCI IAM for the sending user', 'ultimate-multisite'),
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function register_hooks(): void {
+
+ add_action('wu_domain_added', [$this, 'on_domain_added'], 10, 2);
+ add_action('wu_domain_removed', [$this, 'on_domain_removed'], 10, 2);
+ add_action('wu_settings_transactional_email', [$this, 'register_transactional_email_settings']);
+ }
+
+ /**
+ * Registers a status field in the Transactional Email Delivery settings section.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function register_transactional_email_settings(): void {
+
+ $region = $this->get_oci()->get_region();
+
+ wu_register_settings_field(
+ 'emails',
+ 'oracle_oci_active_region',
+ [
+ 'title' => __('Oracle OCI Email Delivery Active Region', 'ultimate-multisite'),
+ 'desc' => sprintf(
+ /* translators: %s is the OCI region identifier, e.g. eu-zurich-1. */
+ __('Outbound email domain verification is currently managed through Oracle OCI Email Delivery in the %s region.', 'ultimate-multisite'),
+ esc_html($region)
+ ),
+ 'type' => 'note',
+ ]
+ );
+ }
+
+ /**
+ * Gets the parent OCI_Email_Integration for API calls.
+ *
+ * @since 2.5.0
+ * @return OCI_Email_Integration
+ */
+ private function get_oci(): OCI_Email_Integration {
+
+ /** @var OCI_Email_Integration */
+ return $this->get_integration();
+ }
+
+ /**
+ * Handles the wu_domain_added action.
+ *
+ * @since 2.5.0
+ *
+ * @param string $domain The domain name that was added.
+ * @param int $site_id The site ID the domain was added to.
+ * @return void
+ */
+ public function on_domain_added(string $domain, int $site_id): void {
+
+ $result = $this->verify_domain($domain);
+
+ if ( ! $result['success']) {
+ wu_log_add(
+ 'integration-oracle-oci',
+ sprintf(
+ 'Failed to initiate OCI Email Delivery domain verification for "%s". Reason: %s',
+ $domain,
+ $result['message'] ?? __('Unknown error', 'ultimate-multisite')
+ ),
+ LogLevel::ERROR
+ );
+
+ return;
+ }
+
+ wu_log_add('integration-oracle-oci', sprintf('Initiated OCI Email Delivery domain verification for "%s".', $domain));
+
+ /**
+ * Fires after OCI domain verification has been initiated.
+ *
+ * @since 2.5.0
+ *
+ * @param string $domain The domain name.
+ * @param int $site_id The site ID.
+ * @param array $dns_records The DNS records that must be added to complete verification.
+ */
+ do_action('wu_domain_verified', $domain, $site_id, $result['dns_records'] ?? []);
+ }
+
+ /**
+ * Handles the wu_domain_removed action.
+ *
+ * @since 2.5.0
+ *
+ * @param string $domain The domain name that was removed.
+ * @param int $site_id The site ID the domain was removed from.
+ * @return void
+ */
+ public function on_domain_removed(string $domain, int $site_id): void {
+
+ $should_delete = apply_filters('wu_oci_delete_email_domain_on_domain_removed', false, $domain, $site_id);
+
+ if ( ! $should_delete) {
+ return;
+ }
+
+ $domain_record = $this->get_email_domain($domain);
+
+ if (is_wp_error($domain_record)) {
+ wu_log_add('integration-oracle-oci', sprintf('Failed to locate OCI email domain for "%s". Reason: %s', $domain, $domain_record->get_error_message()), LogLevel::ERROR);
+
+ return;
+ }
+
+ $domain_id = $domain_record['id'] ?? '';
+
+ if ( ! $domain_id) {
+ return;
+ }
+
+ $result = $this->get_oci()->oci_api_call('emailDomains/' . rawurlencode($domain_id), 'DELETE');
+
+ if (is_wp_error($result)) {
+ wu_log_add('integration-oracle-oci', sprintf('Failed to delete OCI email domain for "%s". Reason: %s', $domain, $result->get_error_message()), LogLevel::ERROR);
+
+ return;
+ }
+
+ wu_log_add('integration-oracle-oci', sprintf('Deleted OCI email domain for "%s".', $domain));
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $domain The domain name.
+ */
+ public function verify_domain(string $domain): array {
+
+ $email_domain = $this->get_or_create_email_domain($domain);
+
+ if (is_wp_error($email_domain)) {
+ return [
+ 'success' => false,
+ 'message' => $email_domain->get_error_message(),
+ ];
+ }
+
+ $dkim = $this->get_or_create_dkim($email_domain, $domain);
+
+ if (is_wp_error($dkim)) {
+ return [
+ 'success' => false,
+ 'message' => $dkim->get_error_message(),
+ ];
+ }
+
+ return [
+ 'success' => true,
+ 'dns_records' => $this->build_dns_records($domain, $email_domain, $dkim),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $domain The domain name.
+ */
+ public function get_domain_verification_status(string $domain): array {
+
+ $result = $this->get_email_domain($domain);
+
+ if (is_wp_error($result)) {
+ return [
+ 'success' => false,
+ 'status' => 'unknown',
+ 'message' => $result->get_error_message(),
+ ];
+ }
+
+ $status = $result['lifecycleState'] ?? $result['lifecycle_state'] ?? $result['status'] ?? 'unknown';
+
+ return [
+ 'success' => true,
+ 'status' => strtolower((string) $status),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $domain The domain name.
+ */
+ public function get_domain_dns_records(string $domain): array {
+
+ $email_domain = $this->get_email_domain($domain);
+
+ if (is_wp_error($email_domain)) {
+ return [
+ 'success' => false,
+ 'message' => $email_domain->get_error_message(),
+ ];
+ }
+
+ $dkim = $this->get_or_create_dkim($email_domain, $domain);
+
+ if (is_wp_error($dkim)) {
+ return [
+ 'success' => false,
+ 'message' => $dkim->get_error_message(),
+ ];
+ }
+
+ return [
+ 'success' => true,
+ 'dns_records' => $this->build_dns_records($domain, $email_domain, $dkim),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $from The sender email address.
+ * @param string $to The recipient email address.
+ * @param string $subject The email subject.
+ * @param string $body The email body.
+ * @param array $headers Optional headers.
+ */
+ public function send_email(string $from, string $to, string $subject, string $body, array $headers = []): array {
+
+ return [
+ 'success' => false,
+ 'message' => __('OCI Email Delivery sending is handled by the configured SMTP transport. This integration manages OCI domain, SPF, DKIM, and DMARC setup.', 'ultimate-multisite'),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $domain The domain name.
+ * @param string $period The metrics period.
+ */
+ public function get_sending_statistics(string $domain, string $period = '24h'): array {
+
+ return [
+ 'success' => true,
+ 'sent' => 0,
+ 'delivered' => 0,
+ 'bounced' => 0,
+ 'complaints' => 0,
+ 'message' => __('OCI Email Delivery delivery metrics are available in OCI Monitoring, not through this transactional email capability.', 'ultimate-multisite'),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $domain The domain name.
+ * @param int $max_per_day Maximum emails allowed per day.
+ */
+ public function set_sending_quota(string $domain, int $max_per_day): array {
+
+ return [
+ 'success' => true,
+ 'message' => __('Sending quota management is handled at the OCI tenancy/account level via Oracle Cloud Console.', 'ultimate-multisite'),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function test_connection() {
+
+ return $this->get_oci()->test_connection();
+ }
+
+ /**
+ * Create or return an existing OCI email domain.
+ *
+ * @since 2.5.0
+ *
+ * @param string $domain Domain name.
+ * @return array|\WP_Error
+ */
+ private function get_or_create_email_domain(string $domain) {
+
+ $existing = $this->get_email_domain($domain);
+
+ if ( ! is_wp_error($existing) && ! empty($existing)) {
+ return $existing;
+ }
+
+ $body = [
+ 'compartmentId' => $this->get_oci()->get_compartment_ocid(),
+ 'name' => $domain,
+ ];
+
+ $body = apply_filters('wu_oci_create_email_domain_body', $body, $domain);
+
+ return $this->get_oci()->oci_api_call('emailDomains', 'POST', $body);
+ }
+
+ /**
+ * Locate an OCI email domain by name.
+ *
+ * @since 2.5.0
+ *
+ * @param string $domain Domain name.
+ * @return array|\WP_Error
+ */
+ private function get_email_domain(string $domain) {
+
+ $result = $this->get_oci()->oci_api_call(
+ 'emailDomains?compartmentId=' . rawurlencode($this->get_oci()->get_compartment_ocid()) . '&name=' . rawurlencode($domain)
+ );
+
+ if (is_wp_error($result)) {
+ return $result;
+ }
+
+ $items = $result['items'] ?? $result;
+
+ if (isset($items['name'])) {
+ return $items;
+ }
+
+ if (is_array($items)) {
+ foreach ($items as $item) {
+ if (isset($item['name']) && strtolower($domain) === strtolower($item['name'])) {
+ return $item;
+ }
+ }
+ }
+
+ return new \WP_Error(
+ 'oracle-oci-domain-not-found',
+ sprintf(
+ /* translators: %s is a domain name. */
+ __('OCI email domain "%s" was not found.', 'ultimate-multisite'),
+ $domain
+ )
+ );
+ }
+
+ /**
+ * Create or return an existing DKIM record for an OCI email domain.
+ *
+ * @since 2.5.0
+ *
+ * @param array $email_domain OCI email domain object.
+ * @param string $domain Domain name.
+ * @return array|\WP_Error
+ */
+ private function get_or_create_dkim(array $email_domain, string $domain) {
+
+ $domain_id = $email_domain['id'] ?? '';
+
+ if ( ! $domain_id) {
+ return new \WP_Error('oracle-oci-domain-id-missing', __('OCI email domain response did not include an ID.', 'ultimate-multisite'));
+ }
+
+ $existing = $this->get_oci()->oci_api_call('emailDomains/' . rawurlencode($domain_id) . '/dkims');
+
+ if ( ! is_wp_error($existing)) {
+ $items = $existing['items'] ?? $existing;
+
+ if (isset($items['id'])) {
+ return $items;
+ }
+
+ if (is_array($items) && ! empty($items)) {
+ return reset($items);
+ }
+ }
+
+ $selector = sanitize_key(apply_filters('wu_oci_dkim_selector', 'ultimate-multisite', $domain, $email_domain));
+ $body = apply_filters('wu_oci_create_dkim_body', ['name' => $selector], $domain, $email_domain);
+
+ return $this->get_oci()->oci_api_call('emailDomains/' . rawurlencode($domain_id) . '/dkims', 'POST', $body);
+ }
+
+ /**
+ * Build DNS records from OCI Email Delivery responses.
+ *
+ * @since 2.5.0
+ *
+ * @param string $domain Domain name.
+ * @param array $email_domain OCI email domain response.
+ * @param array $dkim OCI DKIM response.
+ * @return array
@@ -18,6 +20,30 @@
+ +
+ + + +