infobip-sms-sdk 0.1.0

Async Rust SDK for the Infobip SMS API: send messages, manage scheduled bulks, query delivery reports and logs, fetch inbound SMS, and parse webhook payloads.
Documentation
//! Models for `GET /sms/3/logs` — fetching outbound message logs.
//!
//! Logs are kept for 48 hours, with at most 1000 entries per call. Use
//! these when you need to *re-read* messages you've already pulled
//! delivery reports for, or when investigating issues.
//!
//! # Cursor pagination
//!
//! To page beyond the first batch, set
//! [`LogsQuery::use_cursor`] to `true`, then on each subsequent call
//! pass the previous response's
//! [`crate::models::common::CursorPageInfo::next_cursor`] in
//! [`LogsQuery::cursor`]. When `next_cursor` is `None`, you've hit the
//! last page.
//!
//! ```no_run
//! # use infobip_sms::Client;
//! use infobip_sms::models::logs::LogsQuery;
//!
//! # async fn run(client: Client) -> Result<(), infobip_sms::Error> {
//! let mut query = LogsQuery {
//!     use_cursor: Some(true),
//!     limit: Some(1000),
//!     ..Default::default()
//! };
//!
//! loop {
//!     let page = client.get_logs(&query).await?;
//!     for log in page.results {
//!         // process `log`
//!         # let _ = log;
//!     }
//!     match page.cursor.and_then(|c| c.next_cursor) {
//!         Some(next) => query.cursor = Some(next),
//!         None => break,
//!     }
//! }
//! # Ok(()) }
//! ```

use serde::{Deserialize, Serialize};

use crate::models::common::{
    CursorPageInfo, MessageError, MessageGeneralStatus, Platform, Price, Status,
};
use crate::models::send::SmsMessageContent;

/// Response body for
/// [`Client::get_logs`](crate::Client::get_logs).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LogsResponse {
    /// One [`SmsLog`] per message.
    #[serde(default)]
    pub results: Vec<SmsLog>,
    /// Pagination metadata (only populated when the request used
    /// `useCursor=true`).
    pub cursor: Option<CursorPageInfo>,
}

/// One log entry — a richer view of a sent message than a delivery
/// report.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsLog {
    /// Sender ID.
    pub sender: Option<String>,
    /// Destination address.
    pub destination: Option<String>,
    /// Bulk ID this message belonged to.
    pub bulk_id: Option<String>,
    /// Per-recipient message ID.
    pub message_id: Option<String>,
    /// When the message was sent / scheduled to be sent.
    pub sent_at: Option<String>,
    /// When Infobip finished processing.
    pub done_at: Option<String>,
    /// Number of SMS parts the message split into.
    pub message_count: Option<i32>,
    /// Cost of this message.
    pub price: Option<Price>,
    /// Final status.
    pub status: Option<Status>,
    /// Error details, if any.
    pub error: Option<MessageError>,
    /// Routing platform that handled the message.
    pub platform: Option<Platform>,
    /// The actual content that was sent (text or binary).
    pub content: Option<SmsMessageContent>,
    /// Marketing campaign reference, if set on send.
    pub campaign_reference_id: Option<String>,
    /// Mobile country + network code of the destination operator.
    pub mcc_mnc: Option<String>,
}

/// Query parameters for
/// [`Client::get_logs`](crate::Client::get_logs).
///
/// `Vec<String>` fields (`bulk_id`, `message_id`,
/// `campaign_reference_id`) are sent as a single comma-separated query
/// parameter, which is what the API expects.
///
/// ```
/// use infobip_sms::models::logs::LogsQuery;
/// use infobip_sms::models::common::MessageGeneralStatus;
///
/// let query = LogsQuery {
///     general_status: Some(MessageGeneralStatus::Delivered),
///     limit: Some(500),
///     ..Default::default()
/// };
/// # let _ = query;
/// ```
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LogsQuery {
    /// Mobile Country Code filter.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mcc: Option<String>,
    /// Mobile Network Code filter (requires `mcc`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mnc: Option<String>,
    /// Sender ID filter.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sender: Option<String>,
    /// Destination address filter.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub destination: Option<String>,
    /// Filter by one or more bulk IDs (comma-joined on the wire).
    #[serde(skip_serializing_if = "Vec::is_empty", serialize_with = "csv")]
    pub bulk_id: Vec<String>,
    /// Filter by one or more message IDs (comma-joined on the wire).
    #[serde(skip_serializing_if = "Vec::is_empty", serialize_with = "csv")]
    pub message_id: Vec<String>,
    /// Filter by general status group.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub general_status: Option<MessageGeneralStatus>,
    /// Lower time bound (`yyyy-MM-dd'T'HH:mm:ss.SSSZ`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sent_since: Option<String>,
    /// Upper time bound (`yyyy-MM-dd'T'HH:mm:ss.SSSZ`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sent_until: Option<String>,
    /// Maximum logs to return. Defaults to 50; max 1000.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<i32>,
    /// Filter by sending entity ID.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub entity_id: Option<String>,
    /// Filter by sending application ID.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub application_id: Option<String>,
    /// Filter by campaign reference IDs (comma-joined on the wire).
    #[serde(skip_serializing_if = "Vec::is_empty", serialize_with = "csv")]
    pub campaign_reference_id: Vec<String>,
    /// Enable cursor-based pagination. When `true`, responses include
    /// a [`CursorPageInfo`] with a `nextCursor`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub use_cursor: Option<bool>,
    /// Cursor returned from the previous page. Omit on the first
    /// request; on subsequent requests, set this to
    /// `previous_response.cursor.next_cursor`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cursor: Option<String>,
}

fn csv<S>(values: &[String], serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    serializer.serialize_str(&values.join(","))
}