From a8ff85042ffbfe94b678fd818c02deb613bfd116 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 07 Aug 2026 20:42:54 +0000
Subject: [PATCH] =Continuing on with Integrations setup. Working on PostMark now

---
 inc/integrations/services/PostMark.php |  914 +++++++++++++++++++++-----------------------------------
 1 files changed, 348 insertions(+), 566 deletions(-)

diff --git a/inc/integrations/services/PostMark.php b/inc/integrations/services/PostMark.php
index 4f6f2ee..c8a4956 100644
--- a/inc/integrations/services/PostMark.php
+++ b/inc/integrations/services/PostMark.php
@@ -1,7 +1,12 @@
 <?php
 
 namespace JVBase\integrations\services;
+use JVBase\integrations\Admin;
+use JVBase\integrations\Instance;
 use JVBase\integrations\Integrations;
+use JVBase\integrations\Requests;
+use WP_Error;
+
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -14,123 +19,69 @@
  */
 class PostMark extends Integrations
 {
-	protected ?string $server_token = null;
-	protected string $message_stream = 'outbound';
-	protected string $from_email;
-	protected string $from_name;
-	protected bool $track_open;
-	protected bool $track_links;
-	protected ?string $lastMessageId = null;
+	use Admin, Instance, Requests;
 
-	protected static array $instances = [];
-	public static function getInstance(?int $userID = null):self
-	{
-		$key = is_null($userID) ? 'base' : $userID;
-		if (!array_key_exists($key, self::$instances)) {
-			self::$instances[$key] = new static($userID);
-		}
-		return self::$instances[$key];
-	}
-	/**
-	 * Constructor
-	 */
-	protected function __construct(?int $userID = null)
+	protected function __construct()
 	{
 		$this->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';
+		$this->baseUrl = 'https://api.postmarkapp.com';
 
-		// 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'
-		];
+		$this->defineFields();
+		$this->defineInstructions();
 
-		// 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' => [
+		parent::__construct();
+		$this->defineActions();
+	}
+
+		protected function defineFields():void
+		{
+			$token = $this->addField('service_token', 'Server API Token', 'text', 'manage_options');
+				$token->setSubType('password');
+
+			$stream = $this->addField('message_stream', 'Message Stream ID', 'text', 'manage_options');
+				$stream->setDefault('outbound');
+				$stream->setHint('Message stream to use (default: outbound)');
+
+			$from = $this->addField('from_email', 'From Email', 'email', 'manage_options');
+				$from->setPlaceholder('noreply@yourdomain.com');
+				$from->setDefault(get_option('admin_email'));
+
+			$name = $this->addField('from_name', 'From Name', 'text', 'manage_options');
+				$name->setDefault(get_bloginfo('name'));
+				$name->setHint('Default sender name');
+
+			$opens = $this->addField('track_opens', 'Track Opens', 'select', 'manage_options');
+				$opens->setOptions([
+					'false'	=> 'Disabled',
+					'true'	=> 'Enabled',
+				]);
+				$opens->setDefault('false');
+				$opens->setHint('Track when emails are opened');
+
+			$links = $this->addField('track_links', 'Track Links', 'select', 'manage_options');
+				$links->setOptions([
 					'None' => 'Disabled',
 					'HtmlOnly' => 'HTML Only',
 					'HtmlAndText' => 'HTML and Text',
-				],
-				'default' => 'None',
-				'description' => 'Track link clicks in emails'
-			]
-		];
-
-		parent::__construct();
-
-		// Hook into WordPress mail system if integration is healthy
-		if ($this->is_healthy && $this->isSetUp()) {
-			$this->initializeMailHooks();
+				]);
+				$links->setDefault('None');
+				$links->setHint('Track link clicks in emails');
 		}
