'gSystem', 'systemnumber' => 'gSystem', // valor antiguo del plugin 'system' => 'gSystem', 'gtext' => 'gText', 'text' => 'gText', // valor antiguo del plugin 'gshort' => 'gShort', 'shortcode' => 'gShort', // valor antiguo del plugin 'gown' => 'gOwn', 'from' => 'gOwn', // valor antiguo del plugin 'own' => 'gOwn', 'gmobile' => 'gMobile', 'gpush' => 'gPush', 'gprofile' => 'gProfile', 'profile' => 'gProfile', ]; public function __construct(EmailConfig $config) { $this->config = $config; } public function send(string $to, string $message): array { $this->debug_log .= "Attempting to send via Bulkgate\n"; if (!$this->isConfigured()) { return [ 'success' => false, 'error' => 'Bulkgate configuration is incomplete', 'debug_log' => $this->debug_log ]; } try { // El canal decide el endpoint, no es un parametro de la peticion $url = $this->api_url . $this->endpoint(); $data = [ 'application_id' => $this->config->bulkgate_app_id, 'application_token' => $this->config->bulkgate_app_token, 'number' => $to, 'text' => $message, // Unicode solo cuando el texto lo necesita: activarlo siempre // reduce cada segmento a 70 caracteres y encarece el envio 'unicode' => $this->needsUnicode($message) ? '1' : '0', ]; $data = array_merge($data, $this->senderParams()); $this->debug_log .= "Bulkgate endpoint: " . $url . "\n"; $this->debug_log .= "Bulkgate sender: " . ($data['sender_id'] ?? 'gSystem') . ( isset($data['sender_id_value']) ? ' / ' . $data['sender_id_value'] : '' ) . "\n"; //ray($data); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 20); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/x-www-form-urlencoded' ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { $curl_error = curl_error($ch); curl_close($ch); throw new \Exception('Curl error: ' . $curl_error); } curl_close($ch); $this->debug_log .= "Bulkgate HTTP " . $httpCode . ": " . substr((string) $response, 0, 500) . "\n"; $result = json_decode($response, true); if (!is_array($result)) { throw new \Exception('Bulkgate API error: invalid response (HTTP ' . $httpCode . ')'); } // Los errores vienen en la RAIZ de la respuesta: type, code, error, detail if ($httpCode !== 200 || isset($result['error'])) { throw new \Exception('Bulkgate API error: ' . $this->errorMessage($result, $httpCode)); } $status = $result['data']['status'] ?? 'accepted'; if ($status === 'error') { throw new \Exception('Bulkgate message not accepted: ' . $this->errorMessage($result, $httpCode)); } $this->debug_log .= "Bulkgate message ID: " . ($result['data']['sms_id'] ?? $result['data']['message_id'] ?? 'unknown') . "\n"; return [ 'success' => true, 'result' => [ 'provider' => 'bulkgate', 'message_id' => $result['data']['sms_id'] ?? $result['data']['message_id'] ?? null, 'status' => $status, 'credit_used' => $result['data']['price'] ?? $result['data']['credit'] ?? 0 ], 'debug_log' => $this->debug_log ]; } catch (\Exception $e) { $this->debug_log .= "Bulkgate error: " . $e->getMessage() . "\n"; return [ 'success' => false, 'error' => 'Bulkgate sending failed: ' . $e->getMessage(), 'debug_log' => $this->debug_log ]; } } /** * Endpoint segun el canal elegido en los ajustes. * La API Simple solo tiene transactional y promotional. */ protected function endpoint(): string { return $this->config->bulkgate_channel == 'promotional' ? 'promotional' : 'transactional'; } /** * Parametros del remitente: sender_id = TIPO, sender_id_value = valor * * @return array */ protected function senderParams(): array { $type = $this->senderType(); // gSystem es el remitente por defecto de BulkGate y no lleva valor if ($type == 'gSystem') { return ['sender_id' => 'gSystem']; } $value = trim((string) $this->config->bulkgate_sender_value); // Sin valor no se puede usar el tipo elegido if ($value === '') { $this->debug_log .= "Bulkgate sender value is empty, using gSystem\n"; return ['sender_id' => 'gSystem']; } // Nombre alfanumerico: maximo 11 caracteres if ($type == 'gText') { $value = mb_substr($value, 0, 11); } /*ray([ 'sender_id' => $type, 'sender_id_value' => $value ]);*/ return [ 'sender_id' => $type, 'sender_id_value' => $value ]; } /** * Tipo de remitente normalizado al valor que espera BulkGate */ protected function senderType(): string { $type = trim((string) $this->config->bulkgate_sender_type); if ($type === '') { return 'gSystem'; } // Un numero es un ID de perfil de BulkGate, se envia tal cual if (ctype_digit($type)) { return $type; } $key = strtolower($type); if (isset($this->sender_types_map[$key])) { return $this->sender_types_map[$key]; } $this->debug_log .= "Bulkgate unknown sender type '" . $type . "', using gSystem\n"; return 'gSystem'; } /** * Mensaje de error legible a partir de la respuesta de BulkGate */ protected function errorMessage($result, $httpCode): string { $error = $result['error'] ?? $result['data']['error'] ?? null; $type = $result['type'] ?? null; $detail = $result['detail'] ?? null; if (!$error && !$type) { return 'Unknown error (HTTP ' . $httpCode . ')'; } $message = $error ? $error : $type; if ($type && $error) { $message .= ' [' . $type . ']'; } if (!empty($detail)) { $message .= ' - ' . ( is_scalar($detail) ? $detail : json_encode($detail) ); } return $message; } /** * El texto necesita unicode? * Devuelve false si todos los caracteres estan en el alfabeto GSM 03.38, * que es el que permite 160 caracteres por segmento. */ protected function needsUnicode(string $message): bool { $gsm = "@£\$¥èéùìòÇ\nØø\rÅå_ÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà" . "^{}\\[~]|€"; $chars = preg_split('//u', $message, -1, PREG_SPLIT_NO_EMPTY); if (!is_array($chars)) { return true; } foreach ($chars as $char) { if (mb_strpos($gsm, $char) === false) { return true; } } return false; } public function isConfigured(): bool { // El tipo de remitente no es obligatorio: sin el, BulkGate usa gSystem $app_id = $this->config->bulkgate_app_id; $app_token = $this->config->bulkgate_app_token; return !empty($app_id) && !empty($app_token); } public function getDebugLog(): string { return $this->debug_log; } }