Gesgocom.GesSms 1.0.1

GesSms

Librería .NET 10.0 para envío de SMS a través de la plataforma LabsMobile. Incluye resiliencia HTTP integrada (reintentos, circuit breaker, timeout).

Tabla de Contenidos


Instalación

Desde NuGet privado (Gesgocom)

# Registrar el origen NuGet (una vez por máquina)
dotnet nuget add source https://nuget.gesgocom.es/v3/index.json -n Gesgocom

# Instalar el paquete
dotnet add package Gesgocom.GesSms

Configuración

Opción 1: Configuración desde appsettings.json (Recomendado)

{
  "LabsMobile": {
    "Username": "tu_usuario",
    "ApiToken": "tu_token_api",
    "Sender": "MiApp"
  }
}
// Program.cs
builder.Services.AddLabsMobileSms(builder.Configuration.GetSection("LabsMobile"));

Opción 2: Configuración en código

builder.Services.AddLabsMobileSms(options =>
{
    options.Username = "tu_usuario";
    options.ApiToken = "tu_token_api";
    options.Sender = "MiApp";        // o DefaultSenderId
    options.TestMode = false;        // Opcional
    options.TimeoutSeconds = 30;     // Opcional
});

Opciones de Configuración Disponibles

Propiedad Tipo Descripción Requerido
Username string Usuario de la cuenta LabsMobile
ApiToken string Token API generado en el panel
Sender string Identificador del remitente por defecto No
BaseUrl string URL de la API (por defecto: https://api.labsmobile.com/json/) No
TimeoutSeconds int Timeout de peticiones HTTP (por defecto: 30) No
TestMode bool Modo prueba sin descontar créditos (por defecto: false) No

Uso Básico

Inyección del Servicio

public class MiServicio
{
    private readonly ILabsMobileSmsService _smsService;
    private readonly ISmsUtils _smsUtils;

    public MiServicio(ILabsMobileSmsService smsService, ISmsUtils smsUtils)
    {
        _smsService = smsService;
        _smsUtils = smsUtils;
    }
}

Envío Simple

var resultado = await _smsService.SendAsync("34609123456", "Hola mundo");

if (resultado.IsSuccess)
{
    Console.WriteLine($"SMS enviado. ID: {resultado.SubId}");
}
else
{
    Console.WriteLine($"Error {resultado.Code}: {resultado.Message}");
}

Envío de SMS

Envío a un Destinatario

// Con remitente por defecto (configurado en options)
var resultado = await _smsService.SendAsync(
    "34609123456",
    "Tu código de verificación es: 123456"
);

// Con remitente específico
var resultado = await _smsService.SendAsync(
    "34609123456",
    "Tu código es: 123456",
    "MiApp"
);

Envío Masivo

var destinatarios = new[] { "34609123456", "34666789012", "34612345678" };

var resultado = await _smsService.SendBulkAsync(
    destinatarios,
    "Mensaje para todos los destinatarios",
    "MiApp"
);

// Máximo 10,000 destinatarios por petición

Envío Programado

var fechaEnvio = DateTime.UtcNow.AddHours(2);

var resultado = await _smsService.ScheduleAsync(
    "34609123456",
    "Este mensaje se enviará en 2 horas",
    fechaEnvio,
    "MiApp"
);

// Guarda resultado.SubId para poder cancelarlo después

Envío de Prueba

// No descuenta créditos, útil para desarrollo
var resultado = await _smsService.SendTestAsync(
    "34609123456",
    "Mensaje de prueba"
);

Envío con Opciones Avanzadas

var request = new SendSmsRequest
{
    Recipients = new List<Recipient>
    {
        new("34609123456"),
        new("34666789012")
    },
    Message = "Mensaje con emojis 😀 y enlaces",
    SenderId = "MiApp",

    // Opciones avanzadas
    Ucs2 = "1",        // Habilitar Unicode/emojis
    Long = "1",        // Permitir SMS concatenados
    SubId = "pedido-12345",  // ID personalizado (máx 20 chars)
    Label = "campaña-navidad", // Metadatos (máx 255 chars)

    // Webhooks
    AckUrl = "https://mi-servidor.com/webhook/delivery",
    ClickUrl = "https://mi-servidor.com/webhook/click"
};

var resultado = await _smsService.SendAsync(request);

Consultas de Cuenta

Consultar Saldo

var balance = await _smsService.GetBalanceAsync();

if (balance.IsSuccess)
{
    Console.WriteLine($"Créditos disponibles: {balance.CreditsDecimal}");

    // Verificar si hay suficientes créditos
    if (balance.CreditsDecimal < 100)
    {
        // Alertar que quedan pocos créditos
    }
}

Consultar Precios

// Precios de varios países
var precios = await _smsService.GetPricesAsync(new[] { "ES", "FR", "US", "MX" });

foreach (var (codigo, precio) in precios.Prices)
{
    Console.WriteLine($"{precio.Name} ({precio.Prefix}): {precio.Credits} créditos");
}

// Precio de un solo país
var precioEspana = await _smsService.GetPriceAsync("ES");
if (precioEspana != null)
{
    Console.WriteLine($"España: {precioEspana.Credits} créditos por SMS");
}

Mensajes Programados

Cancelar Mensaje

// Cancelar un mensaje específico
var resultado = await _smsService.CancelScheduledAsync("65f33a88ceb3d");

if (resultado.IsSuccess)
{
    Console.WriteLine("Mensaje cancelado");
}

Cancelar Todos

var resultado = await _smsService.CancelAllScheduledAsync();

Enviar Inmediatamente

// Enviar ahora un mensaje que estaba programado
var resultado = await _smsService.SendScheduledNowAsync("65f33a88ceb3d");

// Enviar todos los programados inmediatamente
var resultado = await _smsService.SendAllScheduledNowAsync();

Utilidades SMS

La librería incluye utilidades para validar y calcular información de mensajes SMS sin necesidad de enviarlos.

Usar Solo Utilidades (Sin Credenciales)

// Si solo necesitas las utilidades sin el servicio de envío
builder.Services.AddSmsUtils();

Analizar Mensaje Completo

var info = _smsUtils.AnalyzeMessage("Hola, tu código es 123456");

Console.WriteLine($"Caracteres: {info.CharacterCount}");
Console.WriteLine($"Segmentos: {info.SegmentCount}");
Console.WriteLine($"Codificación: {info.Encoding}");
Console.WriteLine($"Caracteres restantes: {info.RemainingChars}");
Console.WriteLine($"Es concatenado: {info.IsConcatenated}");
Console.WriteLine($"Excede límite (4 segmentos): {info.ExceedsRecommendedLimit}");
Console.WriteLine($"Multiplicador créditos: {info.CreditMultiplier}x");

Calcular Segmentos

// GSM-7 (caracteres estándar)
var segmentos1 = _smsUtils.CalculateSegments("Hola mundo"); // 1
var segmentos2 = _smsUtils.CalculateSegments(new string('A', 200)); // 2
var segmentos3 = _smsUtils.CalculateSegments(new string('A', 500)); // 4

// Unicode (emojis, caracteres especiales)
var segmentos4 = _smsUtils.CalculateSegments("Hola 😀"); // 1 (pero Unicode)
var segmentos5 = _smsUtils.CalculateSegments("😀" + new string('A', 100)); // 2

Detectar Codificación

var encoding1 = _smsUtils.DetectEncoding("Texto normal con ñ"); // Gsm7
var encoding2 = _smsUtils.DetectEncoding("Con emoji 🎉"); // Unicode
var encoding3 = _smsUtils.DetectEncoding("Текст на русском"); // Unicode

Validar Compatibilidad GSM-7

var mensaje = "Tu pedido está listo 📦";

if (_smsUtils.IsGsmCompatible(mensaje))
{
    Console.WriteLine("Usa GSM-7: 160 chars/segmento");
}
else
{
    var invalidChars = _smsUtils.GetInvalidGsmCharacters(mensaje);
    Console.WriteLine($"Requiere Unicode (70 chars/segmento)");
    Console.WriteLine($"Caracteres problemáticos: {string.Join(", ", invalidChars)}");
}

Contar Caracteres GSM

// Algunos caracteres GSM extendidos ocupan 2 espacios: ^ { } \ [ ~ ] | €
var count1 = _smsUtils.CountGsmCharacters("Hola"); // 4
var count2 = _smsUtils.CountGsmCharacters("Precio: 100€"); // 13 (€ cuenta como 2)
var count3 = _smsUtils.CountGsmCharacters("Array[0]"); // 10 ([ y ] cuentan como 2)

Validar Números de Teléfono

// Validación completa
var result = _smsUtils.ValidatePhoneNumber("+34 609 123 456");

if (result.IsValid)
{
    Console.WriteLine($"Normalizado: {result.NormalizedNumber}"); // 34609123456
    Console.WriteLine($"País: {result.CountryCode}"); // 34
}
else
{
    Console.WriteLine($"Error: {result.ErrorMessage}");
}

// Con código de país por defecto
var result2 = _smsUtils.ValidatePhoneNumber("609123456", "34");
// Resultado: 34609123456

// Normalización directa
var normalized = _smsUtils.NormalizePhoneNumber("609-123-456", "34"); // 34609123456

// Validación rápida
if (_smsUtils.IsValidPhoneNumber("34609123456"))
{
    // Número válido
}

Estimar Costes

var mensaje = "Tu pedido #12345 está en camino";
var destinatarios = 100;
var precioPorCredito = 1.0m; // Precio del país (ej: España)

var estimacion = _smsUtils.EstimateCost(mensaje, destinatarios, precioPorCredito);

Console.WriteLine($"Segmentos por mensaje: {estimacion.SegmentsPerMessage}");
Console.WriteLine($"Total segmentos: {estimacion.TotalSegments}");
Console.WriteLine($"Créditos por destinatario: {estimacion.CreditsPerRecipient}");
Console.WriteLine($"Créditos totales: {estimacion.TotalCredits}");

Truncar Mensajes

// Truncar a máximo 1 segmento
var truncado = _smsUtils.TruncateMessage(mensajeLargo, maxSegments: 1);

// Truncar a máximo 2 segmentos
var truncado2 = _smsUtils.TruncateMessage(mensajeLargo, maxSegments: 2);

Dividir Mensajes Largos

// Dividir en partes de 1 segmento cada una
var partes = _smsUtils.SplitMessage(mensajeMuyLargo, maxSegmentsPerPart: 1);

foreach (var (parte, index) in partes.Select((p, i) => (p, i)))
{
    Console.WriteLine($"Parte {index + 1}: {parte}");
}

Convertir Unicode a GSM

// Reemplaza caracteres Unicode por equivalentes GSM cuando es posible
var original = "Código: «ABC» — información…";
var converted = _smsUtils.ReplaceUnicodeWithGsm(original);
// Resultado: "Codigo: \"ABC\" - informacion..."

// Útil para reducir costes cuando el contenido lo permite

Caracteres Restantes

var mensaje = "Tu código es: ";
var restantes = _smsUtils.GetRemainingCharacters(mensaje);
Console.WriteLine($"Puedes añadir {restantes} caracteres más en este segmento");

Webhooks

Configurar URLs de Webhook

var request = new SendSmsRequest
{
    Recipients = [new("34609123456")],
    Message = "Mensaje con tracking",
    AckUrl = "https://mi-servidor.com/api/sms/delivery",
    ClickUrl = "https://mi-servidor.com/api/sms/click"
};

Recibir Confirmación de Entrega

LabsMobile envía una petición GET a tu ackurl:

[ApiController]
[Route("api/sms")]
public class SmsWebhookController : ControllerBase
{
    [HttpGet("delivery")]
    public IActionResult DeliveryCallback(
        [FromQuery] string acklevel,
        [FromQuery] string desc,
        [FromQuery] string status,
        [FromQuery] string msisdn,
        [FromQuery] string subid,
        [FromQuery] string timestamp)
    {
        var callback = new DeliveryStatusCallback
        {
            AckLevel = acklevel,    // operator, handset, error
            Description = desc,     // DELIVRD, REJECTD, EXPIRED, etc.
            Status = status,        // ok, ko
            Msisdn = msisdn,
            SubId = subid,
            Timestamp = timestamp
        };

        if (callback.IsDelivered)
        {
            // SMS entregado exitosamente
            _logger.LogInformation("SMS {SubId} entregado a {Msisdn}", subid, msisdn);
        }
        else
        {
            // Error en la entrega
            _logger.LogWarning("SMS {SubId} falló: {Desc}", subid, desc);
        }

        return Ok();
    }
}

Estados de entrega posibles:

  • DELIVRD - Mensaje entregado
  • REJECTD - Mensaje rechazado
  • EXPIRED - Mensaje expirado
  • UNDELIV - No entregable
  • BLOCKED - Bloqueado
  • UNKNOWN - Estado desconocido

Recibir Tracking de Clics

LabsMobile envía una petición POST con JSON a tu clickurl:

[HttpPost("click")]
public IActionResult ClickCallback([FromBody] ClickTrackingCallback callback)
{
    _logger.LogInformation(
        "Clic en mensaje {SubId} desde {Msisdn}, IP: {Ip}",
        callback.SubId,
        callback.Msisdn,
        callback.Ip
    );

    // Registrar el clic para analytics
    _analyticsService.TrackClick(callback.SubId, callback.Msisdn);

    return Ok();
}

Recibir Mensajes Entrantes

Si tienes un número virtual configurado:

[HttpPost("inbound")]
public IActionResult InboundCallback([FromBody] InboundMessageCallback callback)
{
    _logger.LogInformation(
        "Mensaje entrante de {Msisdn}: {Message}",
        callback.Msisdn,
        callback.Message
    );

    // Procesar respuesta del usuario
    await _messageProcessor.ProcessInboundAsync(
        callback.Msisdn,
        callback.Message
    );

    return Ok();
}

Referencia de API

ILabsMobileSmsService

Método Descripción
SendAsync(request) Envío completo con todas las opciones
SendAsync(phone, message, sender?) Envío simple a un destinatario
SendBulkAsync(phones, message, sender?) Envío a múltiples destinatarios
ScheduleAsync(phone, message, datetime, sender?) Programar envío futuro
SendTestAsync(phone, message, sender?) Envío de prueba (sin créditos)
GetBalanceAsync() Consultar saldo de créditos
GetPricesAsync(countryCodes) Consultar precios por país
GetPriceAsync(countryCode) Consultar precio de un país
CancelScheduledAsync(subId) Cancelar mensaje programado
CancelAllScheduledAsync() Cancelar todos los programados
SendScheduledNowAsync(subId) Enviar programado inmediatamente
SendAllScheduledNowAsync() Enviar todos los programados ahora

ISmsUtils

Método Descripción
AnalyzeMessage(message) Análisis completo del mensaje
CalculateSegments(message) Calcular número de segmentos
CountGsmCharacters(message) Contar caracteres GSM
DetectEncoding(message) Detectar codificación necesaria
IsGsmCompatible(message) Verificar compatibilidad GSM-7
GetInvalidGsmCharacters(message) Obtener caracteres no GSM
ValidatePhoneNumber(phone, countryCode?) Validar y normalizar teléfono
NormalizePhoneNumber(phone, countryCode?) Normalizar teléfono
IsValidPhoneNumber(phone) Validación rápida de teléfono
EstimateCost(message, recipients, price?) Estimar coste de envío
TruncateMessage(message, maxSegments) Truncar mensaje
SplitMessage(message, maxSegmentsPerPart) Dividir mensaje largo
ReplaceUnicodeWithGsm(message) Convertir Unicode a GSM
GetRemainingCharacters(message) Caracteres restantes

Manejo de Errores

La librería lanza excepciones tipadas LabsMobileApiException que encapsulan los errores de la API y de red. Esto permite un manejo granular de los distintos tipos de fallo.

Tipos de Errores

try
{
    var resultado = await _smsService.SendAsync("34609123456", "Hola");
}
catch (LabsMobileApiException ex) when (ex.StatusCode != null)
{
    // Error HTTP de la API (401, 403, 500, etc.)
    _logger.LogError("Error HTTP {StatusCode}: {Message}", ex.StatusCode, ex.Message);
}
catch (LabsMobileApiException ex) when (ex.InnerException is HttpRequestException)
{
    // Error de conexión (red caída, DNS, etc.)
    _logger.LogError("Error de red: {Message}", ex.Message);
}
catch (LabsMobileApiException ex) when (ex.InnerException is TaskCanceledException)
{
    // Timeout en la petición
    _logger.LogWarning("La petición a LabsMobile superó el timeout");
}
catch (InvalidOperationException)
{
    // Servicio no configurado (faltan Username o ApiToken)
    _logger.LogError("El servicio SMS no está configurado");
}
catch (ArgumentException ex)
{
    // Validación de parámetros (teléfono vacío, mensaje vacío, etc.)
    _logger.LogWarning("Parámetro inválido: {Message}", ex.Message);
}

Propiedades de LabsMobileApiException

Propiedad Tipo Descripción
StatusCode HttpStatusCode? Código HTTP cuando el error es de la API
ErrorCode string? Código de error de LabsMobile
InnerException Exception? Excepción original (HttpRequestException, TaskCanceledException, JsonException)

Resiliencia HTTP

La librería incluye resiliencia HTTP integrada mediante Microsoft.Extensions.Http.Resilience (basado en Polly). Al registrar el servicio con AddLabsMobileSms(), se configura automáticamente:

  • Reintentos con backoff exponencial: Para errores transitorios (408, 429, 500, 502, 503, 504)
  • Circuit breaker: Evita saturar la API cuando está caída, cortando las peticiones temporalmente
  • Timeout por intento: Cada intento individual tiene un timeout configurado
  • Timeout total: Límite máximo para toda la operación incluyendo reintentos

Esto significa que las llamadas HTTP se reintentan automáticamente ante fallos transitorios sin necesidad de código adicional.


Códigos de Error

Código Descripción Solución
0 Éxito -
23 No hay destinatarios Añadir al menos un número
24 Demasiados destinatarios Máximo 10,000 por petición
35 Créditos insuficientes Recargar saldo
39 Formato de fecha inválido Usar formato YYYY-MM-DD HH:MM:SS
52 Mensajes programados no encontrados Verificar SubId

Límites de Caracteres

GSM-7 (Texto Estándar)

Segmentos Caracteres Créditos
1 1-160 1x
2 161-306 2x
3 307-459 3x
4 460-612 4x

Unicode (Emojis/Especiales)

Segmentos Caracteres Créditos
1 1-70 1x
2 71-134 2x
3 135-201 3x
4 202-268 4x

Nota: No se recomienda exceder 4 segmentos por mensaje.

Caracteres GSM-7 Extendidos

Estos caracteres ocupan 2 espacios: ^ { } \ [ ~ ] | €


Ejemplos Completos

Ejemplo 1: Servicio de Verificación por SMS

public class VerificationService
{
    private readonly ILabsMobileSmsService _smsService;
    private readonly ISmsUtils _smsUtils;

    public VerificationService(ILabsMobileSmsService smsService, ISmsUtils smsUtils)
    {
        _smsService = smsService;
        _smsUtils = smsUtils;
    }

    public async Task<VerificationResult> SendVerificationCodeAsync(string phoneNumber)
    {
        // Validar número
        var validation = _smsUtils.ValidatePhoneNumber(phoneNumber, "34");
        if (!validation.IsValid)
        {
            return new VerificationResult
            {
                Success = false,
                Error = validation.ErrorMessage
            };
        }

        // Generar código
        var code = Random.Shared.Next(100000, 999999).ToString();

        // Enviar SMS
        var mensaje = $"Tu código de verificación es: {code}";
        var resultado = await _smsService.SendAsync(
            validation.NormalizedNumber,
            mensaje
        );

        return new VerificationResult
        {
            Success = resultado.IsSuccess,
            Code = resultado.IsSuccess ? code : null,
            MessageId = resultado.SubId,
            Error = resultado.IsSuccess ? null : resultado.Message
        };
    }
}

Ejemplo 2: Notificaciones de Pedidos

public class OrderNotificationService
{
    private readonly ILabsMobileSmsService _smsService;
    private readonly ISmsUtils _smsUtils;

    public async Task NotifyOrderShippedAsync(Order order)
    {
        var mensaje = $"¡Tu pedido #{order.Id} ha sido enviado! " +
                      $"Tracking: {order.TrackingNumber}. " +
                      $"Entrega estimada: {order.EstimatedDelivery:dd/MM}";

        // Verificar que el mensaje no exceda límites
        var info = _smsUtils.AnalyzeMessage(mensaje);
        if (info.ExceedsRecommendedLimit)
        {
            mensaje = _smsUtils.TruncateMessage(mensaje, 4);
        }

        await _smsService.SendAsync(order.CustomerPhone, mensaje);
    }

    public async Task SendBulkPromotionAsync(List<Customer> customers, string promotion)
    {
        // Estimar coste antes de enviar
        var estimacion = _smsUtils.EstimateCost(promotion, customers.Count);

        // Verificar saldo
        var balance = await _smsService.GetBalanceAsync();
        if (balance.CreditsDecimal < estimacion.TotalBaseCredits)
        {
            throw new InsufficientCreditsException(
                $"Se necesitan {estimacion.TotalBaseCredits} créditos, " +
                $"disponibles: {balance.CreditsDecimal}"
            );
        }

        // Enviar en lotes de 10,000
        var batches = customers
            .Select(c => c.Phone)
            .Chunk(10000);

        foreach (var batch in batches)
        {
            await _smsService.SendBulkAsync(batch, promotion);
        }
    }
}

Ejemplo 3: Recordatorios Programados

public class AppointmentReminderService
{
    private readonly ILabsMobileSmsService _smsService;

    public async Task ScheduleReminderAsync(Appointment appointment)
    {
        var mensaje = $"Recordatorio: Tienes cita el {appointment.Date:dd/MM} " +
                      $"a las {appointment.Date:HH:mm} en {appointment.Location}";

        // Programar 24 horas antes
        var fechaEnvio = appointment.Date.AddHours(-24);

        var resultado = await _smsService.ScheduleAsync(
            appointment.PatientPhone,
            mensaje,
            fechaEnvio
        );

        // Guardar SubId para poder cancelar si se reprograma la cita
        appointment.ReminderMessageId = resultado.SubId;
        await _appointmentRepository.UpdateAsync(appointment);
    }

    public async Task CancelReminderAsync(string messageId)
    {
        if (!string.IsNullOrEmpty(messageId))
        {
            await _smsService.CancelScheduledAsync(messageId);
        }
    }
}

Ejemplo 4: Validación y Optimización de Mensajes

public class MessageComposer
{
    private readonly ISmsUtils _smsUtils;

    public MessageComposer(ISmsUtils smsUtils)
    {
        _smsUtils = smsUtils;
    }

    public ComposedMessage Compose(string content)
    {
        var info = _smsUtils.AnalyzeMessage(content);

        var result = new ComposedMessage
        {
            Original = content,
            Optimized = content,
            OriginalSegments = info.SegmentCount,
            OriginalEncoding = info.Encoding
        };

        // Intentar optimizar si usa Unicode innecesariamente
        if (info.Encoding == SmsEncoding.Unicode)
        {
            var optimized = _smsUtils.ReplaceUnicodeWithGsm(content);
            var optimizedInfo = _smsUtils.AnalyzeMessage(optimized);

            if (optimizedInfo.Encoding == SmsEncoding.Gsm7)
            {
                result.Optimized = optimized;
                result.OptimizedSegments = optimizedInfo.SegmentCount;
                result.OptimizedEncoding = SmsEncoding.Gsm7;
                result.CreditsSaved = info.SegmentCount - optimizedInfo.SegmentCount;
            }
        }

        return result;
    }
}

Mejores Prácticas

  1. Validar números antes de enviar: Usa ValidatePhoneNumber() para normalizar y verificar
  2. Verificar codificación: Usa DetectEncoding() para saber el coste real
  3. Estimar costes: Usa EstimateCost() antes de envíos masivos
  4. Verificar saldo: Consulta GetBalanceAsync() antes de campañas grandes
  5. No exceder 4 segmentos: Mensajes muy largos tienen mala entrega
  6. Usar modo test: Desarrolla con TestMode = true para no gastar créditos
  7. Guardar SubId: Necesario para cancelar mensajes programados
  8. Implementar webhooks: Para confirmar entregas y tracking

Requisitos

  • .NET 10.0 o superior
  • Cuenta en LabsMobile con credenciales API

Licencia

MIT License - ver LICENSE para más detalles.


Autor

Jorge Jerez Sabater - Gesgocom

No packages depend on Gesgocom.GesSms.

Version Downloads Last updated
1.0.1 1 02/09/2026