From 1adb505446132cfeadc0a030610c4df750833ee2 Mon Sep 17 00:00:00 2001 From: dchiamp Date: Mon, 8 Sep 2025 15:38:55 -0700 Subject: [PATCH 1/3] adds email endpoint --- EMAIL_API_EXAMPLES.md | 329 +++++++++++++++++++++++ includes/class-plugin.php | 1 + includes/class-rest-email-controller.php | 303 +++++++++++++++++++++ 3 files changed, 633 insertions(+) create mode 100644 EMAIL_API_EXAMPLES.md create mode 100644 includes/class-rest-email-controller.php diff --git a/EMAIL_API_EXAMPLES.md b/EMAIL_API_EXAMPLES.md new file mode 100644 index 0000000..09741b1 --- /dev/null +++ b/EMAIL_API_EXAMPLES.md @@ -0,0 +1,329 @@ +# Fuxt API Email Endpoint Examples + +The Fuxt API now includes a REST endpoint for sending emails. This document provides examples of how to use the email functionality. + +## Endpoint + +**POST** `/wp-json/fuxt/v1/email` + +## Basic Usage + +### Simple Text Email + +```bash +curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ + -H "Content-Type: application/json" \ + -d '{ + "to": "recipient@example.com", + "subject": "Test Email", + "message": "This is a test email from the Fuxt API." + }' +``` + +### HTML Email + +```bash +curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ + -H "Content-Type: application/json" \ + -d '{ + "to": "recipient@example.com", + "subject": "HTML Test Email", + "message": "

Hello World!

This is an HTML email.

