Skip to content

Commit 0a0f675

Browse files
authored
Merge pull request #8811 from ProcessMaker/bugfix/FOUR-30587
FOUR-30587 Actions By Email - Handle Replies process creating request…
2 parents 138a47c + 1661121 commit 0a0f675

3 files changed

Lines changed: 285 additions & 0 deletions

File tree

ProcessMaker/Managers/TaskSchedulerManager.php

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
use PDOException;
1717
use ProcessMaker\Facades\WorkflowManager;
1818
use ProcessMaker\Jobs\StartEventConditional;
19+
use ProcessMaker\Models\EnvironmentVariable;
1920
use ProcessMaker\Models\Process;
2021
use ProcessMaker\Models\ProcessRequest;
2122
use ProcessMaker\Models\ProcessRequestLock;
2223
use ProcessMaker\Models\ScheduledTask;
24+
use ProcessMaker\Models\Setting;
2325
use ProcessMaker\Models\TimerExpression;
2426
use ProcessMaker\Nayra\Bpmn\Models\BoundaryEvent;
2527
use ProcessMaker\Nayra\Bpmn\Models\DatePeriod;
@@ -398,13 +400,211 @@ public function executeTimerStartEvent(ScheduledTask $task, $config)
398400
if (!$definitions->findElementById($config->element_id)) {
399401
return;
400402
}
403+
404+
if ($this->shouldSkipHandleRepliesTimerStart($process)) {
405+
Log::info('Skipping Actions By Email Handle Replies timer event because ABE inbound mail configuration is not adequate', [
406+
'process_id' => $process->id,
407+
'process_name' => $process->name,
408+
]);
409+
410+
return;
411+
}
412+
401413
$event = $definitions->getEvent($config->element_id);
402414
$data = [];
403415

404416
//Trigger the start event
405417
$processRequest = WorkflowManager::triggerStartEvent($process, $event, $data);
406418
}
407419

