Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,10 @@ AWS_USE_PATH_STYLE_ENDPOINT=true
JWT_SECRET=2hoccgHb9r1fqW1lU16C6khSHVa7O0eai6FxkWK95UtQ0LqNDTO5mq1RzDwcq18I
JWT_ALGO=HS256

# SES Bounce/Complaint Handling
# AWS_SNS_TOPIC_ARN=arn:aws:sns:us-east-1:ACCOUNT:ses-notifications
# AWS_SNS_VERIFY_SIGNATURE=true
# SES_SUPPRESSION_ENABLED=false

# Only required for SAAS mode and if your're charging fees
# OPEN_EXCHANGE_RATES_APP_ID=
7 changes: 7 additions & 0 deletions backend/app/DomainObjects/EmailSuppressionDomainObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace HiEvents\DomainObjects;

class EmailSuppressionDomainObject extends Generated\EmailSuppressionDomainObjectAbstract
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<?php

namespace HiEvents\DomainObjects\Generated;

/**
* THIS FILE IS AUTOGENERATED - DO NOT EDIT IT DIRECTLY.
* @package HiEvents\DomainObjects\Generated
*/
abstract class EmailSuppressionDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject
{
final public const SINGULAR_NAME = 'email_suppression';
final public const PLURAL_NAME = 'email_suppressions';
final public const ID = 'id';
final public const ACCOUNT_ID = 'account_id';
final public const EMAIL = 'email';
final public const REASON = 'reason';
final public const BOUNCE_TYPE = 'bounce_type';
final public const BOUNCE_SUB_TYPE = 'bounce_sub_type';
final public const COMPLAINT_TYPE = 'complaint_type';
final public const SOURCE = 'source';
final public const SNS_MESSAGE_ID = 'sns_message_id';
final public const RAW_PAYLOAD = 'raw_payload';
final public const CREATED_AT = 'created_at';
final public const UPDATED_AT = 'updated_at';
final public const DELETED_AT = 'deleted_at';

protected int $id;
protected ?int $account_id = null;
protected string $email;
protected string $reason;
protected ?string $bounce_type = null;
protected ?string $bounce_sub_type = null;
protected ?string $complaint_type = null;
protected string $source;
protected ?string $sns_message_id = null;
protected mixed $raw_payload = null;
protected ?string $created_at = null;
protected ?string $updated_at = null;
protected ?string $deleted_at = null;

public function toArray(): array
{
return [
'id' => $this->id ?? null,
'account_id' => $this->account_id ?? null,
'email' => $this->email ?? null,
'reason' => $this->reason ?? null,
'bounce_type' => $this->bounce_type ?? null,
'bounce_sub_type' => $this->bounce_sub_type ?? null,
'complaint_type' => $this->complaint_type ?? null,
'source' => $this->source ?? null,
'sns_message_id' => $this->sns_message_id ?? null,
'raw_payload' => $this->raw_payload ?? null,
'created_at' => $this->created_at ?? null,
'updated_at' => $this->updated_at ?? null,
'deleted_at' => $this->deleted_at ?? null,
];
}

public function setId(int $id): self
{
$this->id = $id;
return $this;
}

public function getId(): int
{
return $this->id;
}

public function setAccountId(?int $account_id): self
{
$this->account_id = $account_id;
return $this;
}

public function getAccountId(): ?int
{
return $this->account_id;
}

public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}

public function getEmail(): string
{
return $this->email;
}

public function setReason(string $reason): self
{
$this->reason = $reason;
return $this;
}

public function getReason(): string
{
return $this->reason;
}

public function setBounceType(?string $bounce_type): self
{
$this->bounce_type = $bounce_type;
return $this;
}

public function getBounceType(): ?string
{
return $this->bounce_type;
}

public function setBounceSubType(?string $bounce_sub_type): self
{
$this->bounce_sub_type = $bounce_sub_type;
return $this;
}

public function getBounceSubType(): ?string
{
return $this->bounce_sub_type;
}

public function setComplaintType(?string $complaint_type): self
{
$this->complaint_type = $complaint_type;
return $this;
}

public function getComplaintType(): ?string
{
return $this->complaint_type;
}

public function setSource(string $source): self
{
$this->source = $source;
return $this;
}

public function getSource(): string
{
return $this->source;
}

public function setSnsMessageId(?string $sns_message_id): self
{
$this->sns_message_id = $sns_message_id;
return $this;
}

public function getSnsMessageId(): ?string
{
return $this->sns_message_id;
}

public function setRawPayload(mixed $raw_payload): self
{
$this->raw_payload = $raw_payload;
return $this;
}

public function getRawPayload(): mixed
{
return $this->raw_payload;
}

public function setCreatedAt(?string $created_at): self
{
$this->created_at = $created_at;
return $this;
}

public function getCreatedAt(): ?string
{
return $this->created_at;
}