", + "is_html": true + }' +``` + +### Email with Custom Sender + +```bash +curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ + -H "Content-Type: application/json" \ + -d '{ + "to": "recipient@example.com", + "subject": "Email from Custom Sender", + "message": "This email is sent from a custom sender.", + "from_name": "John Doe", + "from_email": "john@example.com", + "reply_to": "noreply@example.com" + }' +``` + +### Email with CC and BCC + +```bash +curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ + -H "Content-Type: application/json" \ + -d '{ + "to": "recipient@example.com", + "subject": "Email with CC and BCC", + "message": "This email has CC and BCC recipients.", + "cc": "cc1@example.com, cc2@example.com", + "bcc": "bcc@example.com" + }' +``` + +### Email with Attachments + +```bash +curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ + -H "Content-Type: application/json" \ + -d '{ + "to": "recipient@example.com", + "subject": "Email with Attachments", + "message": "Please find the attached files.", + "attachments": [ + "/path/to/local/file.pdf", + "https://example.com/remote-file.jpg" + ] + }' +``` + +### Email with Custom Headers + +```bash +curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ + -H "Content-Type: application/json" \ + -d '{ + "to": "recipient@example.com", + "subject": "Email with Custom Headers", + "message": "This email has custom headers.", + "headers": { + "X-Priority": "1", + "X-MSMail-Priority": "High", + "Importance": "high" + } + }' +``` + +## JavaScript/Fetch Examples + +### Basic JavaScript Example + +```javascript +const sendEmail = async (emailData) => { + try { + const response = await fetch('/wp-json/fuxt/v1/email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(emailData) + }); + + const result = await response.json(); + + if (result.success) { + console.log('Email sent successfully:', result.message); + } else { + console.error('Failed to send email:', result.message); + } + + return result; + } catch (error) { + console.error('Error sending email:', error); + throw error; + } +}; + +// Usage +sendEmail({ + to: 'recipient@example.com', + subject: 'Test Email', + message: 'This is a test email from JavaScript.' +}); +``` + +### React Hook Example + +```javascript +import { useState } from 'react'; + +const useEmail = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const sendEmail = async (emailData) => { + setLoading(true); + setError(null); + + try { + const response = await fetch('/wp-json/fuxt/v1/email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(emailData) + }); + + const result = await response.json(); + + if (!result.success) { + throw new Error(result.message); + } + + return result; + } catch (err) { + setError(err.message); + throw err; + } finally { + setLoading(false); + } + }; + + return { sendEmail, loading, error }; +}; + +// Usage in component +const ContactForm = () => { + const { sendEmail, loading, error } = useEmail(); + + const handleSubmit = async (formData) => { + try { + await sendEmail({ + to: 'contact@example.com', + subject: 'New Contact Form Submission', + message: `Name: ${formData.name}\nEmail: ${formData.email}\nMessage: ${formData.message}`, + from_name: formData.name, + from_email: formData.email, + reply_to: formData.email + }); + alert('Message sent successfully!'); + } catch (err) { + alert('Failed to send message: ' + err.message); + } + }; + + return ( +
+ {/* form fields */} +
+ ); +}; +``` + +## PHP Examples + +### Using WordPress HTTP API + +```php +function send_email_via_api($email_data) { + $response = wp_remote_post('https://yoursite.com/wp-json/fuxt/v1/email', [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode($email_data), + 'timeout' => 30, + ]); + + if (is_wp_error($response)) { + return false; + } + + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + return $data['success'] ?? false; +} + +// Usage +$result = send_email_via_api([ + 'to' => 'recipient@example.com', + 'subject' => 'Test Email from PHP', + 'message' => 'This email was sent using the Fuxt API from PHP.', + 'from_name' => 'WordPress Site', + 'from_email' => 'noreply@yoursite.com' +]); +``` + +## Response Format + +### Success Response + +```json +{ + "success": true, + "message": "Email sent successfully.", + "data": { + "to": "recipient@example.com", + "subject": "Test Email" + } +} +``` + +### Error Response + +```json +{ + "success": false, + "message": "Failed to send email.", + "data": {} +} +``` + +## Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `to` | string | Yes | - | Email address to send to | +| `subject` | string | Yes | - | Email subject | +| `message` | string | Yes | - | Email message content | +| `from_name` | string | No | Site name | Sender name | +| `from_email` | string | No | Admin email | Sender email address | +| `reply_to` | string | No | from_email | Reply-to email address | +| `cc` | string | No | - | CC email addresses (comma separated) | +| `bcc` | string | No | - | BCC email addresses (comma separated) | +| `headers` | object | No | - | Additional email headers as key-value pairs | +| `attachments` | array | No | - | File attachments (array of file paths or URLs) | +| `is_html` | boolean | No | false | Whether the message is HTML content | + +## Security Considerations + +1. **Permissions**: By default, the endpoint allows public access. You can restrict access using the `fuxt_api_email_permissions` filter: + +```php +add_filter('fuxt_api_email_permissions', function($allowed, $request) { + // Only allow logged-in users + return is_user_logged_in(); +}, 10, 2); +``` + +2. **Rate Limiting**: Consider implementing rate limiting to prevent abuse. + +3. **Input Validation**: All inputs are sanitized, but you may want to add additional validation based on your needs. + +## Filters + +### fuxt_api_email_data + +Filter the email data before sending: + +```php +add_filter('fuxt_api_email_data', function($email_data, $request) { + // Add custom logic here + $email_data['message'] = 'Prefix: ' . $email_data['message']; + return $email_data; +}, 10, 2); +``` + +### fuxt_api_email_response + +Filter the response data: + +```php +add_filter('fuxt_api_email_response', function($response_data, $request, $sent) { + // Add custom response data + $response_data['timestamp'] = current_time('mysql'); + return $response_data; +}, 10, 3); +``` + +### fuxt_api_email_permissions + +Control access to the email endpoint: + +```php +add_filter('fuxt_api_email_permissions', function($allowed, $request) { + // Custom permission logic + return current_user_can('manage_options'); +}, 10, 2); +``` diff --git a/includes/class-plugin.php b/includes/class-plugin.php index 5fc9279..0033fb0 100644 --- a/includes/class-plugin.php +++ b/includes/class-plugin.php @@ -24,6 +24,7 @@ public function init() { ( new REST_Acf_Controller() )->init(); ( new REST_Posts_Controller() )->init(); ( new REST_User_Controller() )->init(); + ( new REST_Email_Controller() )->init(); $this->update_check(); } diff --git a/includes/class-rest-email-controller.php b/includes/class-rest-email-controller.php new file mode 100644 index 0000000..e8dec38 --- /dev/null +++ b/includes/class-rest-email-controller.php @@ -0,0 +1,303 @@ + \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'send_email' ), + 'permission_callback' => array( $this, 'send_email_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Checks if a given request has access to send emails. + * + * @param \WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has access, WP_Error object otherwise. + */ + public function send_email_permissions_check( $request ) { + // Allow public access by default, but can be restricted via filter + $allowed = apply_filters( 'fuxt_api_email_permissions', true, $request ); + + if ( ! $allowed ) { + return new \WP_Error( + 'rest_email_forbidden', + __( 'Sorry, you are not allowed to send emails.', 'fuxt-api' ), + array( 'status' => 403 ) + ); + } + + return true; + } + + /** + * Retrieves the query params for the email request. + * + * @return array Collection parameters. + */ + public function get_collection_params() { + return array( + 'to' => array( + 'description' => __( 'Email address to send to.', 'fuxt-api' ), + 'type' => 'string', + 'required' => true, + 'format' => 'email', + ), + 'subject' => array( + 'description' => __( 'Email subject.', 'fuxt-api' ), + 'type' => 'string', + 'required' => true, + ), + 'message' => array( + 'description' => __( 'Email message content.', 'fuxt-api' ), + 'type' => 'string', + 'required' => true, + ), + 'from_name' => array( + 'description' => __( 'Sender name.', 'fuxt-api' ), + 'type' => 'string', + 'default' => get_option( 'blogname' ), + ), + 'from_email' => array( + 'description' => __( 'Sender email address.', 'fuxt-api' ), + 'type' => 'string', + 'format' => 'email', + 'default' => get_option( 'admin_email' ), + ), + 'reply_to' => array( + 'description' => __( 'Reply-to email address.', 'fuxt-api' ), + 'type' => 'string', + 'format' => 'email', + ), + 'cc' => array( + 'description' => __( 'CC email addresses (comma separated).', 'fuxt-api' ), + 'type' => 'string', + ), + 'bcc' => array( + 'description' => __( 'BCC email addresses (comma separated).', 'fuxt-api' ), + 'type' => 'string', + ), + 'headers' => array( + 'description' => __( 'Additional email headers as key-value pairs.', 'fuxt-api' ), + 'type' => 'object', + ), + 'attachments' => array( + 'description' => __( 'File attachments (array of file paths or URLs).', 'fuxt-api' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'is_html' => array( + 'description' => __( 'Whether the message is HTML content.', 'fuxt-api' ), + 'type' => 'boolean', + 'default' => false, + ), + ); + } + + /** + * Email response schema. + * + * @return array Schema definition. + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'fuxt_email_response', + 'type' => 'object', + 'properties' => array( + 'success' => array( + 'description' => __( 'Whether the email was sent successfully.', 'fuxt-api' ), + 'type' => 'boolean', + ), + 'message' => array( + 'description' => __( 'Response message.', 'fuxt-api' ), + 'type' => 'string', + ), + 'data' => array( + 'description' => __( 'Additional response data.', 'fuxt-api' ), + 'type' => 'object', + ), + ), + ); + + return $schema; + } + + /** + * Send email. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function send_email( $request ) { + $to = sanitize_email( $request['to'] ); + $subject = sanitize_text_field( $request['subject'] ); + $message = $request['message']; + $from_name = sanitize_text_field( $request['from_name'] ); + $from_email = sanitize_email( $request['from_email'] ); + $reply_to = ! empty( $request['reply_to'] ) ? sanitize_email( $request['reply_to'] ) : $from_email; + $cc = $request['cc']; + $bcc = $request['bcc']; + $headers = $request['headers']; + $attachments = $request['attachments']; + $is_html = (bool) $request['is_html']; + + // Validate required fields + if ( empty( $to ) || empty( $subject ) || empty( $message ) ) { + return new \WP_Error( + 'rest_email_missing_fields', + __( 'Missing required fields: to, subject, and message are required.', 'fuxt-api' ), + array( 'status' => 400 ) + ); + } + + // Prepare email headers + $email_headers = array(); + + // From header + $email_headers[] = 'From: ' . $from_name . ' <' . $from_email . '>'; + + // Reply-To header + if ( ! empty( $reply_to ) ) { + $email_headers[] = 'Reply-To: ' . $reply_to; + } + + // CC header + if ( ! empty( $cc ) ) { + $cc_emails = array_map( 'trim', explode( ',', $cc ) ); + $cc_emails = array_map( 'sanitize_email', $cc_emails ); + $cc_emails = array_filter( $cc_emails ); + if ( ! empty( $cc_emails ) ) { + $email_headers[] = 'Cc: ' . implode( ', ', $cc_emails ); + } + } + + // BCC header + if ( ! empty( $bcc ) ) { + $bcc_emails = array_map( 'trim', explode( ',', $bcc ) ); + $bcc_emails = array_map( 'sanitize_email', $bcc_emails ); + $bcc_emails = array_filter( $bcc_emails ); + if ( ! empty( $bcc_emails ) ) { + $email_headers[] = 'Bcc: ' . implode( ', ', $bcc_emails ); + } + } + + // Content-Type header + if ( $is_html ) { + $email_headers[] = 'Content-Type: text/html; charset=UTF-8'; + } else { + $email_headers[] = 'Content-Type: text/plain; charset=UTF-8'; + } + + // Additional custom headers + if ( ! empty( $headers ) && is_array( $headers ) ) { + foreach ( $headers as $key => $value ) { + $email_headers[] = sanitize_text_field( $key ) . ': ' . sanitize_text_field( $value ); + } + } + + // Process attachments + $processed_attachments = array(); + if ( ! empty( $attachments ) && is_array( $attachments ) ) { + foreach ( $attachments as $attachment ) { + $attachment = sanitize_text_field( $attachment ); + if ( ! empty( $attachment ) ) { + // Check if it's a URL or file path + if ( filter_var( $attachment, FILTER_VALIDATE_URL ) ) { + // Download URL to temporary file + $temp_file = download_url( $attachment ); + if ( ! is_wp_error( $temp_file ) ) { + $processed_attachments[] = $temp_file; + } + } elseif ( file_exists( $attachment ) ) { + $processed_attachments[] = $attachment; + } + } + } + } + + // Allow filtering of email data before sending + $email_data = apply_filters( 'fuxt_api_email_data', array( + 'to' => $to, + 'subject' => $subject, + 'message' => $message, + 'headers' => $email_headers, + 'attachments' => $processed_attachments, + ), $request ); + + // Send the email + $sent = wp_mail( + $email_data['to'], + $email_data['subject'], + $email_data['message'], + $email_data['headers'], + $email_data['attachments'] + ); + + // Clean up temporary files + foreach ( $processed_attachments as $temp_file ) { + if ( strpos( $temp_file, sys_get_temp_dir() ) === 0 ) { + unlink( $temp_file ); + } + } + + if ( $sent ) { + $response_data = array( + 'success' => true, + 'message' => __( 'Email sent successfully.', 'fuxt-api' ), + 'data' => array( + 'to' => $to, + 'subject' => $subject, + ), + ); + } else { + $response_data = array( + 'success' => false, + 'message' => __( 'Failed to send email.', 'fuxt-api' ), + 'data' => array(), + ); + } + + // Allow filtering of response + $response_data = apply_filters( 'fuxt_api_email_response', $response_data, $request, $sent ); + + return rest_ensure_response( $response_data ); + } +} From 52ff4ace0362e4bbd6c18dce09751282843f3a61 Mon Sep 17 00:00:00 2001 From: dchiamp Date: Mon, 8 Sep 2025 16:48:46 -0700 Subject: [PATCH 2/3] update controller and notes --- EMAIL_API_EXAMPLES.md | 70 ++++++++++++++++++++---- includes/class-plugin.php | 6 +- includes/class-rest-email-controller.php | 44 +++++++++++++-- 3 files changed, 103 insertions(+), 17 deletions(-) diff --git a/EMAIL_API_EXAMPLES.md b/EMAIL_API_EXAMPLES.md index 09741b1..8bd6825 100644 --- a/EMAIL_API_EXAMPLES.md +++ b/EMAIL_API_EXAMPLES.md @@ -16,7 +16,9 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ -d '{ "to": "recipient@example.com", "subject": "Test Email", - "message": "This is a test email from the Fuxt API." + "message": "This is a test email from the Fuxt API.", + "trap": "unique-client-id-123", + "clientRequestId": "unique-client-id-123" }' ``` @@ -29,7 +31,9 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ "to": "recipient@example.com", "subject": "HTML Test Email", "message": "