420+
/**
421+
* Determine if the timer should be skipped for the Actions By Email handle replies process.
422+
*
423+
* @param Process $process
424+
* @return bool
425+
*/
426+
private function shouldSkipHandleRepliesTimerStart(Process $process): bool
427+
{
428+
if (!$this->isHandleRepliesProcess($process)) {
429+
return false;
430+
}
431+
432+
return !$this->hasAdequateAbeInboundConfiguration();
433+
}
434+
435+
/**
436+
* Whether Actions By Email has enough configuration to poll inbound mail (IMAP or OAuth).
437+
*
438+
* This is a heuristic in core: the connector may add more keys; we avoid starting the
439+
* Handle Replies timer when the mailbox clearly is not set up (FOUR-30587).
440+
*
441+
* @return bool
442+
*/
443+
private function hasAdequateAbeInboundConfiguration(): bool
444+
{
445+
if (!Setting::readyToUseSettingsDatabase()) {
446+
return false;
447+
}
448+
449+
$authMethodIndex = (int) ($this->getAbeInboundSettingValue(['abe_imap_auth_method']) ?? 0);
450+
$username = $this->getAbeInboundSettingValue(['abe_imap_username', 'email_connector_mail_username']);
451+
452+
if (!$this->hasValue($username)) {
453+
return false;
454+
}
455+
456+
if ($authMethodIndex === 1) {
457+
return $this->hasEnvironmentVariables([
458+
'ABE_GMAIL_API_CLIENT_ID',
459+
'ABE_GMAIL_API_SECRET',
460+
'ABE_GMAIL_API_ACCESS_TOKEN',
461+
'ABE_GMAIL_API_REFRESH_TOKEN',
462+
]);
463+
}
464+
465+
if ($authMethodIndex === 2) {
466+
return $this->hasEnvironmentVariables([
467+
'ABE_OFFICE_365_CLIENT_ID',
468+
'ABE_OFFICE_365_TENANT_ID',
469+
'ABE_OFFICE_365_SECRET',
470+
'ABE_OFFICE_365_ACCESS_TOKEN',
471+
'ABE_OFFICE_365_REFRESH_TOKEN',
472+
'ABE_OFFICE_365_ACCESS_TOKEN_EXPIRE_DATE',
473+
]);
474+
}
475+
476+
return $this->hasStandardAbeInboundConfiguration();
477+
}
478+
479+
/**
480+
* IMAP configuration for standard authentication mode.
481+
*/
482+
private function hasStandardAbeInboundConfiguration(): bool
483+
{
484+
$password = $this->getAbeInboundSettingValue(['abe_imap_password', 'email_connector_mail_password']);
485+
if (!$this->hasValue($password)) {
486+
return false;
487+
}
488+
489+
$inboxUri = $this->getAbeInboundSettingValue(['abe_imap_inbox_uri']);
490+
if ($this->hasValue($inboxUri)) {
491+
return true;
492+
}
493+
494+
$server = $this->getAbeInboundSettingValue(['abe_imap_server', 'email_connector_mail_host']);
495+
$port = $this->getAbeInboundSettingValue(['abe_imap_port', 'email_connector_mail_port']);
496+
497+
return $this->hasValue($server) && $this->hasValue($port);
498+
}
499+
500+
/**
501+
* Reads the first non-empty value from supported Actions By Email / mail settings keys.
502+
*
503+
* @param array $keys
504+
* @return string|null
505+
*/
506+
private function getAbeInboundSettingValue(array $keys): ?string
507+
{
508+
$settings = Setting::query()
509+
->whereIn('key', $keys)
510+
->get()
511+
->keyBy('key');
512+
513+
if ($settings->isEmpty()) {
514+
return null;
515+
}
516+
517+
foreach ($keys as $key) {
518+
$setting = $settings->get($key);
519+
if (!$setting) {
520+
continue;
521+
}
522+
523+
$value = $this->extractSettingValue($setting->config);
524+
if ($this->hasValue($value)) {
525+
return (string) $value;
526+
}
527+
}
528+
529+
return null;
530+
}
531+
532+
/**
533+
* Normalize setting values from different possible setting formats.
534+
*
535+
* @param mixed $value
536+
* @return mixed
537+
*/
538+
private function extractSettingValue($value)
539+
{
540+
if (is_object($value)) {
541+
$value = (array) $value;
542+
}
543+
544+
if (is_array($value)) {
545+
if (array_key_exists('value', $value)) {
546+
return $value['value'];
547+
}
548+
549+
return $value;
550+
}
551+
552+
return $value;
553+
}
554+
555+
/**
556+
* Check if a setting value is present and not empty.
557+
*
558+
* @param mixed $value
559+
* @return bool
560+
*/
561+
private function hasValue($value): bool
562+
{
563+
if (is_array($value)) {
564+
foreach ($value as $item) {
565+
if ($this->hasValue($item)) {
566+
return true;
567+
}
568+
}
569+
570+
return false;
571+
}
572+
573+
return is_scalar($value) && trim((string) $value) !== '';
574+
}
575+
576+
/**
577+
* Identify the handle replies process by name.
578+
*
579+
* @param Process $process
580+
* @return bool
581+
*/
582+
private function isHandleRepliesProcess(Process $process): bool
583+
{
584+
if ((string) $process->package_key === 'package-actions-by-email/handle-replies') {
585+
return true;
586+
}
587+
588+
$name = (string) $process->name;
589+
590+
return stripos($name, 'actions by email') !== false && stripos($name, 'handle replies') !== false;
591+
}
592+
593+
private function hasEnvironmentVariables(array $names): bool
594+
{
595+
$values = EnvironmentVariable::query()
596+
->whereIn('name', $names)
597+
->pluck('value', 'name');
598+
599+
foreach ($names as $name) {
600+
if (!isset($values[$name]) || trim((string) $values[$name]) === '') {
601+
return false;
602+
}
603+
}
604+
605+
return true;
606+
}
607+
408608
/**
409609
* Execute a timer start event
410610
*

tests/Feature/Api/TimerStartEventTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,25 @@ public function testScheduleMustNotStartTimerEventWhenProcessInactive()
228228
$task->type = 'TIMER_START_EVENT';
229229
$manager->executeTimerStartEvent($task, json_decode($task->configuration));
230230
}
231+
232+
public function testScheduleMustNotStartHandleRepliesTimerWhenAbeInboundConfigInadequate()
233+
{
234+
// triggerStartEvent must not run when ABE inbound mail settings are missing/inadequate
235+
WorkflowManager::shouldReceive('triggerStartEvent')
236+
->never()
237+
->with(\Mockery::any(), \Mockery::any(), \Mockery::any());
238+
239+
$data = [];
240+
$data['name'] = 'Actions By Email - Handle Replies';
241+
$data['bpmn'] = Process::getProcessTemplate('TimerStartEvent.bpmn');
242+
243+
$process = Process::factory()->create($data);
244+
245+
$manager = new TaskSchedulerManager();
246+
$task = new ScheduledTask();
247+
$task->process_id = $process->id;
248+
$task->configuration = '{"type":"TimeCycle","interval":"R4\/2019-02-13T13:08:00Z\/PT1M", "element_id" : "_9"}';
249+
$task->type = 'TIMER_START_EVENT';
250+
$manager->executeTimerStartEvent($task, json_decode($task->configuration));
251+
}
231252
}

tests/unit/ProcessMaker/Managers/TaskSchedulerManagerTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
use Carbon\Carbon;
66
use DateTime;
7+
use ProcessMaker\Models\EnvironmentVariable;
8+
use ProcessMaker\Models\Process;
9+
use ProcessMaker\Models\Setting;
10+
use ReflectionMethod;
711
use Tests\TestCase;
812

913
class TaskSchedulerManagerTest extends TestCase
@@ -74,4 +78,64 @@ public function testTruncateDates()
7478
$rounded = $this->manager->truncateDateTime($date);
7579
$this->assertEquals('00:01:00', $rounded->format('H:i:s'));
7680
}
81+
82+
public function testHasAdequateAbeInboundConfigurationForStandardAuth()
83+
{
84+
Setting::updateOrCreate(['key' => 'abe_imap_auth_method'], ['config' => '0']);
85+
Setting::updateOrCreate(['key' => 'abe_imap_username'], ['config' => 'abe@test.com']);
86+
Setting::updateOrCreate(['key' => 'abe_imap_password'], ['config' => '123Test']);
87+
Setting::updateOrCreate(['key' => 'abe_imap_server'], ['config' => 'imap.example.com']);
88+
Setting::updateOrCreate(['key' => 'abe_imap_port'], ['config' => '993']);
89+
90+
$this->assertTrue((bool) $this->invokePrivateMethod('hasAdequateAbeInboundConfiguration'));
91+
}
92+
93+
public function testHasAdequateAbeInboundConfigurationForGoogleOauth()
94+
{
95+
Setting::updateOrCreate(['key' => 'abe_imap_auth_method'], ['config' => '1']);
96+
Setting::updateOrCreate(['key' => 'abe_imap_username'], ['config' => 'abe@test.com']);
97+
98+
foreach ([
99+
'ABE_GMAIL_API_CLIENT_ID',
100+
'ABE_GMAIL_API_SECRET',
101+
'ABE_GMAIL_API_ACCESS_TOKEN',
102+
'ABE_GMAIL_API_REFRESH_TOKEN',
103+
] as $name) {
104+
EnvironmentVariable::factory()->create([
105+
'name' => $name,
106+
'value' => 'value-' . strtolower($name),
107+
]);
108+
}
109+
110+
$this->assertTrue((bool) $this->invokePrivateMethod('hasAdequateAbeInboundConfiguration'));
111+
}
112+
113+
public function testHasAdequateAbeInboundConfigurationReturnsFalseWhenUsernameIsMissing()
114+
{
115+
Setting::updateOrCreate(['key' => 'abe_imap_auth_method'], ['config' => '0']);
116+
Setting::updateOrCreate(['key' => 'abe_imap_username'], ['config' => '']);
117+
Setting::updateOrCreate(['key' => 'abe_imap_password'], ['config' => '123Test']);
118+
Setting::updateOrCreate(['key' => 'abe_imap_server'], ['config' => 'imap.example.com']);
119+
Setting::updateOrCreate(['key' => 'abe_imap_port'], ['config' => '993']);
120+
121+
$this->assertFalse((bool) $this->invokePrivateMethod('hasAdequateAbeInboundConfiguration'));
122+
}
123+
124+
public function testIsHandleRepliesProcessUsesPackageKey()
125+
{
126+
$process = new Process([
127+
'package_key' => 'package-actions-by-email/handle-replies',
128+
'name' => 'Anything',
129+
]);
130+
131+
$this->assertTrue((bool) $this->invokePrivateMethod('isHandleRepliesProcess', [$process]));
132+
}
133+
134+
private function invokePrivateMethod(string $method, array $args = [])
135+
{
136+
$reflection = new ReflectionMethod($this->manager, $method);
137+
$reflection->setAccessible(true);
138+
139+
return $reflection->invokeArgs($this->manager, $args);
140+
}
77141
}

0 commit comments

Comments
 (0)