service_name = 'postmark'; $this->title = 'PostMark'; $this->icon = 'envelope-simple'; $this->cacheName = ($userID)? 'gmb_'.$userID : 'postmark'; // PostMark API configuration $this->apiBase = 'https://api.postmarkapp.com'; $this->apiVersion = 'v1'; // Define available endpoints $this->apiEndpoints = [ 'email', 'email/batch', 'email/withTemplate', 'bounces', 'deliverystats', 'messages/outbound', 'messages/outbound/[^/]+', 'messages/outbound/opens', 'stats/outbound', 'stats/outbound/opens', 'stats/outbound/bounces', 'templates', 'templates/[^/]+', 'server' ]; // Required credentials $this->fields = [ 'server_token' => [ 'label' => 'Server API Token', 'type' => 'text', 'subtype'=> 'password', 'placeholder' => 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 'hint' => 'Your PostMark Server API Token' ], 'message_stream' => [ 'label' => 'Message Stream ID', 'type' => 'text', 'placeholder' => 'outbound', 'default' => 'outbound', 'description' => 'Message stream to use (default: outbound)' ], 'from_email' => [ 'label' => 'From Email', 'type' => 'email', 'placeholder' => 'noreply@yourdomain.com', 'description' => 'Default sender email address (must be verified in PostMark)' ], 'from_name' => [ 'label' => 'From Name', 'type' => 'text', 'placeholder' => get_bloginfo('name'), 'default' => get_bloginfo('name'), 'description' => 'Default sender name' ], 'track_opens' => [ 'label' => 'Track Opens', 'type' => 'select', 'options' => [ 'false' => 'Disabled', 'true' => 'Enabled' ], 'default' => 'false', 'description' => 'Track when emails are opened' ], 'track_links' => [ 'label' => 'Track Links', 'type' => 'select', 'options' => [ 'None' => 'Disabled', 'HtmlOnly' => 'HTML Only', 'HtmlAndText' => 'HTML and Text', ], 'default' => 'None', 'description' => 'Track link clicks in emails' ] ]; parent::__construct($userID); // Hook into WordPress mail system if integration is healthy if ($this->is_healthy && $this->isSetUp()) { $this->initializeMailHooks(); } } /** * Initialize WordPress mail hooks */ protected function initializeMailHooks(): void { // Replace wp_mail with PostMark add_filter('wp_mail', [$this, 'sendViaPostMark'], 10, 1); // Optional: Hook for logging sent emails add_action('postmark_email_sent', [$this, 'logSentEmail'], 10, 2); add_action('postmark_email_failed', [$this, 'logFailedEmail'], 10, 2); } /** * Send email via PostMark API * Hooks into wp_mail filter * * @param array $args Email arguments from wp_mail * @return array Modified args (or original if sending via PostMark) */ public function sendViaPostMark(array $args): array { // Extract email components $to = $args['to']; $subject = $args['subject']; $message = $args['message']; $headers = $args['headers'] ?? ''; $attachments = $args['attachments'] ?? []; // Parse headers $parsed_headers = $this->parseMailHeaders($headers); // Determine content type $is_html = $parsed_headers['content-type'] === 'text/html' || strpos($message, 'buildEmailPayload( $to, $subject, $message, $is_html, $parsed_headers, $attachments ); // Send via PostMark $result = $this->sendEmail($payload); if ($result === true) { // Prevent default wp_mail from sending add_filter('pre_wp_mail', '__return_true'); do_action('postmark_email_sent', $args, $payload); } else { // Log failure but allow fallback to default mail do_action('postmark_email_failed', $args, $result); // Optionally fall back to default wp_mail if ($this->shouldFallback()) { return $args; } } return $args; } /** * Build email payload for PostMark API */ protected function buildEmailPayload( $to, string $subject, string $message, bool $is_html, array $headers, array $attachments ): array { $credentials = $this->getCredentials(); // Handle multiple recipients $to_addresses = is_array($to) ? implode(',', $to) : $to; // Build base payload $payload = [ 'From' => sprintf( '%s <%s>', $headers['from-name'] ?? $credentials['from_name'], $headers['from'] ?? $credentials['from_email'] ), 'To' => $to_addresses, 'Subject' => $subject, 'TrackOpens' => $credentials['track_opens'] === 'true', 'TrackLinks' => $credentials['track_links'], 'MessageStream' => $credentials['message_stream'] ?? 'outbound' ]; // Set content based on type if ($is_html) { $payload['HtmlBody'] = $message; // Generate text version from HTML $payload['TextBody'] = $this->generateTextFromHtml($message); } else { $payload['TextBody'] = $message; } // Add CC/BCC if present if (!empty($headers['cc'])) { $payload['Cc'] = is_array($headers['cc']) ? implode(',', $headers['cc']) : $headers['cc']; } if (!empty($headers['bcc'])) { $payload['Bcc'] = is_array($headers['bcc']) ? implode(',', $headers['bcc']) : $headers['bcc']; } // Add Reply-To if present if (!empty($headers['reply-to'])) { $payload['ReplyTo'] = $headers['reply-to']; } // Handle attachments if (!empty($attachments)) { $payload['Attachments'] = $this->processAttachments($attachments); } // Add custom headers if needed if (!empty($headers['x-headers'])) { $payload['Headers'] = $headers['x-headers']; } // Add tags for tracking (optional) $payload['Tag'] = $this->determineEmailTag($subject, $headers); return $payload; } /** * Send email via PostMark API */ protected function sendEmail(array $payload): bool|WP_Error { if (!$this->isSetUp()) { return false; } try { $response = $this->postRequest('email', $payload); if (is_wp_error($response)) { return $response; } // Store message ID for tracking if (!empty($response['MessageID'])) { $this->lastMessageId = $response['MessageID']; // Cache for potential status checking $this->cache->set( 'postmark_message_' . $response['MessageID'], $payload, 3600 // 1 hour ); } return true; } catch (\Exception $e) { $this->logError('[POSTMARK]', ['method' => 'sendEmail', 'error' => $e->getMessage()]); return new WP_Error( 'postmark_send_failed', $e->getMessage() ); } } /** * Parse mail headers from various formats */ protected function parseMailHeaders($headers): array { $parsed = [ 'content-type' => 'text/plain', 'from' => null, 'from-name' => null, 'reply-to' => null, 'cc' => [], 'bcc' => [], 'x-headers' => [] ]; if (empty($headers)) { return $parsed; } // Handle array format if (is_array($headers)) { foreach ($headers as $header) { $this->parseHeaderLine($header, $parsed); } } else { // Handle string format (multiple lines) $lines = explode("\n", $headers); foreach ($lines as $line) { $this->parseHeaderLine($line, $parsed); } } return $parsed; } /** * Parse individual header line */ protected function parseHeaderLine(string $line, array &$parsed): void { if (empty($line)) { return; } // Split header name and value $parts = explode(':', $line, 2); if (count($parts) !== 2) { return; } $name = strtolower(trim($parts[0])); $value = trim($parts[1]); switch ($name) { case 'content-type': if (strpos($value, 'text/html') !== false) { $parsed['content-type'] = 'text/html'; } break; case 'from': // Parse "Name " format if (preg_match('/^(.+?)\s*<(.+)>$/', $value, $matches)) { $parsed['from-name'] = trim($matches[1], '"\''); $parsed['from'] = $matches[2]; } else { $parsed['from'] = $value; } break; case 'reply-to': $parsed['reply-to'] = $value; break; case 'cc': $parsed['cc'][] = $value; break; case 'bcc': $parsed['bcc'][] = $value; break; default: // Store custom headers if (strpos($name, 'x-') === 0) { $parsed['x-headers'][] = [ 'Name' => $parts[0], // Original case 'Value' => $value ]; } } } /** * Process attachments for PostMark */ protected function processAttachments(array $attachments): array { $processed = []; foreach ($attachments as $file) { if (!file_exists($file)) { continue; } $processed[] = [ 'Name' => basename($file), 'Content' => base64_encode(file_get_contents($file)), 'ContentType' => mime_content_type($file) ?: 'application/octet-stream' ]; } return $processed; } /** * Generate plain text from HTML content */ protected function generateTextFromHtml(string $html): string { // Remove HTML tags $text = strip_tags($html); // Convert entities $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); // Clean up whitespace $text = preg_replace('/\s+/', ' ', $text); $text = trim($text); return $text; } /** * Determine email tag based on content */ protected function determineEmailTag(string $subject, array $headers): string { // Password reset emails if (stripos($subject, 'password') !== false && stripos($subject, 'reset') !== false) { return 'password-reset'; } // New user registration if (stripos($subject, 'welcome') !== false || stripos($subject, 'registration') !== false) { return 'registration'; } // Account notifications if (stripos($subject, 'account') !== false) { return 'account'; } // Order/transaction emails if (stripos($subject, 'order') !== false || stripos($subject, 'receipt') !== false) { return 'transaction'; } // Default return 'general'; } /** * Check if should fallback to default mail */ protected function shouldFallback(): bool { // Don't fallback if we've had too many errors if ($this->error_stats['consecutive_errors'] >= 3) { return false; } // Check if fallback is enabled in settings $credentials = $this->getCredentials(); return ($credentials['enable_fallback'] ?? 'true') === 'true'; } /** * Override makeRequest to add PostMark headers */ protected function getRequestHeaders(): array { $credentials = $this->getCredentials(); // Add PostMark specific headers return [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'X-Postmark-Server-Token' => $credentials['server_token'] ?? '' ]; } /** * Get email statistics */ public function getEmailStats(int $days = 30): array { $cache_key = 'postmark_stats_' . $days; $cached = $this->cache->get($cache_key); if ($cached !== false) { return $cached; } try { $stats = $this->getRequest('stats/outbound', [ 'fromdate' => date('Y-m-d', strtotime("-{$days} days")), 'todate' => date('Y-m-d') ]); if (!is_wp_error($stats)) { $this->cache->set($cache_key, $stats, 3600); // Cache for 1 hour return $stats; } } catch (\Exception $e) { $this->logError('stats/outbound', ['error' => $e->getMessage()]); } return []; } /** * Get bounce information */ public function getBounces(int $count = 25, int $offset = 0): array { try { return $this->getRequest('bounces', [ 'count' => $count, 'offset' => $offset ]); } catch (\Exception $e) { $this->logError('[POSTMARK]', ['method' => 'getBounces', 'error' => $e->getMessage()]); return []; } } /** * Log sent emails for debugging/tracking */ public function logSentEmail(array $args, array $payload): void { if (!defined('WP_DEBUG') || !WP_DEBUG) { return; } $this->logDebug('Email sent via PostMark', [ 'to' => $args['to'], 'subject' => $args['subject'], 'message_id' => $this->lastMessageId ?? 'unknown', 'tag' => $payload['Tag'] ?? 'none' ]); } /** * Log failed email attempts */ public function logFailedEmail(array $args, $error): void { $error_message = is_wp_error($error) ? $error->get_error_message() : 'Unknown error'; $this->logError('Failed to send email via PostMark', [ 'to' => $args['to'], 'subject' => $args['subject'], 'error' => $error_message ]); } /** * Test email configuration */ public function sendTestEmail(string $to_email): bool|WP_Error { $payload = [ 'From' => $this->getCredentials()['from_email'] . ' <' . $this->getCredentials()['from_email'] . '>', 'To' => $to_email, 'Subject' => 'PostMark Test Email - ' . get_bloginfo('name'), 'TextBody' => 'This is a test email from your PostMark integration.', 'HtmlBody' => '