Hello World!

This is an HTML email.

", - "is_html": true + "is_html": true, + "trap": "unique-client-id-456", + "clientRequestId": "unique-client-id-456" }' ``` @@ -44,7 +48,9 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ "message": "This email is sent from a custom sender.", "from_name": "John Doe", "from_email": "john@example.com", - "reply_to": "noreply@example.com" + "reply_to": "noreply@example.com", + "trap": "unique-client-id-789", + "clientRequestId": "unique-client-id-789" }' ``` @@ -58,7 +64,9 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ "subject": "Email with CC and BCC", "message": "This email has CC and BCC recipients.", "cc": "cc1@example.com, cc2@example.com", - "bcc": "bcc@example.com" + "bcc": "bcc@example.com", + "trap": "unique-client-id-101", + "clientRequestId": "unique-client-id-101" }' ``` @@ -74,7 +82,9 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ "attachments": [ "/path/to/local/file.pdf", "https://example.com/remote-file.jpg" - ] + ], + "trap": "unique-client-id-102", + "clientRequestId": "unique-client-id-102" }' ``` @@ -91,7 +101,9 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ "X-Priority": "1", "X-MSMail-Priority": "High", "Importance": "high" - } + }, + "trap": "unique-client-id-103", + "clientRequestId": "unique-client-id-103" }' ``` @@ -129,7 +141,9 @@ const sendEmail = async (emailData) => { sendEmail({ to: 'recipient@example.com', subject: 'Test Email', - message: 'This is a test email from JavaScript.' + message: 'This is a test email from JavaScript.', + trap: 'unique-client-id-js-123', + clientRequestId: 'unique-client-id-js-123' }); ``` @@ -179,13 +193,16 @@ const ContactForm = () => { const handleSubmit = async (formData) => { try { + const clientId = `contact-form-${Date.now()}`; await sendEmail({ to: 'contact@example.com', subject: 'New Contact Form Submission', message: `Name: ${formData.name}\nEmail: ${formData.email}\nMessage: ${formData.message}`, from_name: formData.name, from_email: formData.email, - reply_to: formData.email + reply_to: formData.email, + trap: clientId, + clientRequestId: clientId }); alert('Message sent successfully!'); } catch (err) { @@ -226,12 +243,15 @@ function send_email_via_api($email_data) { } // Usage +$client_id = 'php-email-' . time(); $result = send_email_via_api([ 'to' => 'recipient@example.com', 'subject' => 'Test Email from PHP', 'message' => 'This email was sent using the Fuxt API from PHP.', 'from_name' => 'WordPress Site', - 'from_email' => 'noreply@yoursite.com' + 'from_email' => 'noreply@yoursite.com', + 'trap' => $client_id, + 'clientRequestId' => $client_id ]); ``` @@ -267,6 +287,8 @@ $result = send_email_via_api([ | `to` | string | Yes | - | Email address to send to | | `subject` | string | Yes | - | Email subject | | `message` | string | Yes | - | Email message content | +| `trap` | string | Yes | - | Anti-spam measure. Must equal clientRequestId | +| `clientRequestId` | string | Yes | - | Client request ID for anti-spam verification | | `from_name` | string | No | Site name | Sender name | | `from_email` | string | No | Admin email | Sender email address | | `reply_to` | string | No | from_email | Reply-to email address | @@ -276,9 +298,37 @@ $result = send_email_via_api([ | `attachments` | array | No | - | File attachments (array of file paths or URLs) | | `is_html` | boolean | No | false | Whether the message is HTML content | +## Spam Protection + +The email endpoint includes a built-in spam trap mechanism to prevent automated spam submissions: + +- **trap**: A string value that must be provided by the client +- **clientRequestId**: A string value that must be provided by the client +- **Validation**: The `trap` value must exactly match the `clientRequestId` value, otherwise the email will be rejected + +This simple mechanism helps prevent automated spam bots from sending emails through your API, as legitimate clients will generate matching values for both parameters. + +### Example Spam Trap Implementation + +```javascript +// Generate a unique client ID +const clientId = `email-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + +// Both trap and clientRequestId must be the same value +const emailData = { + to: 'recipient@example.com', + subject: 'Test Email', + message: 'This email has spam protection.', + trap: clientId, + clientRequestId: clientId +}; +``` + ## Security Considerations -1. **Permissions**: By default, the endpoint allows public access. You can restrict access using the `fuxt_api_email_permissions` filter: +1. **Spam Protection**: The trap/clientRequestId mechanism provides basic spam protection. Consider additional measures for high-traffic sites. + +2. **Permissions**: By default, the endpoint allows public access. You can restrict access using the `fuxt_api_email_permissions` filter: ```php add_filter('fuxt_api_email_permissions', function($allowed, $request) { diff --git a/includes/class-plugin.php b/includes/class-plugin.php index 0033fb0..7a9e19c 100644 --- a/includes/class-plugin.php +++ b/includes/class-plugin.php @@ -24,7 +24,11 @@ public function init() { ( new REST_Acf_Controller() )->init(); ( new REST_Posts_Controller() )->init(); ( new REST_User_Controller() )->init(); - ( new REST_Email_Controller() )->init(); + + // Email controller is commented out for security - it exposes a public email endpoint + // To enable: uncomment the line below and ensure proper spam protection is in place + // See EMAIL_API_EXAMPLES.md for usage examples and security considerations + // ( new REST_Email_Controller() )->init(); $this->update_check(); } diff --git a/includes/class-rest-email-controller.php b/includes/class-rest-email-controller.php index e8dec38..b5bf740 100644 --- a/includes/class-rest-email-controller.php +++ b/includes/class-rest-email-controller.php @@ -29,21 +29,23 @@ public function init() { * Register email endpoint. */ public function register_endpoint() { + // Debug: Log that we're registering the endpoint + error_log( 'Fuxt API: Registering POST-only email endpoint at ' . self::REST_NAMESPACE . self::ROUTE ); + register_rest_route( self::REST_NAMESPACE, self::ROUTE, array( - array( - 'methods' => \WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'send_email' ), - 'permission_callback' => array( $this, 'send_email_permissions_check' ), - 'args' => $this->get_collection_params(), - ), + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'send_email' ), + 'permission_callback' => array( $this, 'send_email_permissions_check' ), + 'args' => $this->get_collection_params(), 'schema' => array( $this, 'get_item_schema' ), ) ); } + /** * Checks if a given request has access to send emails. * @@ -128,6 +130,16 @@ public function get_collection_params() { 'type' => 'boolean', 'default' => false, ), + 'trap' => array( + 'description' => __( 'Crude anti-spam measure. This must equal the clientRequestId, otherwise the email will not be sent.', 'fuxt-api' ), + 'type' => 'string', + 'required' => true, + ), + 'clientRequestId' => array( + 'description' => __( 'Client request ID for anti-spam verification.', 'fuxt-api' ), + 'type' => 'string', + 'required' => true, + ), ); } @@ -178,6 +190,8 @@ public function send_email( $request ) { $headers = $request['headers']; $attachments = $request['attachments']; $is_html = (bool) $request['is_html']; + $trap = sanitize_text_field( $request['trap'] ); + $client_request_id = sanitize_text_field( $request['clientRequestId'] ); // Validate required fields if ( empty( $to ) || empty( $subject ) || empty( $message ) ) { @@ -188,6 +202,24 @@ public function send_email( $request ) { ); } + // Validate spam trap + if ( empty( $trap ) || empty( $client_request_id ) ) { + return new \WP_Error( + 'rest_email_missing_spam_fields', + __( 'Missing required fields: trap and clientRequestId are required for spam protection.', 'fuxt-api' ), + array( 'status' => 400 ) + ); + } + + // Check spam trap + if ( $trap !== $client_request_id ) { + return new \WP_Error( + 'rest_email_spam_trap_failed', + __( 'Spam trap validation failed. Email not sent.', 'fuxt-api' ), + array( 'status' => 400 ) + ); + } + // Prepare email headers $email_headers = array(); From 2a33e752ec17827799ba648a18024e629cc129f8 Mon Sep 17 00:00:00 2001 From: dchiamp Date: Tue, 9 Sep 2025 13:32:11 -0700 Subject: [PATCH 3/3] update client_request_id name convention --- EMAIL_API_EXAMPLES.md | 32 ++++++++++++------------ includes/class-rest-email-controller.php | 6 ++--- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/EMAIL_API_EXAMPLES.md b/EMAIL_API_EXAMPLES.md index 8bd6825..34f15d4 100644 --- a/EMAIL_API_EXAMPLES.md +++ b/EMAIL_API_EXAMPLES.md @@ -18,7 +18,7 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ "subject": "Test Email", "message": "This is a test email from the Fuxt API.", "trap": "unique-client-id-123", - "clientRequestId": "unique-client-id-123" + "client_request_id": "unique-client-id-123" }' ``` @@ -33,7 +33,7 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ "message": "

