Skip to content
Open
108 changes: 108 additions & 0 deletions src/Utopia/Messaging/Adapter/SMS/VonageMessages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

namespace Utopia\Messaging\Adapter\SMS;

use Utopia\Messaging\Adapter\SMS as SMSAdapter;
use Utopia\Messaging\Messages\SMS as SMSMessage;
use Utopia\Messaging\Response;

// Vonage Messages API SMS Adapter.
// This adapter uses the modern Vonage Messages API (V1) which is more cost-effective
// and versatile than the legacy SMS API.
// https://developer.vonage.com/en/api/messages
class VonageMessages extends SMSAdapter
{
protected const NAME = 'Vonage Messages';

public function __construct(
private string $apiKey,
private string $apiSecret,
private ?string $from = null
) {
}

// Vonage Messages API endpoint.
protected function getApiEndpoint(): string
{
return 'https://api.vonage.com/v1/messages';
}

// Generates the Basic Authorization header.
protected function getAuthorizationHeader(): string
{
return 'Basic ' . \base64_encode("{$this->apiKey}:{$this->apiSecret}");
}

// Sets common headers for the API request.
protected function getRequestHeaders(): array
{
return [
"Authorization: {$this->getAuthorizationHeader()}",
'Content-Type: application/json',
'Accept: application/json',
'User-Agent: Utopia Messaging',
];
}

// Get adapter name.
public function getName(): string
{
return static::NAME;
}

// Get max messages per request.
public function getMaxMessagesPerRequest(): int
{
return 1;
}

protected function process(SMSMessage $message): array
{
$to = \ltrim($message->getTo()[0], '+');
$from = $this->from ?? $message->getFrom();
$from = $from !== null ? \ltrim($from, '+') : null;

$response = new Response($this->getType());

if (empty($from)) {
$response->addResult($message->getTo()[0], 'The "from" field is required for the Vonage Messages API.');
return $response->toArray();
}

$result = $this->request(
method: 'POST',
url: $this->getApiEndpoint(),
headers: $this->getRequestHeaders(),
body: [
'message_type' => 'text',
'to' => $to,
'from' => $from,
'text' => $message->getContent(),
'channel' => 'sms',
],
);

if ($result['statusCode'] === 202) {
$response->setDeliveredTo(1);
$response->addResult($message->getTo()[0]);
} else {
$errorMessage = "Error {$result['statusCode']}";

if (\is_array($result['response'])) {
if (isset($result['response']['detail'])) {
$errorMessage = $result['response']['detail'];
} elseif (isset($result['response']['title'])) {
$errorMessage = $result['response']['title'];
}
} elseif (!empty($result['error'])) {
$errorMessage = $result['error'];
} elseif (\is_string($result['response']) && !empty($result['response'])) {
$errorMessage = "Error {$result['statusCode']}: " . \mb_strimwidth(\strip_tags($result['response']), 0, 100, '...');
}

$response->addResult($message->getTo()[0], $errorMessage);
}

return $response->toArray();
}
}
65 changes: 65 additions & 0 deletions tests/Messaging/Adapter/SMS/VonageMessagesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Utopia\Tests\Adapter\SMS;

use Utopia\Messaging\Adapter\SMS\VonageMessages;
use Utopia\Messaging\Messages\SMS;
use Utopia\Tests\Adapter\Base;

class VonageMessagesTest extends Base
{
// Tests sending an SMS with the 'from' number set directly in the adapter.
public function testSendSMS(): void
{
$apiKey = \getenv('VONAGE_API_KEY');
$apiSecret = \getenv('VONAGE_API_SECRET');
$to = \getenv('VONAGE_TO');

if (!$apiKey || !$apiSecret || !$to) {
$this->markTestSkipped('Vonage Messages credentials or recipient are not available.');
}

$sender = new VonageMessages(
apiKey: $apiKey,
apiSecret: $apiSecret,
from: \getenv('VONAGE_FROM') ?: 'Vonage',
);

$message = new SMS(
to: [$to],
content: 'Test Content',
);

$response = $sender->send($message);

$this->assertResponse($response);
}

// Tests sending an SMS where the 'from' number is provided by the message object.
public function testSendSMSWithFallbackFrom(): void
{
$apiKey = \getenv('VONAGE_API_KEY');
$apiSecret = \getenv('VONAGE_API_SECRET');
$to = \getenv('VONAGE_TO');
$from = \getenv('VONAGE_FROM') ?: null;

if (!$apiKey || !$apiSecret || !$to || !$from) {
$this->markTestSkipped('Vonage Messages credentials or sender/recipient are not available.');
}

$sender = new VonageMessages(
apiKey: $apiKey,
apiSecret: $apiSecret,
);

$message = new SMS(
to: [$to],
content: 'Test Content',
from: $from,
);

$response = $sender->send($message);

$this->assertResponse($response);
}
}
Loading