PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.0 2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 All 43 releases
← All changes | app/Hooks/Handlers/ScheduleHandler.php +441 -197 1.12 → 2.2.0 View file →
@@ -1,26 +1,32 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\App\Hooks\Handlers;
4 4
5 +use FluentBoards\App\Models\Board;
5 6 use FluentBoards\App\Models\Comment;
7 +use FluentBoards\App\Models\Meta;
8 +use FluentBoards\App\Models\Relation;
6 9 use FluentBoards\App\Models\Task;
7 10 use FluentBoards\App\Models\User;
11 +use FluentBoards\App\Services\Constant;
8 12 use FluentBoards\App\Services\Helper;
13 +use FluentBoards\App\Services\Libs\Mailer;
9 14
10 15 class ScheduleHandler
11 16 {
12 17 public function sendEmailForComment(
13 18 $commentId,
14 - $usersToSendEmail,
19 + $recipientUserIds,
15 20 $current_user_id
16 21 ) {
22 + $context = ['type' => 'comment', 'comment_id' => $commentId];
17 23 try {
18 24 $comment = Comment::find($commentId) ?? null;
19 25 if ( ! $comment) {
20 26 return;
21 27 }
22 - if ('comment' != $comment->type) {
28 + if (!in_array($comment->type, ['comment', 'reply'])) {
23 29 return;
24 30 }
25 31
26 32 $task = Task::findOrFail($comment->task_id);
@@ -26,58 +32,138 @@
26 32 $task = Task::findOrFail($comment->task_id);
27 33 if ( ! $task) {
28 34 return;
29 35 }
30 -// $assignees = $task->assignees;
36 +
37 + $usersToSendEmail = $this->getEligibleCommentRecipientEmails(
38 + $recipientUserIds,
39 + $task->board_id
40 + );
41 +
42 + if (!$usersToSendEmail) {
43 + return;
44 + }
45 +
31 46 $board = $task->board;
47 +
32 48 $page_url = fluent_boards_page_url();
49 + $redirectUrl = site_url('/') . '?redirect=to_task&taskId='.$task->id;
33 50
34 - $user = $task->user($comment->created_by);
51 + $userData = $this->getUserData($current_user_id);
35 52
36 - $userData = $this->getUserData($current_user_id);
53 + $taskUrl = $this->taskUrl($page_url, $board->id, $task);
37 54
38 - $boardUrl = $page_url.'boards/'.$board->id;
39 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'.$task->id.'-'
40 - .substr($task->title, 0, 10);
55 + $isReply = $comment->type == 'reply';
41 56
42 - $userLinkTag = '<strong>'.htmlspecialchars($user->display_name)
43 - .'</strong>';
44 - $taskLinkTag = '<a target="_blank" href="'
45 - .htmlspecialchars($taskUrl).'">'
46 - .htmlspecialchars($task->title).'</a>';
47 - $boardLinkTag = '<a target="_blank" href="'
48 - .htmlspecialchars($boardUrl).'">'
49 - .htmlspecialchars($board->title).'</a>';
57 + if ($isReply) {
58 + $parentComment = $comment->parent_id ? Comment::find($comment->parent_id) : null;
50 59
51 - $data = [
52 - 'body' => 'commented on '.$taskLinkTag.' on '
53 - .$boardLinkTag.' board.',
54 - 'pre_header' => 'comment added in fluent task',
55 - 'show_footer' => true,
60 + $data = [
61 + 'badge' => ['label' => __('Reply', 'fluent-boards'), 'tone' => 'accent'],
62 + // translators: %s is the name of the person who replied
63 + 'heading' => sprintf(__('%s replied to your comment', 'fluent-boards'), $userData['display_name']),
64 + 'parent_comment' => $parentComment ? $parentComment->description : '',
65 + 'cta' => ['label' => __('Reply', 'fluent-boards'), 'url' => $redirectUrl],
66 + 'email_reason' => __('You’re getting this because someone replied to your comment.', 'fluent-boards'),
67 + 'pre_header' => wp_trim_words(wp_strip_all_tags($comment->description), 20),
68 + ];
69 + // translators: %s is the name of the person who replied
70 + $mailSubject = sprintf(__('%s replied to your comment', 'fluent-boards'), $userData['display_name']);
71 + } else {
72 + $data = [
73 + 'badge' => ['label' => __('New comment', 'fluent-boards'), 'tone' => 'accent'],
74 + // translators: %s is the name of the commenter
75 + 'heading' => sprintf(__('%s commented on a task', 'fluent-boards'), $userData['display_name']),
76 + 'cta' => ['label' => __('Reply to comment', 'fluent-boards'), 'url' => $redirectUrl],
77 + 'email_reason' => __('You’re getting this because of your email settings for this board.', 'fluent-boards'),
78 + 'pre_header' => wp_trim_words(wp_strip_all_tags($comment->description), 20),
79 + ];
80 + // translators: %1$s is the commenter name, %2$s is the task title
81 + $mailSubject = sprintf(__('%1$s commented on “%2$s”', 'fluent-boards'), $userData['display_name'], $task->title);
82 + }
83 +
84 + $data = array_merge($this->getBrandingData(), $data, [
85 + 'context' => $this->contextLine($task->title, $taskUrl, $board->title, $page_url.'boards/'.$board->id),
56 86 'comment' => $comment->description,
57 87 'userData' => $userData,
58 - 'site_url' => site_url(),
59 - 'site_title' => get_bloginfo('name'),
60 - 'site_logo' => fluent_boards_site_logo(),
61 - ];
88 + 'show_footer' => true,
89 + ]);
62 90
63 - $mailSubject = 'new comment added on task';
64 91 $message = Helper::loadView('emails.comment2', $data);
65 - $headers = ['Content-Type: text/html; charset=UTF-8'];
66 92
67 93 foreach ($usersToSendEmail as $email) {
68 - \wp_mail($email, $mailSubject, $message, $headers);
94 + (new Mailer($email, $mailSubject, $message))->sendNotification($context);
69 95 }
70 - } catch (\Exception $e) {
71 - // do nothing // better to log here
96 + } catch (\Throwable $e) {
97 + Mailer::reportFailure($context, $e);
72 98 }
73 99 }
74 100
101 + public function sendEmailForMention($commentId, $recipientUserIds, $current_user_id)
102 + {
103 + $context = ['type' => 'mention', 'comment_id' => $commentId];
104 + try {
105 + $comment = Comment::find($commentId) ?? null;
106 + if ( ! $comment) {
107 + return;
108 + }
109 +
110 + if (!in_array($comment->type, ['comment', 'reply'])) {
111 + return;
112 + }
113 +
114 + $task = Task::findOrFail($comment->task_id);
115 + if ( ! $task) {
116 + return;
117 + }
118 +
119 + $usersToSendEmail = $this->getEligibleCommentRecipientEmails(
120 + $recipientUserIds,
121 + $task->board_id
122 + );
123 + if (!$usersToSendEmail) {
124 + return;
125 + }
126 + $board = $task->board;
127 +
128 + $page_url = fluent_boards_page_url();
129 + $redirectUrl = site_url('/') . '?redirect=to_task&taskId='.$task->id;
130 +
131 + $userData = $this->getUserData($current_user_id);
132 +
133 + $taskUrl = $this->taskUrl($page_url, $board->id, $task);
134 +
135 + $data = array_merge($this->getBrandingData(), [
136 + 'badge' => ['label' => __('Mentioned you', 'fluent-boards'), 'tone' => 'accent'],
137 + // translators: %s is the name of the person who mentioned the recipient
138 + 'heading' => sprintf(__('%s mentioned you in a comment', 'fluent-boards'), $userData['display_name']),
139 + 'context' => $this->contextLine($task->title, $taskUrl, $board->title, $page_url.'boards/'.$board->id),
140 + 'comment' => $comment->description,
141 + 'userData' => $userData,
142 + 'cta' => ['label' => __('Reply to comment', 'fluent-boards'), 'url' => $redirectUrl],
143 + 'email_reason' => __('You’re getting this because someone mentioned you.', 'fluent-boards'),
144 + 'pre_header' => wp_trim_words(wp_strip_all_tags($comment->description), 20),
145 + 'show_footer' => true,
146 + ]);
147 +
148 + // translators: %1$s is the name of the person who mentioned the recipient, %2$s is the task title
149 + $mailSubject = sprintf(__('%1$s mentioned you in “%2$s”', 'fluent-boards'), $userData['display_name'], $task->title);
150 + $message = Helper::loadView('emails.comment2', $data);
151 +
152 + foreach ($usersToSendEmail as $email) {
153 + (new Mailer($email, $mailSubject, $message))->sendNotification($context);
154 + }
155 + } catch (\Throwable $e) {
156 + Mailer::reportFailure($context, $e);
157 + }
158 + }
159 +
75 160 public function sendEmailForAddAssignee(
76 161 $taskId,
77 162 $newAssigneeId,
78 163 $current_user_id
79 164 ) {
165 + $context = ['type' => 'add_assignee', 'task_id' => $taskId];
80 166 try {
81 167 $task = Task::findOrFail($taskId);
82 168 $board = $task->board;
83 169 $assignee = User::findOrFail($newAssigneeId);
@@ -82,65 +168,63 @@
82 168 $board = $task->board;
83 169 $assignee = User::findOrFail($newAssigneeId);
84 170
85 171 $userData = $this->getUserData($current_user_id);
172 + $page_url = fluent_boards_page_url();
173 + $boardUrl = $page_url.'boards/'.$board->id;
86 174
87 - $page_url = fluent_boards_page_url();
88 - $boardUrl = $page_url.'boards/'.$board->id;
89 - $boardLinkTag = '<a target="_blank" href="'
90 - .htmlspecialchars($boardUrl).'">'
91 - .htmlspecialchars($board->title).'</a>';
92 -
93 175 if (null == $task->parent_id) {
94 176 // this is a task
95 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'
96 - .$task->id.'-'.substr($task->title, 0, 10);
97 - $taskLinkTag = '<a target="_blank" href="'
98 - .htmlspecialchars($taskUrl).'">'
99 - .htmlspecialchars($task->title).'</a>';
100 - $data = [
101 - 'body' => 'has assigned you to task '.$taskLinkTag
102 - .' on the board '.$boardLinkTag,
103 - 'pre_header' => 'you have been assigned to task',
104 - 'show_footer' => true,
105 - 'userData' => $userData,
106 - 'site_url' => site_url(),
107 - 'site_title' => get_bloginfo('name'),
108 - 'site_logo' => fluent_boards_site_logo(),
177 + $taskUrl = $this->taskUrl($page_url, $board->id, $task);
178 +
179 + $data = [
180 + 'badge' => ['label' => __('Assigned to you', 'fluent-boards'), 'tone' => 'accent'],
181 + // translators: %s is the name of the person who assigned the task
182 + 'heading' => sprintf(__('%s assigned you to a task', 'fluent-boards'), $userData['display_name']),
183 + 'task_card' => [
184 + 'title' => $task->title,
185 + 'rows' => $this->taskRows($task, $board, $boardUrl),
186 + ],
187 + 'cta' => ['label' => __('Open task', 'fluent-boards'), 'url' => $taskUrl],
188 + 'pre_header' => $this->taskPreHeader($task, $board),
109 189 ];
190 + // translators: %s is the task title
191 + $mailSubject = sprintf(__('You’re assigned to “%s”', 'fluent-boards'), $task->title);
110 192 } else {
111 - // this is a subtask
112 - $task = $task->parentTask($task->parent_id);
193 + // this is a subtask; subtasks open inside their parent task
194 + $parentTask = $task->parentTask($task->parent_id);
195 + $taskUrl = $this->taskUrl($page_url, $board->id, $parentTask);
113 196
114 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'
115 - .$task->id.'-'.substr($task->title, 0, 10);
116 - $taskLinkTag = '<a target="_blank" href="'
117 - .htmlspecialchars($taskUrl).'">'
118 - .htmlspecialchars($task->title).'</a>';
119 -
120 197 $data = [
121 - 'body' => 'has assigned you to subtask <strong>'
122 - .$task->title.'</strong> of task '
123 - .$taskLinkTag.' on the board '
124 - .$boardLinkTag,
125 - 'pre_header' => 'you have been assigned to subtask',
126 - 'show_footer' => true, 'user' => $assignee,
127 - 'userData' => $userData,
128 - 'site_url' => site_url(),
129 - 'site_title' => get_bloginfo('name'),
130 - 'site_logo' => fluent_boards_site_logo(),
198 + 'badge' => ['label' => __('Subtask assigned', 'fluent-boards'), 'tone' => 'accent'],
199 + // translators: %s is the name of the person who assigned the subtask
200 + 'heading' => sprintf(__('%s assigned you to a subtask', 'fluent-boards'), $userData['display_name']),
201 + 'task_card' => [
202 + 'title' => $task->title,
203 + 'parent_title' => $parentTask->title,
204 + 'parent_url' => $taskUrl,
205 + 'rows' => $this->taskRows($task, $board, $boardUrl, false),
206 + ],
207 + 'cta' => ['label' => __('Open subtask', 'fluent-boards'), 'url' => $taskUrl],
208 + 'pre_header' => $this->taskPreHeader($task, $board),
131 209 ];
210 + // translators: %s is the subtask title
211 + $mailSubject = sprintf(__('You’re assigned to subtask “%s”', 'fluent-boards'), $task->title);
132 212 }
133 213
134 - $mailSubject = 'You have been assigned to task';
214 + $data = array_merge($this->getBrandingData(), $data, [
215 + 'email_reason' => __('You’re getting this because you were assigned to this task.', 'fluent-boards'),
216 + 'show_footer' => true,
217 + 'user' => $assignee,
218 + 'userData' => $userData,
219 + ]);
220 +
135 221 $message = Helper::loadView('emails.assignee2', $data);
136 - $headers = ['Content-Type: text/html; charset=UTF-8'];
137 222
223 + (new Mailer($assignee->user_email, $mailSubject, $message))->sendNotification($context);
138 224
139 - \wp_mail($assignee->user_email, $mailSubject, $message, $headers);
140 -
141 - } catch (\Exception $e) {
142 - throw new \Exception('Error in sending mail to new assignees', 1);
225 + } catch (\Throwable $e) {
226 + Mailer::reportFailure($context, $e);
143 227 }
144 228 }
145 229
146 230 public function sendEmailForRemoveAssignee(
@@ -147,8 +231,9 @@
147 231 $taskId,
148 232 $newAssigneeId,
149 233 $current_user_id
150 234 ) {
235 + $context = ['type' => 'remove_assignee', 'task_id' => $taskId];
151 236 try {
152 237 $task = Task::findOrFail($taskId);
153 238 $board = $task->board;
154 239 $assignee = User::findOrFail($newAssigneeId);
@@ -153,62 +238,64 @@
153 238 $board = $task->board;
154 239 $assignee = User::findOrFail($newAssigneeId);
155 240
156 241 $userData = $this->getUserData($current_user_id);
157 -
158 242 $page_url = fluent_boards_page_url();
243 + $boardUrl = $page_url.'boards/'.$board->id;
159 244
160 - $boardUrl = $page_url.'boards/'.$board->id;
161 - $boardLinkTag = '<a target="_blank" href="'
162 - .htmlspecialchars($boardUrl).'">'
163 - .htmlspecialchars($board->title).'</a>';
164 -
165 245 if (null == $task->parent_id) {
166 246 // this is a task
167 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'
168 - .$task->id.'-'.substr($task->title, 0, 10);
169 - $taskLinkTag = '<a target="_blank" href="'
170 - .htmlspecialchars($taskUrl).'">'
171 - .htmlspecialchars($task->title).'</a>';
172 - $data = [
173 - 'body' => 'has removed you from task '.$taskLinkTag
174 - .' on the board '.$boardLinkTag,
175 - 'pre_header' => 'you have been removed from task',
176 - 'show_footer' => true,
177 - 'userData' => $userData,
178 - 'site_url' => site_url(),
179 - 'site_title' => get_bloginfo('name'),
180 - 'site_logo' => fluent_boards_site_logo(),
247 + $taskUrl = $this->taskUrl($page_url, $board->id, $task);
248 +
249 + $data = [
250 + // translators: %s is the name of the person who removed the assignee
251 + 'heading' => sprintf(__('%s removed you from a task', 'fluent-boards'), $userData['display_name']),
252 + 'task_card' => [
253 + 'title' => $task->title,
254 + 'rows' => $this->taskRows($task, $board, $boardUrl, true, false),
255 + 'muted' => true,
256 + ],
257 + 'note' => __('You won’t get updates about this task unless you’re mentioned or assigned again.', 'fluent-boards'),
258 + 'cta' => ['label' => __('View task', 'fluent-boards'), 'url' => $taskUrl, 'style' => 'secondary'],
181 259 ];
260 + // translators: %s is the task title
261 + $mailSubject = sprintf(__('You were removed from “%s”', 'fluent-boards'), $task->title);
182 262 } else {
183 - // this is a subtask
184 - $task = $task->parentTask($task->parent_id);
263 + // this is a subtask; subtasks open inside their parent task
264 + $parentTask = $task->parentTask($task->parent_id);
265 + $taskUrl = $this->taskUrl($page_url, $board->id, $parentTask);
185 266
186 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'
187 - .$task->id.'-'.substr($task->title, 0, 10);
188 - $taskLinkTag = '<a target="_blank" href="'
189 - .htmlspecialchars($taskUrl).'">'
190 - .htmlspecialchars($task->title).'</a>';
191 -
192 267 $data = [
193 - 'body' => 'has removed you from subtask <strong>'
194 - .$task->title.'</strong> of task '
195 - .$taskLinkTag.' on the board '
196 - .$boardLinkTag,
197 - 'pre_header' => 'you have been removed from subtask',
198 - 'show_footer' => true,
199 - 'userData' => $userData,
268 + // translators: %s is the name of the person who removed the assignee
269 + 'heading' => sprintf(__('%s removed you from a subtask', 'fluent-boards'), $userData['display_name']),
270 + 'task_card' => [
271 + 'title' => $task->title,
272 + 'parent_title' => $parentTask->title,
273 + 'parent_url' => $taskUrl,
274 + 'rows' => [[__('Board', 'fluent-boards'), $this->link($board->title, $boardUrl)]],
275 + 'muted' => true,
276 + ],
277 + 'note' => __('You won’t get updates about this subtask unless you’re mentioned or assigned again.', 'fluent-boards'),
278 + 'cta' => ['label' => __('View subtask', 'fluent-boards'), 'url' => $taskUrl, 'style' => 'secondary'],
200 279 ];
280 + // translators: %s is the subtask title
281 + $mailSubject = sprintf(__('You were removed from subtask “%s”', 'fluent-boards'), $task->title);
201 282 }
202 283
203 - $mailSubject = 'You have been removed from task';
284 + $data = array_merge($this->getBrandingData(), $data, [
285 + 'badge' => ['label' => __('Unassigned', 'fluent-boards'), 'tone' => 'neutral'],
286 + 'pre_header' => $this->taskPreHeader($task, $board),
287 + 'email_reason' => __('You’re getting this because you were assigned to this task.', 'fluent-boards'),
288 + 'show_footer' => true,
289 + 'userData' => $userData,
290 + ]);
291 +
204 292 $message = Helper::loadView('emails.assignee2', $data);
205 - $headers = ['Content-Type: text/html; charset=UTF-8'];
206 293
207 - \wp_mail($assignee->user_email, $mailSubject, $message, $headers);
294 + (new Mailer($assignee->user_email, $mailSubject, $message))->sendNotification($context);
208 295
209 - } catch (\Exception $e) {
210 - throw new \Exception('Error in sending mail to new assignees', 1);
296 + } catch (\Throwable $e) {
297 + Mailer::reportFailure($context, $e);
211 298 }
212 299 }
213 300
214 301
@@ -216,8 +303,9 @@
216 303 $taskId,
217 304 $newAssigneeEmails,
218 305 $current_user_id
219 306 ) {
307 + $context = ['type' => 'stage_change', 'task_id' => $taskId];
220 308 try {
221 309 $task = Task::findOrFail($taskId);
222 310 $board = $task->board;
223 311
@@ -222,42 +310,34 @@
222 310 $board = $task->board;
223 311
224 312 $userData = $this->getUserData($current_user_id);
225 313
226 - $page_url = fluent_boards_page_url();
227 - $boardUrl = $page_url.'boards/'.$board->id;
228 - $boardLinkTag = '<a target="_blank" href="'
229 - .htmlspecialchars($boardUrl).'">'
230 - .htmlspecialchars($board->title).'</a>';
314 + $page_url = fluent_boards_page_url();
315 + $taskUrl = $this->taskUrl($page_url, $board->id, $task);
316 + $stage = $task->stage ? $task->stage->title : '';
231 317
232 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'.$task->id
233 - .'-'.substr($task->title, 0, 10);
234 - $taskLinkTag = '<a target="_blank" href="'
235 - .htmlspecialchars($taskUrl).'">'
236 - .htmlspecialchars($task->title).'</a>';
318 + $data = array_merge($this->getBrandingData(), [
319 + 'badge' => ['label' => __('Stage changed', 'fluent-boards'), 'tone' => 'accent'],
320 + // translators: %s is the name of the person who moved the task
321 + 'heading' => sprintf(__('%s moved a task', 'fluent-boards'), $userData['display_name']),
322 + 'context' => $this->contextLine($task->title, $taskUrl, $board->title, $page_url.'boards/'.$board->id),
323 + 'change' => ['label' => __('Now in', 'fluent-boards'), 'to' => $stage],
324 + 'cta' => ['label' => __('Open task', 'fluent-boards'), 'url' => $taskUrl],
325 + 'pre_header' => $this->taskPreHeader($task, $board),
326 + 'email_reason' => __('You’re getting this because you’re assigned to this task.', 'fluent-boards'),
327 + 'show_footer' => true,
328 + 'userData' => $userData,
329 + ]);
237 330
238 - $data = [
239 - 'body' => 'has moved '.$taskLinkTag.' task to <strong>'
240 - .$task->stage->title
241 - .'</strong> stage of board '.$boardLinkTag,
242 - 'pre_header' => 'Task stage has been changed',
243 - 'show_footer' => true,
244 - 'userData' => $userData,
245 - 'site_url' => site_url(),
246 - 'site_title' => get_bloginfo('name'),
247 - 'site_logo' => fluent_boards_site_logo(),
248 - ];
249 -
250 -
251 - $mailSubject = 'Task stage has been changed';
331 + // translators: %1$s is the task title, %2$s is the stage title
332 + $mailSubject = sprintf(__('“%1$s” moved to %2$s', 'fluent-boards'), $task->title, $stage);
252 333 $message = Helper::loadView('emails.assignee2', $data);
253 - $headers = ['Content-Type: text/html; charset=UTF-8'];
254 334
255 335 foreach ($newAssigneeEmails as $assignee_email) {
256 - \wp_mail($assignee_email, $mailSubject, $message, $headers);
336 + (new Mailer($assignee_email, $mailSubject, $message))->sendNotification($context);
257 337 }
258 - } catch (\Exception $e) {
259 - throw new \Exception('Error in sending mail to new assignees', 1);
338 + } catch (\Throwable $e) {
339 + Mailer::reportFailure($context, $e);
260 340 }
261 341 }
262 342
263 343 public function sendEmailForDueDateUpdate(
@@ -264,8 +344,9 @@
264 344 $taskId,
265 345 $newAssigneeEmails,
266 346 $current_user_id
267 347 ) {
348 + $context = ['type' => 'due_date_update', 'task_id' => $taskId];
268 349 try {
269 350 $task = Task::findOrFail($taskId);
270 351 $board = $task->board;
271 352
@@ -270,42 +351,34 @@
270 351 $board = $task->board;
271 352
272 353 $userData = $this->getUserData($current_user_id);
273 354
274 - $page_url = fluent_boards_page_url();
275 - $boardUrl = $page_url.'boards/'.$board->id;
276 - $boardLinkTag = '<a target="_blank" href="'
277 - .htmlspecialchars($boardUrl).'">'
278 - .htmlspecialchars($board->title).'</a>';
355 + $page_url = fluent_boards_page_url();
356 + $taskUrl = $this->taskUrl($page_url, $board->id, $task);
357 + $dueDate = $task->due_at ? $this->formatDate($task->due_at) : __('No due date', 'fluent-boards');
279 358
280 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'.$task->id
281 - .'-'.substr($task->title, 0, 10);
282 - $taskLinkTag = '<a target="_blank" href="'
283 - .htmlspecialchars($taskUrl).'">'
284 - .htmlspecialchars($task->title).'</a>';
359 + $data = array_merge($this->getBrandingData(), [
360 + 'badge' => ['label' => __('Due date changed', 'fluent-boards'), 'tone' => 'accent'],
361 + // translators: %s is the name of the person who changed the due date
362 + 'heading' => sprintf(__('%s changed a due date', 'fluent-boards'), $userData['display_name']),
363 + 'context' => $this->contextLine($task->title, $taskUrl, $board->title, $page_url.'boards/'.$board->id),
364 + 'change' => ['label' => __('Now due', 'fluent-boards'), 'to' => $dueDate],
365 + 'cta' => ['label' => __('Open task', 'fluent-boards'), 'url' => $taskUrl],
366 + 'pre_header' => $this->taskPreHeader($task, $board),
367 + 'email_reason' => __('You’re getting this because you’re assigned to this task.', 'fluent-boards'),
368 + 'show_footer' => true,
369 + 'userData' => $userData,
370 + ]);
285 371
286 - $data = [
287 - 'body' => 'has updated due date of '.$taskLinkTag
288 - .' task to <strong>'.$task->due_at
289 - .'</strong> of board '.$boardLinkTag,
290 - 'pre_header' => 'Task due date has been changed',
291 - 'show_footer' => true,
292 - 'userData' => $userData,
293 - 'site_url' => site_url(),
294 - 'site_title' => get_bloginfo('name'),
295 - 'site_logo' => fluent_boards_site_logo(),
296 - ];
297 -
298 -
299 - $mailSubject = 'Task due date has been changed';
372 + // translators: %s is the task title
373 + $mailSubject = sprintf(__('Due date changed for “%s”', 'fluent-boards'), $task->title);
300 374 $message = Helper::loadView('emails.assignee2', $data);
301 - $headers = ['Content-Type: text/html; charset=UTF-8'];
302 375
303 376 foreach ($newAssigneeEmails as $assignee_email) {
304 - \wp_mail($assignee_email, $mailSubject, $message, $headers);
377 + (new Mailer($assignee_email, $mailSubject, $message))->sendNotification($context);
305 378 }
306 - } catch (\Exception $e) {
307 - throw new \Exception('Error in sending mail to new assignees', 1);
379 + } catch (\Throwable $e) {
380 + Mailer::reportFailure($context, $e);
308 381 }
309 382 }
310 383
311 384 public function sendEmailForArchivedTask(
@@ -312,8 +385,9 @@
312 385 $taskId,
313 386 $newAssigneeEmails,
314 387 $current_user_id
315 388 ) {
389 + $context = ['type' => 'task_archived', 'task_id' => $taskId];
316 390 try {
317 391 $task = Task::findOrFail($taskId);
318 392 $board = $task->board;
319 393
@@ -318,42 +392,141 @@
318 392 $board = $task->board;
319 393
320 394 $userData = $this->getUserData($current_user_id);
321 395
322 - $page_url = fluent_boards_page_url();
323 - $boardUrl = $page_url.'boards/'.$board->id;
324 - $boardLinkTag = '<a target="_blank" href="'
325 - .htmlspecialchars($boardUrl).'">'
326 - .htmlspecialchars($board->title).'</a>';
396 + $page_url = fluent_boards_page_url();
397 + $boardUrl = $page_url.'boards/'.$board->id;
398 + $taskUrl = $this->taskUrl($page_url, $board->id, $task);
327 399
328 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'.$task->id
329 - .'-'.substr($task->title, 0, 10);
330 - $taskLinkTag = '<a target="_blank" href="'
331 - .htmlspecialchars($taskUrl).'">'
332 - .htmlspecialchars($task->title).'</a>';
400 + $data = array_merge($this->getBrandingData(), [
401 + 'badge' => ['label' => __('Archived', 'fluent-boards'), 'tone' => 'neutral'],
402 + // translators: %s is the name of the person who archived the task
403 + 'heading' => sprintf(__('%s archived a task', 'fluent-boards'), $userData['display_name']),
404 + 'task_card' => [
405 + 'title' => $task->title,
406 + 'rows' => $this->taskRows($task, $board, $boardUrl, true, false),
407 + 'muted' => true,
408 + ],
409 + 'note' => __('Archived tasks can be restored from the board’s archive.', 'fluent-boards'),
410 + 'cta' => ['label' => __('View task', 'fluent-boards'), 'url' => $taskUrl, 'style' => 'secondary'],
411 + 'pre_header' => $this->taskPreHeader($task, $board),
412 + 'email_reason' => __('You’re getting this because you’re assigned to this task.', 'fluent-boards'),
413 + 'show_footer' => true,
414 + 'userData' => $userData,
415 + ]);
333 416
334 - $data = [
335 - 'body' => 'has archived '.$taskLinkTag.' task of board '
336 - .$boardLinkTag,
337 - 'pre_header' => 'Task has been archived',
338 - 'show_footer' => true,
339 - 'userData' => $userData,
340 - 'site_url' => site_url(),
341 - 'site_title' => get_bloginfo('name'),
342 - 'site_logo' => fluent_boards_site_logo(),
343 - ];
417 + // translators: %s is the task title
418 + $mailSubject = sprintf(__('“%s” was archived', 'fluent-boards'), $task->title);
419 + $message = Helper::loadView('emails.assignee2', $data);
344 420
421 + foreach ($newAssigneeEmails as $assignee_email) {
422 + (new Mailer($assignee_email, $mailSubject, $message))->sendNotification($context);
423 + }
424 + } catch (\Throwable $e) {
425 + Mailer::reportFailure($context, $e);
426 + }
427 + }
345 428
346 - $mailSubject = 'Task has been archived';
347 - $message = Helper::loadView('emails.assignee2', $data);
348 - $headers = ['Content-Type: text/html; charset=UTF-8'];
429 + /**
430 + * Resolve current recipient emails after revalidating board access and preferences.
431 + *
432 + * Email values are accepted only for queued actions created before recipient IDs
433 + * were introduced. They are mapped back to current WordPress users before checks.
434 + *
435 + * @param array $queuedRecipients
436 + * @param int $boardId
437 + * @return array
438 + */
439 + private function getEligibleCommentRecipientEmails($queuedRecipients, $boardId)
440 + {
441 + $legacyEmails = [];
442 + foreach ((array) $queuedRecipients as $queuedRecipient) {
443 + if (is_string($queuedRecipient) && !ctype_digit($queuedRecipient)) {
444 + $legacyEmail = sanitize_email($queuedRecipient);
445 + if ($legacyEmail && is_email($legacyEmail)) {
446 + $legacyEmails[strtolower($legacyEmail)] = $legacyEmail;
447 + }
448 + }
449 + }
349 450
350 - foreach ($newAssigneeEmails as $assignee_email) {
351 - \wp_mail($assignee_email, $mailSubject, $message, $headers);
451 + $legacyUserIdsByEmail = [];
452 + if ($legacyEmails) {
453 + $legacyUsers = User::whereIn('user_email', array_values($legacyEmails))
454 + ->get(['ID', 'user_email']);
455 + foreach ($legacyUsers as $legacyUser) {
456 + $legacyUserIdsByEmail[strtolower($legacyUser->user_email)] = absint($legacyUser->ID);
352 457 }
353 - } catch (\Exception $e) {
354 - throw new \Exception('Error in sending mail to new assignees', 1);
355 458 }
459 +
460 + $recipientUserIds = [];
461 + $seenRecipientUserIds = [];
462 + foreach ((array) $queuedRecipients as $queuedRecipient) {
463 + $recipientUserId = 0;
464 +
465 + if (is_int($queuedRecipient) || (is_string($queuedRecipient) && ctype_digit($queuedRecipient))) {
466 + $recipientUserId = absint($queuedRecipient);
467 + } elseif (is_string($queuedRecipient)) {
468 + $legacyEmail = sanitize_email($queuedRecipient);
469 + if ($legacyEmail) {
470 + $recipientUserId = $legacyUserIdsByEmail[strtolower($legacyEmail)] ?? 0;
471 + }
472 + }
473 +
474 + if (!$recipientUserId || isset($seenRecipientUserIds[$recipientUserId])) {
475 + continue;
476 + }
477 + $seenRecipientUserIds[$recipientUserId] = true;
478 + $recipientUserIds[] = $recipientUserId;
479 + }
480 +
481 + if (!$recipientUserIds) {
482 + return [];
483 + }
484 +
485 + $eligibleRecipientIds = [];
486 + $boardRelations = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
487 + ->where('object_id', absint($boardId))
488 + ->whereIn('foreign_id', $recipientUserIds)
489 + ->get(['foreign_id', 'preferences']);
490 +
491 + foreach ($boardRelations as $boardRelation) {
492 + $preferences = maybe_unserialize($boardRelation->preferences);
493 + if (!is_array($preferences)) {
494 + continue;
495 + }
496 +
497 + if (
498 + !array_key_exists(Constant::BOARD_EMAIL_COMMENT, $preferences) ||
499 + $preferences[Constant::BOARD_EMAIL_COMMENT]
500 + ) {
501 + $eligibleRecipientIds[absint($boardRelation->foreign_id)] = true;
502 + }
503 + }
504 +
505 + if (!$eligibleRecipientIds) {
506 + return [];
507 + }
508 +
509 + $usersById = [];
510 + $recipients = User::whereIn('ID', array_keys($eligibleRecipientIds))
511 + ->get(['ID', 'user_email']);
512 + foreach ($recipients as $recipient) {
513 + $usersById[absint($recipient->ID)] = $recipient;
514 + }
515 +
516 + $recipientEmails = [];
517 + foreach ($recipientUserIds as $recipientUserId) {
518 + if (!isset($eligibleRecipientIds[$recipientUserId], $usersById[$recipientUserId])) {
519 + continue;
520 + }
521 +
522 + $email = sanitize_email($usersById[$recipientUserId]->user_email);
523 + if ($email && is_email($email)) {
524 + $recipientEmails[] = $email;
525 + }
526 + }
527 +
528 + return $recipientEmails;
356 529 }
357 530
358 531 private function getUserData($userId)
359 532 {
@@ -366,5 +539,76 @@
366 539 'photo' => $gravaterPhoto,
367 540 ];
368 541 }
369 542
543 + /**
544 + * Site branding passed to every email view (header logo or initial, footer name).
545 + *
546 + * @return array
547 + */
548 + private function getBrandingData()
549 + {
550 + return [
551 + 'site_url' => site_url(),
552 + 'site_title' => get_bloginfo('name'),
553 + 'site_logo' => fluent_boards_site_logo(),
554 + ];
555 + }
556 +
557 + private function taskUrl($pageUrl, $boardId, $task)
558 + {
559 + return $pageUrl.'boards/'.$boardId.'/tasks/'.$task->id.'-'.substr($task->title, 0, 10);
560 + }
561 +
562 + private function link($text, $url)
563 + {
564 + return '<a class="fbs_email_link" target="_blank" href="'.esc_url($url).'" style="color: #335CFF; font-weight: 600; text-decoration: none;">'
565 + .esc_html($text).'</a>';
566 + }
567 +
568 + /**
569 + * "Task · Board" line under an email headline, both linked.
570 + */
571 + private function contextLine($taskTitle, $taskUrl, $boardTitle, $boardUrl)
572 + {
573 + return $this->link($taskTitle, $taskUrl).' &middot; '.$this->link($boardTitle, $boardUrl);
574 + }
575 +
576 + /**
577 + * Label/value rows for the task card. Values are HTML and already escaped.
578 + *
579 + * @return array
580 + */
581 + private function taskRows($task, $board, $boardUrl, $withStage = true, $withDue = true)
582 + {
583 + $rows = [[__('Board', 'fluent-boards'), $this->link($board->title, $boardUrl)]];
584 +
585 + if ($withStage && $task->stage) {
586 + $rows[] = [__('Stage', 'fluent-boards'), esc_html($task->stage->title)];
587 + }
588 +
589 + if ($withDue && $task->due_at) {
590 + $rows[] = [__('Due', 'fluent-boards'), esc_html($this->formatDate($task->due_at))];
591 + }
592 +
593 + return $rows;
594 + }
595 +
596 + private function taskPreHeader($task, $board)
597 + {
598 + $parts = [$board->title];
599 + if ($task->due_at) {
600 + // translators: %s is the due date
601 + $parts[] = sprintf(__('Due %s', 'fluent-boards'), $this->formatDate($task->due_at));
602 + }
603 +
604 + return implode(' · ', $parts);
605 + }
606 +
607 + /**
608 + * Due dates are stored in site time; format them with the site's date and time settings.
609 + */
610 + private function formatDate($dateTime)
611 + {
612 + return date_i18n(get_option('date_format').' '.get_option('time_format'), strtotime($dateTime));
613 + }
370 614 }