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 +420 -263 1.21 → 2.2.0 View file →
@@ -4,26 +4,29 @@
4 4
5 5 use FluentBoards\App\Models\Board;
6 6 use FluentBoards\App\Models\Comment;
7 7 use FluentBoards\App\Models\Meta;
8 +use FluentBoards\App\Models\Relation;
8 9 use FluentBoards\App\Models\Task;
9 10 use FluentBoards\App\Models\User;
10 11 use FluentBoards\App\Services\Constant;
11 12 use FluentBoards\App\Services\Helper;
13 +use FluentBoards\App\Services\Libs\Mailer;
12 14
13 15 class ScheduleHandler
14 16 {
15 17 public function sendEmailForComment(
16 18 $commentId,
17 - $usersToSendEmail,
19 + $recipientUserIds,
18 20 $current_user_id
19 21 ) {
22 + $context = ['type' => 'comment', 'comment_id' => $commentId];
20 23 try {
21 24 $comment = Comment::find($commentId) ?? null;
22 25 if ( ! $comment) {
23 26 return;
24 27 }
25 - if ('comment' != $comment->type) {
28 + if (!in_array($comment->type, ['comment', 'reply'])) {
26 29 return;
27 30 }
28 31
29 32 $task = Task::findOrFail($comment->task_id);
@@ -29,58 +32,138 @@
29 32 $task = Task::findOrFail($comment->task_id);
30 33 if ( ! $task) {
31 34 return;
32 35 }
33 -// $assignees = $task->assignees;
36 +
37 + $usersToSendEmail = $this->getEligibleCommentRecipientEmails(
38 + $recipientUserIds,
39 + $task->board_id
40 + );
41 +
42 + if (!$usersToSendEmail) {
43 + return;
44 + }
45 +
34 46 $board = $task->board;
47 +
35 48 $page_url = fluent_boards_page_url();
49 + $redirectUrl = site_url('/') . '?redirect=to_task&taskId='.$task->id;
36 50
37 - $user = $task->user($comment->created_by);
51 + $userData = $this->getUserData($current_user_id);
38 52
39 - $userData = $this->getUserData($current_user_id);
53 + $taskUrl = $this->taskUrl($page_url, $board->id, $task);
40 54
41 - $boardUrl = $page_url.'boards/'.$board->id;
42 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'.$task->id.'-'
43 - .substr($task->title, 0, 10);
55 + $isReply = $comment->type == 'reply';
44 56
45 - $userLinkTag = '<strong>'.htmlspecialchars($user->display_name)
46 - .'</strong>';
47 - $taskLinkTag = '<a target="_blank" href="'
48 - .htmlspecialchars($taskUrl).'">'
49 - .htmlspecialchars($task->title).'</a>';
50 - $boardLinkTag = '<a target="_blank" href="'
51 - .htmlspecialchars($boardUrl).'">'
52 - .htmlspecialchars($board->title).'</a>';
57 + if ($isReply) {
58 + $parentComment = $comment->parent_id ? Comment::find($comment->parent_id) : null;
53 59
54 - $data = [
55 - 'body' => 'commented on '.$taskLinkTag.' on '
56 - .$boardLinkTag.' board.',
57 - 'pre_header' => 'comment added in fluent task',
58 - '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),
59 86 'comment' => $comment->description,
60 87 'userData' => $userData,
61 - 'site_url' => site_url(),
62 - 'site_title' => get_bloginfo('name'),
63 - 'site_logo' => fluent_boards_site_logo(),
64 - ];
88 + 'show_footer' => true,
89 + ]);
65 90
66 - $mailSubject = 'new comment added on task';
67 91 $message = Helper::loadView('emails.comment2', $data);
68 - $headers = ['Content-Type: text/html; charset=UTF-8'];
69 92
70 93 foreach ($usersToSendEmail as $email) {
71 - \wp_mail($email, $mailSubject, $message, $headers);
94 + (new Mailer($email, $mailSubject, $message))->sendNotification($context);
72 95 }
73 - } catch (\Exception $e) {
74 - // do nothing // better to log here
96 + } catch (\Throwable $e) {
97 + Mailer::reportFailure($context, $e);
75 98 }
76 99 }
77 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 +
78 160 public function sendEmailForAddAssignee(
79 161 $taskId,
80 162 $newAssigneeId,
81 163 $current_user_id
82 164 ) {
165 + $context = ['type' => 'add_assignee', 'task_id' => $taskId];
83 166 try {
84 167 $task = Task::findOrFail($taskId);
85 168 $board = $task->board;
86 169 $assignee = User::findOrFail($newAssigneeId);
@@ -85,65 +168,63 @@
85 168 $board = $task->board;
86 169 $assignee = User::findOrFail($newAssigneeId);
87 170
88 171 $userData = $this->getUserData($current_user_id);
172 + $page_url = fluent_boards_page_url();
173 + $boardUrl = $page_url.'boards/'.$board->id;
89 174
90 - $page_url = fluent_boards_page_url();
91 - $boardUrl = $page_url.'boards/'.$board->id;
92 - $boardLinkTag = '<a target="_blank" href="'
93 - .htmlspecialchars($boardUrl).'">'
94 - .htmlspecialchars($board->title).'</a>';
95 -
96 175 if (null == $task->parent_id) {
97 176 // this is a task
98 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'
99 - .$task->id.'-'.substr($task->title, 0, 10);
100 - $taskLinkTag = '<a target="_blank" href="'
101 - .htmlspecialchars($taskUrl).'">'
102 - .htmlspecialchars($task->title).'</a>';
103 - $data = [
104 - 'body' => 'has assigned you to task '.$taskLinkTag
105 - .' on the board '.$boardLinkTag,
106 - 'pre_header' => 'you have been assigned to task',
107 - 'show_footer' => true,
108 - 'userData' => $userData,
109 - 'site_url' => site_url(),
110 - 'site_title' => get_bloginfo('name'),
111 - '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),
112 189 ];
190 + // translators: %s is the task title
191 + $mailSubject = sprintf(__('You’re assigned to “%s”', 'fluent-boards'), $task->title);
113 192 } else {
114 - // this is a subtask
115 - $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);
116 196
117 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'
118 - .$task->id.'-'.substr($task->title, 0, 10);
119 - $taskLinkTag = '<a target="_blank" href="'
120 - .htmlspecialchars($taskUrl).'">'
121 - .htmlspecialchars($task->title).'</a>';
122 -
123 197 $data = [
124 - 'body' => 'has assigned you to subtask <strong>'
125 - .$task->title.'</strong> of task '
126 - .$taskLinkTag.' on the board '
127 - .$boardLinkTag,
128 - 'pre_header' => 'you have been assigned to subtask',
129 - 'show_footer' => true, 'user' => $assignee,
130 - 'userData' => $userData,
131 - 'site_url' => site_url(),
132 - 'site_title' => get_bloginfo('name'),
133 - '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),
134 209 ];
210 + // translators: %s is the subtask title
211 + $mailSubject = sprintf(__('You’re assigned to subtask “%s”', 'fluent-boards'), $task->title);
135 212 }
136 213
137 - $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 +
138 221 $message = Helper::loadView('emails.assignee2', $data);
139 - $headers = ['Content-Type: text/html; charset=UTF-8'];
140 222
223 + (new Mailer($assignee->user_email, $mailSubject, $message))->sendNotification($context);
141 224
142 - \wp_mail($assignee->user_email, $mailSubject, $message, $headers);
143 -
144 - } catch (\Exception $e) {
145 - throw new \Exception('Error in sending mail to new assignees', 1);
225 + } catch (\Throwable $e) {
226 + Mailer::reportFailure($context, $e);
146 227 }
147 228 }
148 229
149 230 public function sendEmailForRemoveAssignee(
@@ -150,8 +231,9 @@
150 231 $taskId,
151 232 $newAssigneeId,
152 233 $current_user_id
153 234 ) {
235 + $context = ['type' => 'remove_assignee', 'task_id' => $taskId];
154 236 try {
155 237 $task = Task::findOrFail($taskId);
156 238 $board = $task->board;
157 239 $assignee = User::findOrFail($newAssigneeId);
@@ -156,62 +238,64 @@
156 238 $board = $task->board;
157 239 $assignee = User::findOrFail($newAssigneeId);
158 240
159 241 $userData = $this->getUserData($current_user_id);
160 -
161 242 $page_url = fluent_boards_page_url();
243 + $boardUrl = $page_url.'boards/'.$board->id;
162 244
163 - $boardUrl = $page_url.'boards/'.$board->id;
164 - $boardLinkTag = '<a target="_blank" href="'
165 - .htmlspecialchars($boardUrl).'">'
166 - .htmlspecialchars($board->title).'</a>';
167 -
168 245 if (null == $task->parent_id) {
169 246 // this is a task
170 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'
171 - .$task->id.'-'.substr($task->title, 0, 10);
172 - $taskLinkTag = '<a target="_blank" href="'
173 - .htmlspecialchars($taskUrl).'">'
174 - .htmlspecialchars($task->title).'</a>';
175 - $data = [
176 - 'body' => 'has removed you from task '.$taskLinkTag
177 - .' on the board '.$boardLinkTag,
178 - 'pre_header' => 'you have been removed from task',
179 - 'show_footer' => true,
180 - 'userData' => $userData,
181 - 'site_url' => site_url(),
182 - 'site_title' => get_bloginfo('name'),
183 - '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'],
184 259 ];
260 + // translators: %s is the task title
261 + $mailSubject = sprintf(__('You were removed from “%s”', 'fluent-boards'), $task->title);
185 262 } else {
186 - // this is a subtask
187 - $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);
188 266
189 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'
190 - .$task->id.'-'.substr($task->title, 0, 10);
191 - $taskLinkTag = '<a target="_blank" href="'
192 - .htmlspecialchars($taskUrl).'">'
193 - .htmlspecialchars($task->title).'</a>';
194 -
195 267 $data = [
196 - 'body' => 'has removed you from subtask <strong>'
197 - .$task->title.'</strong> of task '
198 - .$taskLinkTag.' on the board '
199 - .$boardLinkTag,
200 - 'pre_header' => 'you have been removed from subtask',
201 - 'show_footer' => true,
202 - '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'],
203 279 ];
280 + // translators: %s is the subtask title
281 + $mailSubject = sprintf(__('You were removed from subtask “%s”', 'fluent-boards'), $task->title);
204 282 }
205 283
206 - $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 +
207 292 $message = Helper::loadView('emails.assignee2', $data);
208 - $headers = ['Content-Type: text/html; charset=UTF-8'];
209 293
210 - \wp_mail($assignee->user_email, $mailSubject, $message, $headers);
294 + (new Mailer($assignee->user_email, $mailSubject, $message))->sendNotification($context);
211 295
212 - } catch (\Exception $e) {
213 - throw new \Exception('Error in sending mail to new assignees', 1);
296 + } catch (\Throwable $e) {
297 + Mailer::reportFailure($context, $e);
214 298 }
215 299 }
216 300
217 301
@@ -219,8 +303,9 @@
219 303 $taskId,
220 304 $newAssigneeEmails,
221 305 $current_user_id
222 306 ) {
307 + $context = ['type' => 'stage_change', 'task_id' => $taskId];
223 308 try {
224 309 $task = Task::findOrFail($taskId);
225 310 $board = $task->board;
226 311
@@ -225,42 +310,34 @@
225 310 $board = $task->board;
226 311
227 312 $userData = $this->getUserData($current_user_id);
228 313
229 - $page_url = fluent_boards_page_url();
230 - $boardUrl = $page_url.'boards/'.$board->id;
231 - $boardLinkTag = '<a target="_blank" href="'
232 - .htmlspecialchars($boardUrl).'">'
233 - .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 : '';
234 317
235 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'.$task->id
236 - .'-'.substr($task->title, 0, 10);
237 - $taskLinkTag = '<a target="_blank" href="'
238 - .htmlspecialchars($taskUrl).'">'
239 - .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 + ]);
240 330
241 - $data = [
242 - 'body' => 'has moved '.$taskLinkTag.' task to <strong>'
243 - .$task->stage->title
244 - .'</strong> stage of board '.$boardLinkTag,
245 - 'pre_header' => 'Task stage has been changed',
246 - 'show_footer' => true,
247 - 'userData' => $userData,
248 - 'site_url' => site_url(),
249 - 'site_title' => get_bloginfo('name'),
250 - 'site_logo' => fluent_boards_site_logo(),
251 - ];
252 -
253 -
254 - $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);
255 333 $message = Helper::loadView('emails.assignee2', $data);
256 - $headers = ['Content-Type: text/html; charset=UTF-8'];
257 334
258 335 foreach ($newAssigneeEmails as $assignee_email) {
259 - \wp_mail($assignee_email, $mailSubject, $message, $headers);
336 + (new Mailer($assignee_email, $mailSubject, $message))->sendNotification($context);
260 337 }
261 - } catch (\Exception $e) {
262 - throw new \Exception('Error in sending mail to new assignees', 1);
338 + } catch (\Throwable $e) {
339 + Mailer::reportFailure($context, $e);
263 340 }
264 341 }
265 342
266 343 public function sendEmailForDueDateUpdate(
@@ -267,8 +344,9 @@
267 344 $taskId,
268 345 $newAssigneeEmails,
269 346 $current_user_id
270 347 ) {
348 + $context = ['type' => 'due_date_update', 'task_id' => $taskId];
271 349 try {
272 350 $task = Task::findOrFail($taskId);
273 351 $board = $task->board;
274 352
@@ -273,42 +351,34 @@
273 351 $board = $task->board;
274 352
275 353 $userData = $this->getUserData($current_user_id);
276 354
277 - $page_url = fluent_boards_page_url();
278 - $boardUrl = $page_url.'boards/'.$board->id;
279 - $boardLinkTag = '<a target="_blank" href="'
280 - .htmlspecialchars($boardUrl).'">'
281 - .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');
282 358
283 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'.$task->id
284 - .'-'.substr($task->title, 0, 10);
285 - $taskLinkTag = '<a target="_blank" href="'
286 - .htmlspecialchars($taskUrl).'">'
287 - .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 + ]);
288 371
289 - $data = [
290 - 'body' => 'has updated due date of '.$taskLinkTag
291 - .' task to <strong>'.$task->due_at
292 - .'</strong> of board '.$boardLinkTag,
293 - 'pre_header' => 'Task due date has been changed',
294 - 'show_footer' => true,
295 - 'userData' => $userData,
296 - 'site_url' => site_url(),
297 - 'site_title' => get_bloginfo('name'),
298 - 'site_logo' => fluent_boards_site_logo(),
299 - ];
300 -
301 -
302 - $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);
303 374 $message = Helper::loadView('emails.assignee2', $data);
304 - $headers = ['Content-Type: text/html; charset=UTF-8'];
305 375
306 376 foreach ($newAssigneeEmails as $assignee_email) {
307 - \wp_mail($assignee_email, $mailSubject, $message, $headers);
377 + (new Mailer($assignee_email, $mailSubject, $message))->sendNotification($context);
308 378 }
309 - } catch (\Exception $e) {
310 - throw new \Exception('Error in sending mail to new assignees', 1);
379 + } catch (\Throwable $e) {
380 + Mailer::reportFailure($context, $e);
311 381 }
312 382 }
313 383
314 384 public function sendEmailForArchivedTask(
@@ -315,8 +385,9 @@
315 385 $taskId,
316 386 $newAssigneeEmails,
317 387 $current_user_id
318 388 ) {
389 + $context = ['type' => 'task_archived', 'task_id' => $taskId];
319 390 try {
320 391 $task = Task::findOrFail($taskId);
321 392 $board = $task->board;
322 393
@@ -321,42 +392,141 @@
321 392 $board = $task->board;
322 393
323 394 $userData = $this->getUserData($current_user_id);
324 395
325 - $page_url = fluent_boards_page_url();
326 - $boardUrl = $page_url.'boards/'.$board->id;
327 - $boardLinkTag = '<a target="_blank" href="'
328 - .htmlspecialchars($boardUrl).'">'
329 - .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);
330 399
331 - $taskUrl = $page_url.'boards/'.$board->id.'/tasks/'.$task->id
332 - .'-'.substr($task->title, 0, 10);
333 - $taskLinkTag = '<a target="_blank" href="'
334 - .htmlspecialchars($taskUrl).'">'
335 - .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 + ]);
336 416
337 - $data = [
338 - 'body' => 'has archived '.$taskLinkTag.' task of board '
339 - .$boardLinkTag,
340 - 'pre_header' => 'Task has been archived',
341 - 'show_footer' => true,
342 - 'userData' => $userData,
343 - 'site_url' => site_url(),
344 - 'site_title' => get_bloginfo('name'),
345 - 'site_logo' => fluent_boards_site_logo(),
346 - ];
417 + // translators: %s is the task title
418 + $mailSubject = sprintf(__('“%s” was archived', 'fluent-boards'), $task->title);
419 + $message = Helper::loadView('emails.assignee2', $data);
347 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 + }
348 428
349 - $mailSubject = 'Task has been archived';
350 - $message = Helper::loadView('emails.assignee2', $data);
351 - $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 + }
352 450
353 - foreach ($newAssigneeEmails as $assignee_email) {
354 - \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);
355 457 }
356 - } catch (\Exception $e) {
357 - throw new \Exception('Error in sending mail to new assignees', 1);
358 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;
359 529 }
360 530
361 531 private function getUserData($userId)
362 532 {
@@ -369,89 +539,76 @@
369 539 'photo' => $gravaterPhoto,
370 540 ];
371 541 }
372 542
373 - public function sendInvitationViaEmail($boardId, $email, $current_user_id)
543 + /**
544 + * Site branding passed to every email view (header logo or initial, footer name).
545 + *
546 + * @return array
547 + */
548 + private function getBrandingData()
374 549 {
375 - try {
376 - $userData = $this->getUserData($current_user_id);
550 + return [
551 + 'site_url' => site_url(),
552 + 'site_title' => get_bloginfo('name'),
553 + 'site_logo' => fluent_boards_site_logo(),
554 + ];
555 + }
377 556
378 - $board = Board::findOrFail($boardId) ?? null;
379 - if (!$board) {
380 - return;
381 - }
557 + private function taskUrl($pageUrl, $boardId, $task)
558 + {
559 + return $pageUrl.'boards/'.$boardId.'/tasks/'.$task->id.'-'.substr($task->title, 0, 10);
560 + }
382 561
383 - $page_url = fluent_boards_page_url();
384 -
385 - $hashCode = $this->hashGenerate(20);
386 -
387 - $siteUrl = add_query_arg( array(
388 - 'fbs' => 1,
389 - 'invitation' => 'board',
390 - 'email' => $email,
391 - 'hash' => $hashCode,
392 - 'bid' => $boardId
393 - ), site_url('index.php') );
394 -
395 - $data = [
396 - 'body' => 'has invited you to join board: '. $board->title ,
397 - 'pre_header' => __('join board invitation in fluent boards', 'fluent-boards'),
398 - 'btn_title' => __('Join Board', 'fluent-boards'),
399 - 'show_footer' => true,
400 - 'userData' => $userData,
401 - 'boardLink' => $siteUrl,
402 - 'site_url' => site_url(),
403 - 'site_title' => get_bloginfo('name'),
404 - 'site_logo' => fluent_boards_site_logo(),
405 - ];
406 -
407 - $mailSubject = 'Invitation for joining board';
408 - $message = Helper::loadView('emails.invite-external', $data);
409 - $headers = ['Content-Type: text/html; charset=UTF-8'];
410 -
411 - $this->saveHashByEmail($boardId, $email, $hashCode);
412 -
413 - \wp_mail($email, $mailSubject, $message, $headers);
414 - } catch (\Exception $e) {
415 - // do nothing // better to log here
416 - }
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>';
417 566 }
418 567
419 - private function hashGenerate($chars)
568 + /**
569 + * "Task · Board" line under an email headline, both linked.
570 + */
571 + private function contextLine($taskTitle, $taskUrl, $boardTitle, $boardUrl)
420 572 {
421 - $data = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcefghijklmnopqrstuvwxyz';
422 - return substr(str_shuffle($data), 0, $chars);
573 + return $this->link($taskTitle, $taskUrl).' &middot; '.$this->link($boardTitle, $boardUrl);
423 574 }
424 575
425 - private function saveHashByEmail($boardId, $email, $hash)
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)
426 582 {
427 - $this->deleteHashCode($boardId, $email);
583 + $rows = [[__('Board', 'fluent-boards'), $this->link($board->title, $boardUrl)]];
428 584
429 - $fbs_meta = new Meta();
430 - $fbs_meta->object_id = $boardId;
431 - $fbs_meta->object_type = Constant::OBJECT_TYPE_BOARD;
432 - $fbs_meta->key = Constant::BOARD_INVITATION;
433 - $fbs_meta->value = ['email' => $email, 'hash' => $hash];
434 - $fbs_meta->save();
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;
435 594 }
436 595
437 - private function deleteHashCode($boardId, $email)
596 + private function taskPreHeader($task, $board)
438 597 {
439 - $activeHashCodes = $this->getActiveHashCodes($boardId);
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 + }
440 603
441 - foreach ($activeHashCodes as $savedHash) {
442 - $value = maybe_unserialize($savedHash->value);
443 - if($value['email'] == $email){
444 - Meta::where('id', $savedHash->id)->delete();
445 - }
446 - }
604 + return implode(' · ', $parts);
447 605 }
448 606
449 - private function getActiveHashCodes($boardId)
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)
450 611 {
451 - return Meta::query()->where('object_id', $boardId)
452 - ->where('object_type', Constant::OBJECT_TYPE_BOARD)
453 - ->where('key', Constant::BOARD_INVITATION)
454 - ->get();
612 + return date_i18n(get_option('date_format').' '.get_option('time_format'), strtotime($dateTime));
455 613 }
456 -
457 614 }