This is a test email from your PostMark integration.

', 'Tag' => 'test', 'TrackOpens' => false, 'TrackLinks' => 'None' ]; return $this->sendEmail($payload); } /** * Render additional options in dashboard */ public function renderAdditionalOptions(): void { $stats = $this->getEmailStats(7); ?>

Email Statistics (Last 7 Days)

Emails Sent
Bounced
% Open Rate

No statistics available. Check your API configuration.

Send Test Email

getBounces(5)): ?>

Recent Bounces

  • - ()
credentials)) { $this->loadCredentials(); } $this->server_token = (array_key_exists('server_token', $this->credentials)) ? $this->credentials['server_token'] : null; $this->from_email = (array_key_exists('from_email', $this->credentials)) ? $this->credentials['from_email'] : get_bloginfo('admin_email'); $this->from_name = (array_key_exists('from_name', $this->credentials)) ? $this->credentials['from_name'] : get_bloginfo('name'); $this->message_stream = (array_key_exists('message_stream', $this->credentials)) ? $this->credentials['message_stream'] : 'outbound'; $this->track_open = (array_key_exists('track_open', $this->credentials)) ? $this->credentials['track_open'] : false; $this->track_links = (array_key_exists('track_links', $this->credentials)) ? $this->credentials['track_links'] : false; } }