Skip to content
Closed
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], '+');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Empty recipients crash processing

When an SMS has an empty to array, the inherited validation permits it, but this line accesses index 0 and passes null to ltrim(), causing the send operation to fail with a TypeError instead of returning a messaging response.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Utopia/Messaging/Adapter/SMS/VonageMessages.php
Line: 61

Comment:
**Empty recipients crash processing**

When an SMS has an empty `to` array, the inherited validation permits it, but this line accesses index 0 and passes null to `ltrim()`, causing the send operation to fail with a `TypeError` instead of returning a messaging response.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

$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.');
}
Comment on lines +18 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Adapter behavior lacks deterministic coverage

These tests skip without live credentials and exercise only successful deliveries, leaving the new request construction and HTTP, transport, and malformed-response handling untested in normal CI. Add deterministic client or local-server tests that assert the outgoing request and representative error responses.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/Messaging/Adapter/SMS/VonageMessagesTest.php
Line: 18-20

Comment:
**Adapter behavior lacks deterministic coverage**

These tests skip without live credentials and exercise only successful deliveries, leaving the new request construction and HTTP, transport, and malformed-response handling untested in normal CI. Add deterministic client or local-server tests that assert the outgoing request and representative error responses.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex


$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);
}
}