Skip to content

Commit d93bced

Browse files
authored
Merge pull request #14 from stellarwp/fix/handle-multiple-email-recipients
Fixes email recipient handling and updates version
2 parents dcf2995 + 737b425 commit d93bced

File tree

8 files changed

+199
-6
lines changed

8 files changed

+199
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to this project will be documented in this file. This project adhere to the [Semantic Versioning](http://semver.org/) standard.
44

5+
## [0.0.6] 2025-08-26
6+
7+
* Fix - Update Email task to properly handle multiple email recipients separated by commas.
8+
9+
[0.0.6]: https://github.com/stellarwp/shepherd/releases/tag/0.0.6
10+
511
## [0.0.5] 2025-08-19
612

713
* Fix - Ensure the AS logger table exists before using it. Introduce a filter `shepherd_<hook_prefix>_should_log` to disable logging.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Shepherd is a lightweight and powerful background processing library for WordPre
99
- **Automatic Retries**: Configurable automatic retries for failed tasks.
1010
- **Debouncing**: Prevent tasks from running too frequently.
1111
- **Logging**: Built-in database logging for task lifecycle events.
12-
- **Included Tasks**: Comes with a ready-to-use `Email` task.
12+
- **Included Tasks**: Comes with ready-to-use tasks including `Email` (with multi-recipient support), `HTTP_Request`, and `Herding` tasks.
1313

1414
## Getting Started
1515

docs/tasks.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Sends emails asynchronously using WordPress's `wp_mail()` function.
1212

1313
- Automatic retries (up to 4 additional attempts)
1414
- Support for HTML content and attachments
15+
- Support for multiple recipients (comma-separated)
1516
- Comprehensive error handling
1617
- WordPress action hooks for tracking
1718

@@ -20,6 +21,7 @@ Sends emails asynchronously using WordPress's `wp_mail()` function.
2021
```php
2122
use StellarWP\Shepherd\Tasks\Email;
2223

24+
// Single recipient
2325
$email = new Email(
2426
'user@example.com',
2527
'Welcome!',
@@ -28,6 +30,15 @@ $email = new Email(
2830
);
2931

3032
shepherd()->dispatch( $email );
33+
34+
// Multiple recipients
35+
$team_email = new Email(
36+
'user1@example.com, user2@example.com, admin@example.com',
37+
'Team Update',
38+
'Important announcement for the team'
39+
);
40+
41+
shepherd()->dispatch( $team_email );
3142
```
3243

3344
### [HTTP Request Task](./tasks/http-request.md)

docs/tasks/email.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public function __construct(
1616

1717
### Parameters
1818

19-
- **`$to_email`** (string, required): Recipient's email address
19+
- **`$to_email`** (string, required): Recipient's email address(es). Can be a single email or multiple comma-separated emails.
2020
- **`$subject`** (string, required): Email subject line
2121
- **`$body`** (string, required): Email body content (HTML or plain text)
2222
- **`$headers`** (array, optional): Email headers (e.g., content type, reply-to)
@@ -65,6 +65,29 @@ $email = new Email(
6565
shepherd()->dispatch( $email );
6666
```
6767

68+
### Email to Multiple Recipients
69+
70+
```php
71+
// Send to multiple recipients
72+
$email = new Email(
73+
'user1@example.com, user2@example.com, admin@example.com',
74+
'Team Update',
75+
'Important update for all team members.'
76+
);
77+
78+
shepherd()->dispatch( $email );
79+
80+
// With proper spacing (whitespace is automatically handled)
81+
$email = new Email(
82+
'user1@example.com,user2@example.com, user3@example.com',
83+
'Newsletter',
84+
'<h1>Weekly Newsletter</h1><p>Here are this week\'s updates...</p>',
85+
[ 'Content-Type: text/html; charset=UTF-8' ]
86+
);
87+
88+
shepherd()->dispatch( $email );
89+
```
90+
6891
### Email with Attachments
6992

7093
```php

shepherd.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* @wordpress-plugin
1010
* Plugin Name: Shepherd
1111
* Description: A library for offloading tasks to background processes.
12-
* Version: 0.0.5
12+
* Version: 0.0.6
1313
* Author: StellarWP
1414
* Author URI: https://stellarwp.com
1515
* License: GPL-2.0-or-later

src/Tasks/Email.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ class Email extends Task_Abstract {
3030
* The email task's constructor.
3131
*
3232
* @since 0.0.1
33+
* @since TBD - Allow multiple comma-separated recipients.
3334
*
34-
* @param string $to_email The email address to send the email to.
35+
* @param string $to_email The email address(es) to send the email to. Can be comma-separated for multiple recipients.
3536
* @param string $subject The email subject.
3637
* @param string $body The email body.
3738
* @param string[] $headers Optional. Additional headers.
@@ -81,8 +82,23 @@ protected function validate_args(): void {
8182
throw new InvalidArgumentException( __( 'Email task requires at least 3 arguments.', 'stellarwp-shepherd' ) );
8283
}
8384

84-
if ( ! is_email( $args[0] ) ) {
85-
throw new InvalidArgumentException( __( 'Email address is invalid.', 'stellarwp-shepherd' ) );
85+
$recipients = $args[0];
86+
if ( ! is_string( $recipients ) || empty( trim( $recipients ) ) ) {
87+
throw new InvalidArgumentException( __( 'Email recipients must be a non-empty string.', 'stellarwp-shepherd' ) );
88+
}
89+
90+
// Split by comma and validate each email.
91+
$emails = array_map( 'trim', explode( ',', $recipients ) );
92+
$invalid_emails = array_filter( $emails, fn( $email ) => ! is_email( $email ) );
93+
94+
if ( ! empty( $invalid_emails ) ) {
95+
throw new InvalidArgumentException(
96+
sprintf(
97+
// translators: %s is a comma-separated list of invalid email addresses.
98+
__( 'Invalid email address(es): %s', 'stellarwp-shepherd' ),
99+
implode( ', ', $invalid_emails )
100+
)
101+
);
86102
}
87103

88104
if ( ! is_string( $args[1] ) ) {

tests/integration/Tasks/Email_Test.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,62 @@ public function it_should_schedule_multiple_tasks_with_different_args(): void {
248248
$this->assertSame( [ 'test1@test.com', 'subject1', 'body1', [], [] ], $spy[0] );
249249
$this->assertSame( [ 'test2@test.com', 'subject2', 'body2', [], [] ], $spy[1] );
250250
}
251+
252+
/**
253+
* @test
254+
*/
255+
public function it_should_dispatch_and_process_email_with_multiple_recipients(): void {
256+
$spy = [];
257+
$this->set_fn_return( 'wp_mail', function ( ...$args ) use ( &$spy ) {
258+
$spy[] = $args;
259+
return true;
260+
}, true );
261+
262+
$shepherd = shepherd();
263+
$this->assertNull( $shepherd->get_last_scheduled_task_id() );
264+
265+
$dummy_task = new Email( 'test1@test.com, test2@test.com, test3@test.com', 'subject', 'body', [ 'Reply-To: sender@test.com' ] );
266+
$shepherd->dispatch( $dummy_task );
267+
268+
$last_scheduled_task_id = $shepherd->get_last_scheduled_task_id();
269+
270+
$this->assertIsInt( $last_scheduled_task_id );
271+
272+
$this->assertTaskHasActionPending( $last_scheduled_task_id );
273+
$this->assertTaskIsScheduledForExecutionAt( $last_scheduled_task_id, time() );
274+
$this->assertTaskExecutesWithoutErrors( $last_scheduled_task_id );
275+
276+
$this->assertCount( 1, $spy );
277+
$this->assertSame( [ 'test1@test.com, test2@test.com, test3@test.com', 'subject', 'body', [ 'Reply-To: sender@test.com' ], [] ], $spy[0] );
278+
279+
$logs = $this->get_logger()->retrieve_logs( $last_scheduled_task_id );
280+
$this->assertCount( 3, $logs );
281+
$this->assertSame( 'created', $logs[0]->get_type() );
282+
$this->assertSame( 'started', $logs[1]->get_type() );
283+
$this->assertSame( 'finished', $logs[2]->get_type() );
284+
}
285+
286+
/**
287+
* @test
288+
*/
289+
public function it_should_handle_multiple_recipients_with_varying_whitespace(): void {
290+
$spy = [];
291+
$this->set_fn_return( 'wp_mail', function ( ...$args ) use ( &$spy ) {
292+
$spy[] = $args;
293+
return true;
294+
}, true );
295+
296+
$shepherd = shepherd();
297+
298+
// Test with various whitespace patterns
299+
$task = new Email( 'user1@test.com, user2@test.com ,user3@test.com', 'Test', 'Body' );
300+
$shepherd->dispatch( $task );
301+
$task_id = $shepherd->get_last_scheduled_task_id();
302+
303+
$this->assertTaskExecutesWithoutErrors( $task_id );
304+
305+
$this->assertCount( 1, $spy );
306+
// wp_mail receives the exact string we pass, WordPress handles the parsing
307+
$this->assertSame( 'user1@test.com, user2@test.com ,user3@test.com', $spy[0][0] );
308+
}
251309
}

tests/wpunit/Tasks/Email_Test.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Email_Test extends WPTestCase {
1717
*/
1818
public function it_should_throw_exception_for_invalid_email() {
1919
$this->expectException( InvalidArgumentException::class );
20+
$this->expectExceptionMessage( 'Invalid email address(es): not-an-email' );
2021
new Email( 'not-an-email', 'Subject', 'Body' );
2122
}
2223

@@ -68,4 +69,82 @@ public function it_should_throw_shepherd_exception_if_wp_mail_fails() {
6869
$this->expectException( ShepherdTaskException::class );
6970
$email->process();
7071
}
72+
73+
/**
74+
* @test
75+
*/
76+
public function it_should_accept_multiple_recipients_separated_by_comma() {
77+
$email = new Email( 'test1@test.com, test2@test.com, test3@test.com', 'Subject', 'Body' );
78+
79+
$spy = [];
80+
$this->set_fn_return( 'wp_mail', function ( $to, $subject, $body, $headers = [], $attachments = [] ) use ( &$spy ) {
81+
$spy = [ $to, $subject, $body, $headers, $attachments ];
82+
return true;
83+
}, true );
84+
85+
$email->process();
86+
87+
$this->assertEquals( [ 'test1@test.com, test2@test.com, test3@test.com', 'Subject', 'Body', [], [] ], $spy );
88+
}
89+
90+
/**
91+
* @test
92+
*/
93+
public function it_should_accept_multiple_recipients_with_spaces() {
94+
$email = new Email( 'test1@test.com, test2@test.com ,test3@test.com', 'Subject', 'Body' );
95+
96+
$spy = [];
97+
$this->set_fn_return( 'wp_mail', function ( $to, $subject, $body, $headers = [], $attachments = [] ) use ( &$spy ) {
98+
$spy = [ $to, $subject, $body, $headers, $attachments ];
99+
return true;
100+
}, true );
101+
102+
$email->process();
103+
104+
$this->assertEquals( [ 'test1@test.com, test2@test.com ,test3@test.com', 'Subject', 'Body', [], [] ], $spy );
105+
}
106+
107+
/**
108+
* @test
109+
*/
110+
public function it_should_throw_exception_for_invalid_email_in_multiple_recipients() {
111+
$this->expectException( InvalidArgumentException::class );
112+
$this->expectExceptionMessage( 'Invalid email address(es): not-an-email, another-invalid' );
113+
new Email( 'test@test.com, not-an-email, valid@email.com, another-invalid', 'Subject', 'Body' );
114+
}
115+
116+
/**
117+
* @test
118+
*/
119+
public function it_should_throw_exception_for_empty_string_recipients() {
120+
$this->expectException( InvalidArgumentException::class );
121+
$this->expectExceptionMessage( 'Email recipients must be a non-empty string' );
122+
new Email( '', 'Subject', 'Body' );
123+
}
124+
125+
/**
126+
* @test
127+
*/
128+
public function it_should_throw_exception_for_whitespace_only_recipients() {
129+
$this->expectException( InvalidArgumentException::class );
130+
$this->expectExceptionMessage( 'Email recipients must be a non-empty string' );
131+
new Email( ' ', 'Subject', 'Body' );
132+
}
133+
134+
/**
135+
* @test
136+
*/
137+
public function it_should_accept_single_recipient() {
138+
$email = new Email( 'test@test.com', 'Subject', 'Body' );
139+
140+
$spy = [];
141+
$this->set_fn_return( 'wp_mail', function ( $to, $subject, $body, $headers = [], $attachments = [] ) use ( &$spy ) {
142+
$spy = [ $to, $subject, $body, $headers, $attachments ];
143+
return true;
144+
}, true );
145+
146+
$email->process();
147+
148+
$this->assertEquals( [ 'test@test.com', 'Subject', 'Body', [], [] ], $spy );
149+
}
71150
}

0 commit comments

Comments
 (0)