<?php
|
namespace JVBase\integrations;
|
|
use WP_Error;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* PostMark Email Service Integration
|
*
|
* Provides integration with PostMark for transactional email delivery
|
* Hooks into WordPress's wp_mail() function for seamless integration
|
*/
|
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;
|
/**
|
* Constructor
|
*/
|
public function __construct(?int $userID = null)
|
{
|
$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';
|
|
// 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, '<html') !== false ||
|
strpos($message, '<body') !== false;
|
|
// Build PostMark payload
|
$payload = $this->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 <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;
|
}
|
|
$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' => '<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;
|
}
|
}
|