public function setUpdatedAt(?string $updated_at): self
{
$this->updated_at = $updated_at;
return $this;
}

public function getUpdatedAt(): ?string
{
return $this->updated_at;
}

public function setDeletedAt(?string $deleted_at): self
{
$this->deleted_at = $deleted_at;
return $this;
}

public function getDeletedAt(): ?string
{
return $this->deleted_at;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ abstract class OutgoingMessageDomainObjectAbstract extends \HiEvents\DomainObjec
final public const SUBJECT = 'subject';
final public const RECIPIENT = 'recipient';
final public const STATUS = 'status';
final public const SES_MESSAGE_ID = 'ses_message_id';
final public const CREATED_AT = 'created_at';
final public const UPDATED_AT = 'updated_at';
final public const DELETED_AT = 'deleted_at';
Expand All @@ -26,6 +27,7 @@ abstract class OutgoingMessageDomainObjectAbstract extends \HiEvents\DomainObjec
protected string $subject;
protected string $recipient;
protected string $status;
protected ?string $ses_message_id = null;
protected ?string $created_at = null;
protected ?string $updated_at = null;
protected ?string $deleted_at = null;
Expand All @@ -39,6 +41,7 @@ public function toArray(): array
'subject' => $this->subject ?? null,
'recipient' => $this->recipient ?? null,
'status' => $this->status ?? null,
'ses_message_id' => $this->ses_message_id ?? null,
'created_at' => $this->created_at ?? null,
'updated_at' => $this->updated_at ?? null,
'deleted_at' => $this->deleted_at ?? null,
Expand Down Expand Up @@ -111,6 +114,17 @@ public function getStatus(): string
return $this->status;
}

public function setSesMessageId(?string $ses_message_id): self
{
$this->ses_message_id = $ses_message_id;
return $this;
}

public function getSesMessageId(): ?string
{
return $this->ses_message_id;
}

public function setCreatedAt(?string $created_at): self
{
$this->created_at = $created_at;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace HiEvents\DomainObjects\Status;

enum EmailSuppressionReasonEnum: string
{
case BOUNCE = 'bounce';
case COMPLAINT = 'complaint';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace HiEvents\DomainObjects\Status;

enum EmailSuppressionSourceEnum: string
{
case SES_NOTIFICATION = 'ses_notification';
case MANUAL = 'manual';
}
2 changes: 2 additions & 0 deletions backend/app/DomainObjects/Status/OutgoingMessageStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ enum OutgoingMessageStatus
{
case SENT;
case FAILED;
case SUPPRESSED;
case BOUNCED;
}
11 changes: 11 additions & 0 deletions backend/app/Exceptions/SnsSignatureVerificationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace HiEvents\Exceptions;

use Exception;

class SnsSignatureVerificationException extends Exception
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace HiEvents\Http\Actions\Admin\EmailSuppressions;

use HiEvents\DomainObjects\Enums\Role;
use HiEvents\DomainObjects\Status\EmailSuppressionReasonEnum;
use HiEvents\DomainObjects\Status\EmailSuppressionSourceEnum;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Http\Resources\Admin\EmailSuppressionResource;
use HiEvents\Services\Domain\Email\EmailSuppressionService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class CreateEmailSuppressionAction extends BaseAction
{
public function __construct(
private readonly EmailSuppressionService $emailSuppressionService,
) {
}

public function __invoke(Request $request): JsonResponse
{
$this->minimumAllowedRole(Role::SUPERADMIN);

$validated = $request->validate([
'email' => ['required', 'email', 'max:255'],
'reason' => ['required', 'in:' . implode(',', array_column(EmailSuppressionReasonEnum::cases(), 'value'))],
'bounce_type' => ['nullable', 'string', 'max:50'],
'complaint_type' => ['nullable', 'string', 'max:50'],
]);

$suppression = $this->emailSuppressionService->suppressEmail(
email: $validated['email'],
reason: $validated['reason'],
source: EmailSuppressionSourceEnum::MANUAL->value,
bounceType: $validated['bounce_type'] ?? null,
complaintType: $validated['complaint_type'] ?? null,
);

return $this->resourceResponse(
resource: EmailSuppressionResource::class,
data: $suppression,
statusCode: 201,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace HiEvents\Http\Actions\Admin\EmailSuppressions;

use HiEvents\DomainObjects\Enums\Role;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Services\Domain\Email\EmailSuppressionService;
use Illuminate\Http\Response;

class DeleteEmailSuppressionAction extends BaseAction
{
public function __construct(
private readonly EmailSuppressionService $emailSuppressionService,
) {
}

public function __invoke(int $suppression_id): Response
{
$this->minimumAllowedRole(Role::SUPERADMIN);

$this->emailSuppressionService->removeSuppressionById($suppression_id);

return $this->deletedResponse();
}
}
Loading
Loading