-	}
+		protected function defineInstructions():void
+		{
 
-	/**
-	 * Initialize WordPress mail hooks
-	 */
-	protected function initializeMailHooks(): void
+		}
+		protected function defineActions():void
+		{
+
+		}
+	protected function initialize(): void
 	{
 		// Replace wp_mail with PostMark
-		add_filter('wp_mail', [$this, 'sendViaPostMark'], 10, 1);
+		add_filter('wp_mail', [$this, 'wp_mail'], 10, 1);
 
 		// Optional: Hook for logging sent emails
 		add_action('postmark_email_sent', [$this, 'logSentEmail'], 10, 2);
@@ -138,289 +89,224 @@
 	}
 
 	/**
-	 * 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)
+	 * Fired on the wp_mail filter
+	 * @param array $args Email arguments from wp_mail, including 'to', 'subject', 'message', 'headers', 'attachments'
+	 * @return array
 	 */
-	public function sendViaPostMark(array $args): array
-	{
-		// Extract email components
-		$to = $args['to'];
-		$subject = $args['subject'];
-		$message = $args['message'];
-		$headers = $args['headers'] ?? '';
-		$attachments = $args['attachments'] ?? [];
+		public function wp_mail(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);
+			$headers = $this->parseMailHeaders($headers);
 
-		// Determine content type
-		$is_html = $parsed_headers['content-type'] === 'text/html' ||
-			strpos($message, '<html') !== false ||
-			strpos($message, '<body') !== false;
+			$is_html = $headers['content-type'] === 'text/html' || str_contains($message, '<html') || str_contains($message, '<body');
 
-		// Build PostMark payload
-		$payload = $this->buildEmailPayload(
-			$to,
-			$subject,
-			$message,
-			$is_html,
-			$parsed_headers,
-			$attachments
-		);
-
-		// Send via PostMark
-		$result = $this->sendEmail($payload);
-
-		if ($result === true) {
-			error_log('================================ Email sent! ================================');
-			// Prevent default wp_mail from sending
-			add_filter('pre_wp_mail', '__return_true');
-			do_action('postmark_email_sent', $args, $payload);
-		} else {
-			error_log('=-======================[POSTMARK]Something went wrong... ================================');
-			// 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);
-			error_log('================================ POSTMARK RESPONSE: ================================'.print_r($response, true));
-			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()
+			$response = $this->sendEmail(
+				$to,
+				$subject,
+				$message,
+				$is_html,
+				$headers,
+				$attachments
 			);
-		}
-	}
 