Hello World!

This is an HTML email.

", "is_html": true, "trap": "unique-client-id-456", - "clientRequestId": "unique-client-id-456" + "client_request_id": "unique-client-id-456" }' ``` @@ -50,7 +50,7 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ "from_email": "john@example.com", "reply_to": "noreply@example.com", "trap": "unique-client-id-789", - "clientRequestId": "unique-client-id-789" + "client_request_id": "unique-client-id-789" }' ``` @@ -66,7 +66,7 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ "cc": "cc1@example.com, cc2@example.com", "bcc": "bcc@example.com", "trap": "unique-client-id-101", - "clientRequestId": "unique-client-id-101" + "client_request_id": "unique-client-id-101" }' ``` @@ -84,7 +84,7 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ "https://example.com/remote-file.jpg" ], "trap": "unique-client-id-102", - "clientRequestId": "unique-client-id-102" + "client_request_id": "unique-client-id-102" }' ``` @@ -103,7 +103,7 @@ curl -X POST "https://yoursite.com/wp-json/fuxt/v1/email" \ "Importance": "high" }, "trap": "unique-client-id-103", - "clientRequestId": "unique-client-id-103" + "client_request_id": "unique-client-id-103" }' ``` @@ -143,7 +143,7 @@ sendEmail({ subject: 'Test Email', message: 'This is a test email from JavaScript.', trap: 'unique-client-id-js-123', - clientRequestId: 'unique-client-id-js-123' + client_request_id: 'unique-client-id-js-123' }); ``` @@ -202,7 +202,7 @@ const ContactForm = () => { from_email: formData.email, reply_to: formData.email, trap: clientId, - clientRequestId: clientId + client_request_id: clientId }); alert('Message sent successfully!'); } catch (err) { @@ -251,7 +251,7 @@ $result = send_email_via_api([ 'from_name' => 'WordPress Site', 'from_email' => 'noreply@yoursite.com', 'trap' => $client_id, - 'clientRequestId' => $client_id + 'client_request_id' => $client_id ]); ``` @@ -287,8 +287,8 @@ $result = send_email_via_api([ | `to` | string | Yes | - | Email address to send to | | `subject` | string | Yes | - | Email subject | | `message` | string | Yes | - | Email message content | -| `trap` | string | Yes | - | Anti-spam measure. Must equal clientRequestId | -| `clientRequestId` | string | Yes | - | Client request ID for anti-spam verification | +| `trap` | string | Yes | - | Anti-spam measure. Must equal client_request_id | +| `client_request_id` | string | Yes | - | Client request ID for anti-spam verification | | `from_name` | string | No | Site name | Sender name | | `from_email` | string | No | Admin email | Sender email address | | `reply_to` | string | No | from_email | Reply-to email address | @@ -303,8 +303,8 @@ $result = send_email_via_api([ The email endpoint includes a built-in spam trap mechanism to prevent automated spam submissions: - **trap**: A string value that must be provided by the client -- **clientRequestId**: A string value that must be provided by the client -- **Validation**: The `trap` value must exactly match the `clientRequestId` value, otherwise the email will be rejected +- **client_request_id**: A string value that must be provided by the client +- **Validation**: The `trap` value must exactly match the `client_request_id` value, otherwise the email will be rejected This simple mechanism helps prevent automated spam bots from sending emails through your API, as legitimate clients will generate matching values for both parameters. @@ -314,19 +314,19 @@ This simple mechanism helps prevent automated spam bots from sending emails thro // Generate a unique client ID const clientId = `email-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; -// Both trap and clientRequestId must be the same value +// Both trap and client_request_id must be the same value const emailData = { to: 'recipient@example.com', subject: 'Test Email', message: 'This email has spam protection.', trap: clientId, - clientRequestId: clientId + client_request_id: clientId }; ``` ## Security Considerations -1. **Spam Protection**: The trap/clientRequestId mechanism provides basic spam protection. Consider additional measures for high-traffic sites. +1. **Spam Protection**: The trap/client_request_id mechanism provides basic spam protection. Consider additional measures for high-traffic sites. 2. **Permissions**: By default, the endpoint allows public access. You can restrict access using the `fuxt_api_email_permissions` filter: diff --git a/includes/class-rest-email-controller.php b/includes/class-rest-email-controller.php index b5bf740..72b6069 100644 --- a/includes/class-rest-email-controller.php +++ b/includes/class-rest-email-controller.php @@ -135,7 +135,7 @@ public function get_collection_params() { 'type' => 'string', 'required' => true, ), - 'clientRequestId' => array( + 'client_request_id' => array( 'description' => __( 'Client request ID for anti-spam verification.', 'fuxt-api' ), 'type' => 'string', 'required' => true, @@ -191,7 +191,7 @@ public function send_email( $request ) { $attachments = $request['attachments']; $is_html = (bool) $request['is_html']; $trap = sanitize_text_field( $request['trap'] ); - $client_request_id = sanitize_text_field( $request['clientRequestId'] ); + $client_request_id = sanitize_text_field( $request['client_request_id'] ); // Validate required fields if ( empty( $to ) || empty( $subject ) || empty( $message ) ) { @@ -206,7 +206,7 @@ public function send_email( $request ) { if ( empty( $trap ) || empty( $client_request_id ) ) { return new \WP_Error( 'rest_email_missing_spam_fields', - __( 'Missing required fields: trap and clientRequestId are required for spam protection.', 'fuxt-api' ), + __( 'Missing required fields: trap and client_request_id are required for spam protection.', 'fuxt-api' ), array( 'status' => 400 ) ); }