-	/**
-	 * Parse mail headers from various formats
-	 */
-	protected function parseMailHeaders($headers): array
+			if ($response['success']) {
+				error_log('================================ Email sent! ================================');
+				// Prevent default wp_mail from sending
+				add_filter('pre_wp_mail', '__return_true');
+				do_action('postmark_email_sent', $args, $response);
+			} else {
+				error_log('=-======================[POSTMARK]Something went wrong... ================================');
+				// Log failure but allow fallback to default mail
+				do_action('postmark_email_failed', $args, $response);
+			}
+			return $args;
+		}
+
+			protected function parseMailHeaders(string|array $headers):array
+			{
+				$parsed = [
+					'content-type'	=> 'text/plain',
+					'from'			=> null,
+					'from-name'	=> null,
+					'reply-top'		=> null,
+					'cc'			=> [],
+					'bcc'			=> [],
+					'x-headers'		=> [],
+				];
+
+				if (empty($headers)) {
+					return $parsed;
+				}
+
+				if (is_string($headers)) {
+					$headers = explode("\n", $headers);
+				}
+
+				foreach ($headers as $line) {
+					$this->parseHeaderLine($line, $parsed);
+				}
+
+				return $parsed;
+			}
+				protected function parseHeaderLine(string $line, array &$parsed):void
+				{
+					if (empty($line)) {
+						return;
+					}
+					$parts = explode(':', $line, 2);
+					if (count($parts) !== 2) {
+						return;
+					}
+
+					$name = strtolower(trim($parts[0]));
+					$value = trim($parts[1]);
+					switch ($name) {
+						case 'content-type':
+							if (str_contains($value, 'text/html')) {
+								$parsed['content-type'] = $value;
+							}
+							break;
+
+						case 'from':
+							// Parse "Name <email>" format
+							if (preg_match('/^(.+?)\s*<(.+)>$/', $value, $matches)) {
+								$parsed['from-name'] = trim($matches[1], '"\'');
+								$parsed['from'] = $matches[2];
+							} else {
+								$parsed['from'] = $value;
+							}
+							break;
+
+						case 'cc':
+						case 'bcc':
+							$parsed[$name][] = $value;
+							break;
+
+						default:
+							//TODO: Verify we need this. This feels extra clunky
+							//Why not just $parsed['x-headers'][$name] = $value?
+							if (str_contains($name, 'x-')) {
+								$parsed['x-headers'][] = [
+									'Name'	=> $name,
+									'Value'	=> $value
+								];
+							} else {
+								$parsed[$name] = $value;
+							}
+					}
+				}
+
+	protected function getRequestHeaders(): array
 	{
-		$parsed = [
-			'content-type' => 'text/plain',
-			'from' => null,
-			'from-name' => null,
-			'reply-to' => null,
-			'cc' => [],
-			'bcc' => [],
-			'x-headers' => []
+		return [
+			'Accept' => 'application/json',
+			'Content-Type' => 'application/json',
+			'X-Postmark-Server-Token' => $this->getServerToken()
 		];
-
-		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
+	protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
 	{
-		if (empty($line)) {
-			return;
+		if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') {
+			return $this->response(false, 'Rate limit exceeded');
 		}
-
-		// 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 <email>" 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;
+		$endpoint = str_replace($this->apiBase, '', $endpoint);
+		$error = $response['ErrorCode'] ?? false;
+		if ($error) {
+			switch ($error) {
+				case '0':
+					//Do nothing, no error found
+					break;
+				//Bad or missing API Token
+				case '10':
+				//Multiple errors
+				case '11':
+				//Resource Not found
+				case '12':
+				//Invalid Pagination Key
+				case '13':
+				//Maintenance; PostMark API down for a sec
+				case '100':
+				//Invalid Email Request
+				case '300':
+				//Sender Signature not found
+				case '400':
+				//Sender signature not confirmed
+				case '401':
+				//Invalid JSON
+				case '402':
+				//Invalid request field(s) - {FIELD_NAME}
+				case '403':
+				//Not allowed to send (no more credits in account)
+				case '405':
+				//Inactive recipient. Recipient hard bounces, or filed spam complaint
+				case '406':
+				//JSON required
+				case '409':
+				//Too many requests (500 max)
+				case '410':
+				//Forbidden Attachment type
+				case '411':
+				//Account is pending
+				case '412':
+				//Account may not exist
+				case '413':
+				//Invalid query string parameter
+				case '500':
+				default:
+					$this->logError(
+						'handleResponse',
+						'Something went wrong: '.$response['Message']??'',
+						$response
+					);
+					return $this->response(false, 'Something went wrong: '.$response['Message']??'');
 			}
-
-			$processed[] = [
-				'Name' => basename($file),
-				'Content' => base64_encode(file_get_contents($file)),
-				'ContentType' => mime_content_type($file) ?: 'application/octet-stream'
-			];
 		}
-
-		return $processed;
+		return match ($endpoint) {
+			'/email' => $this->handleEmailResponse($response, $method),
+			'/email/bulk'	=> $this->handleBulkEmailResponse($response, $method),
+			default => $this->response(false, 'Unhandled Response for '. $endpoint . ' endpoint, with method: '. $method),
+		};
 	}
 
-	/**
-	 * Generate plain text from HTML content
-	 */
-	protected function generateTextFromHtml(string $html): string
+	/**********************************************************
+	 * UTILITY
+	**********************************************************/
+	protected function getServerToken():string|false
+	{
+		return $this->loadCredentials()['server_token']??false;
+	}
+	protected function getMessageStream():string
+	{
+		return $this->loadCredentials()['message_stream']??'outbound';
+	}
+	protected function getFromEmail():string
+	{
+		return $this->loadCredentials()['from_email']??get_option('admin_email');
+	}
+	protected function getFromName():string
+	{
+		return $this->loadCredentials()['from_name']??get_bloginfo('name');
+	}
+	protected function getTrackOpens():string
+	{
+		return $this->loadCredentials()['track_opens']??'false';
+	}
+	protected function getTrackLinks():string
+	{
+		return $this->loadCredentials()['track_links']??'None';
+	}
+
+	/****************************************************************************
+	 * HELPERS
+	****************************************************************************/
+	protected function textFromHtml(string $html):string
 	{
 		// Remove HTML tags
 		$text = strip_tags($html);
@@ -434,228 +320,124 @@
 	}
 
 	/**
-	 * Determine email tag based on content
+	 * @param array $attachments Either an array of file paths, or attachment IDs
+	 * @return array
 	 */
-	protected function determineEmailTag(string $subject, array $headers): string
+	protected function processAttachments(array $attachments):array
 	{
-		// Password reset emails
-		if (stripos($subject, 'password') !== false && stripos($subject, 'reset') !== false) {
+		$processed = [];
+		foreach ($attachments as $file) {
+			if (is_int($file)) {
+				$file = get_attached_file($file);
+			}
+			if (!$file || !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;
+	}
+
+	protected function determineEmailTag(string $subject, array $headers):string
+	{
+		if (array_key_exists('email-tag', $headers)) {
+			return $headers['email-tag'];
+		}
+		$tmp = strtolower($subject);
+		if (str_contains($tmp, 'password')) {
 			return 'password-reset';
 		}
 
-		// New user registration
-		if (stripos($subject, 'welcome') !== false || stripos($subject, 'registration') !== false) {
+		if (str_contains($tmp, 'welcome') || str_contains($tmp, 'registr') || str_contains($tmp, 'register')) {
 			return 'registration';
 		}
 
-		// Account notifications
-		if (stripos($subject, 'account') !== false) {
+		if (str_contains($tmp, 'account')) {
 			return 'account';
 		}
 
-		// Order/transaction emails
-		if (stripos($subject, 'order') !== false || stripos($subject, 'receipt') !== false) {
+		if (str_contains($tmp, 'order') || str_contains($tmp, 'receipt')) {
 			return 'transaction';
 		}
 
-		// Default
 		return 'general';
 	}
 
-	/**
-	 * Check if should fallback to default mail
-	 */
-	protected function shouldFallback(): bool
+	/****************************************************************************
+	 * MAILERS
+	****************************************************************************/
+	public function sendEmail(string $to, string $subject, string $message, bool $is_html = true, array $headers = [], array $attachments = []):array
 	{
-		// Don't fallback if we've had too many errors
-		if ($this->error_stats['consecutive_errors'] >= 3) {
-			return false;
+		if (!$this->isSetUp()) {
+			return $this->response(false, 'Not set up');
 		}
 
-		// Check if fallback is enabled in settings
-		$credentials = $this->getCredentials();
-		return ($credentials['enable_fallback'] ?? 'true') === 'true';
+		$payload = $this->buildPayload($to, $subject, $message, $is_html, $headers, $attachments);
+
+		$response = $this->postRequest(
+			sprintf('%s/email', $this->apiBase),
+			$payload
+		);
 	}
+		protected function handleEmailResponse(WP_Error|array $response, string $method):array
+		{
 
-	/**
-	 * 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;
 		}
+		protected function buildPayload(string|array $to, string $subject, string $message, bool $is_html = true, array $headers = [], array $attachments = []):array
+		{
+			$to = is_array($to) ? implode(',',$to) : $to;
 
-		try {
-			$stats = $this->getRequest('stats/outbound', [
-				'fromdate' => date('Y-m-d', strtotime("-{$days} days")),
-				'todate' => date('Y-m-d')
-			]);
+			$payload = [
+				'From'	=> sprintf(
+					'%s <%s>',
+					$headers['from-name'] ?? $this->getFromName(),
+					$headers['from'] ?? $this->getFromEmail()
+				),
+				'To'			=> $to,
+				'Subject'		=> sanitize_text_field($subject),
+				'TrackOpens'	=> $this->getTrackOpens() === 'true',
+				'TrackLinks'	=> $this->getTrackLinks(),
+				'MessageStream'	=> $this->getMessageStream()
+			];
 
-			if (!is_wp_error($stats)) {
-				$this->cache->set($cache_key, $stats, 3600); // Cache for 1 hour
-				return $stats;
+			if ($is_html) {
+				$payload['HtmlBody'] = $message;
+				$payload['TextBody'] = $this->textFromHtml($message);
+			} else {
+				$payload['TextBody'] = $message;
 			}
-		} catch (\Exception $e) {
-			$this->logError('stats/outbound', ['error' => $e->getMessage()]);
+
+			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'];
+			}
+
+			if (!empty($headers['reply-to'])) {
+				$payload['ReplyTo'] = $headers['reply-to'];
+			}
+
+			if (!empty($attachments)) {
+				$payload['Attachments'] = $this->processAttachments($attachments);
+			}
+
+			if (!empty($headers['x-headers'])) {
+				$payload['Headers'] = $headers['x-headers'];
+			}
+
+			$payload['Tag'] = $this->determineEmailTag($subject, $headers);
+
+			return $payload;
 		}
-
-		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' => '<p>This is a <strong>test email</strong> from your PostMark integration.</p>',
-			'Tag' => 'test',
-			'TrackOpens' => false,
-			'TrackLinks' => 'None'
-		];
-
-		return $this->sendEmail($payload);
-	}
-
-	/**
-	 * Render additional options in dashboard
-	 */
-	public function renderAdditionalOptions(): void
-	{
-		$stats = $this->getEmailStats(7);
-
-		?>
-		<div class="postmark-dashboard">
-			<h3>Email Statistics (Last 7 Days)</h3>
-
-			<?php if (!empty($stats)): ?>
-				<div class="stats-grid">
-					<div class="stat-item">
-						<span class="stat-value"><?= number_format($stats['Sent'] ?? 0) ?></span>
-						<span class="stat-label">Emails Sent</span>
-					</div>
-					<div class="stat-item">
-						<span class="stat-value"><?= number_format($stats['Bounced'] ?? 0) ?></span>
-						<span class="stat-label">Bounced</span>
-					</div>
-					<div class="stat-item">
-						<span class="stat-value"><?= $stats['Opens'] ?? 0 ?>%</span>
-						<span class="stat-label">Open Rate</span>
-					</div>
-				</div>
-			<?php else: ?>
-				<p>No statistics available. Check your API configuration.</p>
-			<?php endif; ?>
-
-			<div class="test-email-section">
-				<h4>Send Test Email</h4>
-				<form id="postmark-test-email" method="post">
-					<input type="email"
-						   name="test_email"
-						   placeholder="test@example.com"
-						   value="<?= esc_attr(wp_get_current_user()->user_email) ?>"
-						   required>
-					<button type="submit" class="button">Send Test</button>
-				</form>
-			</div>
-
-			<?php if ($bounces = $this->getBounces(5)): ?>
-				<div class="recent-bounces">
-					<h4>Recent Bounces</h4>
-					<ul>
-						<?php foreach ($bounces as $bounce): ?>
-							<li>
-								<?= esc_html($bounce['Email']) ?> -
-								<?= esc_html($bounce['Type']) ?>
-								<small>(<?= esc_html($bounce['BouncedAt']) ?>)</small>
-							</li>
-						<?php endforeach; ?>
-					</ul>
-				</div>
-			<?php endif; ?>
-		</div>
-		<?php
-	}
-
-	protected function initialize(): void
-	{
-		if (empty($this->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;
-	}
 }

--
Gitblit v1.10.0