sms_types/
sms.rs

1//! Generic types that apply to both HTTP and Websocket interfaces.
2
3use serde::{Deserialize, Serialize};
4
5/// Represents a stored SMS message from the database.
6#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
7pub struct SmsMessage {
8    /// Unique identifier for the message.
9    pub message_id: Option<i64>,
10
11    /// The phone number associated with this message.
12    pub phone_number: String,
13
14    /// The actual text content of the message.
15    pub message_content: String,
16
17    /// Optional reference number for message tracking.
18    /// This is assigned by the modem and is only present for outgoing messages.
19    pub message_reference: Option<u8>,
20
21    /// Whether this message was sent (true) or received (false).
22    pub is_outgoing: bool,
23
24    /// Service message center delivery status.
25    pub status: Option<SmsDeliveryReportStatus>,
26
27    /// Unix timestamp when the message was created.
28    pub created_at: Option<u32>,
29
30    /// Optional Unix timestamp when the message was completed/delivered.
31    pub completed_at: Option<u32>,
32}
33impl SmsMessage {
34    /// Returns a clone of the message with the `message_id` option replaced.
35    #[must_use]
36    pub fn with_message_id(&self, id: Option<i64>) -> Self {
37        Self {
38            message_id: id,
39            ..self.clone()
40        }
41    }
42}
43
44/// A partial outgoing message.
45#[derive(Debug)]
46pub struct SmsOutgoingMessage {
47
48    /// Target phone number.
49    pub phone_number: String,
50
51    /// Message text content.
52    pub content: String,
53
54    /// Should the message be sent as a Class 0 flash delivery.
55    pub flash: bool,
56
57    /// An optional validity period used by the SMC, default 24hr.
58    pub validity_period: Option<u8>,
59
60    /// A timeout to use for sending an SMS message.
61    pub timeout: Option<u32>,
62}
63impl SmsOutgoingMessage {
64
65    /// Get the message sending validity period, either as set or default.
66    #[must_use]
67    pub fn get_validity_period(&self) -> u8 {
68        self.validity_period.unwrap_or(167) // 24hr
69    }
70}
71impl From<&SmsOutgoingMessage> for SmsMessage {
72    fn from(outgoing: &SmsOutgoingMessage) -> Self {
73        SmsMessage {
74            message_id: None,
75            phone_number: outgoing.phone_number.clone(),
76            message_content: outgoing.content.clone(),
77            message_reference: None,
78            is_outgoing: true,
79            status: None,
80            created_at: None,
81            completed_at: None,
82        }
83    }
84}
85
86/// An incoming message from the Modem.
87#[derive(Debug, Clone)]
88pub struct SmsIncomingMessage {
89    /// The incoming sender address. This could also be an alphanumeric sender name.
90    /// This is usually for registered businesses or carrier messages.
91    pub phone_number: String,
92
93    /// The decoded multipart header.
94    pub user_data_header: Option<SmsMultipartHeader>,
95
96    /// The raw message content.
97    pub content: String,
98}
99impl From<&SmsIncomingMessage> for SmsMessage {
100    fn from(incoming: &SmsIncomingMessage) -> Self {
101        SmsMessage {
102            message_id: None,
103            phone_number: incoming.phone_number.clone(),
104            message_content: incoming.content.clone(),
105            message_reference: None,
106            is_outgoing: false,
107            status: None,
108            created_at: None,
109            completed_at: None,
110        }
111    }
112}
113
114/// A partial message delivery report, as it comes from the modem.
115#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
116pub struct SmsPartialDeliveryReport {
117    /// The target phone number that received the message (and has now sent back a delivery report).
118    pub phone_number: String,
119    /// The modem assigned message reference, this is basically useless outside short-term tracking
120    /// the `message_id` is unique should always be used instead for identification.
121    pub reference_id: u8,
122
123    /// The SMS TP-Status: <https://www.etsi.org/deliver/etsi_ts/123000_123099/123040/16.00.00_60/ts_123040v160000p.pdf#page=71>
124    pub status: SmsDeliveryReportStatus,
125}
126
127/// <https://www.etsi.org/deliver/etsi_ts/123000_123099/123040/16.00.00_60/ts_123040v160000p.pdf#page=71>
128#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
129#[repr(u8)]
130pub enum SmsDeliveryReportStatus {
131    // Short message transaction completed (0x00-0x1F)
132    /// Short message received by the SME successfully
133    ReceivedBySme = 0x00,
134    /// Short message forwarded by the SC to the SME but delivery confirmation unavailable
135    ForwardedButUnconfirmed = 0x01,
136    /// Short message replaced by the SC
137    ReplacedBySc = 0x02,
138    // 0x03-0x0F Reserved
139    // 0x10-0x1F SC specific values
140
141    // Temporary error, SC still trying (0x20-0x3F)
142    /// Network congestion preventing delivery, SC will retry
143    Congestion = 0x20,
144    /// SME is busy, SC will retry delivery
145    SmeBusy = 0x21,
146    /// No response from SME, SC will retry delivery
147    NoResponseFromSme = 0x22,
148    /// Service rejected by network, SC will retry delivery
149    ServiceRejected = 0x23,
150    /// Quality of service not available, SC will retry delivery
151    QualityOfServiceNotAvailable = 0x24,
152    /// Error in SME, SC will retry delivery
153    ErrorInSme = 0x25,
154    // 0x26-0x2F Reserved
155    // 0x30-0x3F SC specific values
156
157    // Permanent error, SC not making more attempts (0x40-0x5F)
158    /// Remote procedure error - permanent failure
159    RemoteProcedureError = 0x40,
160    /// Incompatible destination - permanent failure
161    IncompatibleDestination = 0x41,
162    /// Connection rejected by SME - permanent failure
163    ConnectionRejectedBySme = 0x42,
164    /// Destination not obtainable - permanent failure
165    NotObtainable = 0x43,
166    /// Quality of service not available - permanent failure
167    QualityOfServiceNotAvailablePermanent = 0x44,
168    /// No interworking available - permanent failure
169    NoInterworkingAvailable = 0x45,
170    /// SM validity period expired - permanent failure
171    SmValidityPeriodExpired = 0x46,
172    /// SM deleted by originating SME - permanent failure
173    SmDeletedByOriginatingSme = 0x47,
174    /// SM deleted by SC administration - permanent failure
175    SmDeletedByScAdministration = 0x48,
176    /// SM does not exist in SC - permanent failure
177    SmDoesNotExist = 0x49,
178    // 0x4A-0x4F Reserved
179    // 0x50-0x5F SC specific values
180
181    // Temporary error, SC not making more attempts (0x60-0x7F)
182    /// Network congestion, SC has stopped retry attempts
183    CongestionNoRetry = 0x60,
184    /// SME busy, SC has stopped retry attempts
185    SmeBusyNoRetry = 0x61,
186    /// No response from SME, SC has stopped retry attempts
187    NoResponseFromSmeNoRetry = 0x62,
188    /// Service rejected, SC has stopped retry attempts
189    ServiceRejectedNoRetry = 0x63,
190    /// Quality of service not available, SC has stopped retry attempts
191    QualityOfServiceNotAvailableNoRetry = 0x64,
192    /// Error in SME, SC has stopped retry attempts
193    ErrorInSmeNoRetry = 0x65,
194    // 0x66-0x69 Reserved
195    // 0x6A-0x6F Reserved
196    // 0x70-0x7F SC specific values
197    /// Unknown or reserved status code - treated as service rejected per spec
198    Unknown(u8),
199}
200impl From<u8> for SmsDeliveryReportStatus {
201    fn from(value: u8) -> Self {
202        use SmsDeliveryReportStatus::{
203            Congestion, CongestionNoRetry, ConnectionRejectedBySme, ErrorInSme, ErrorInSmeNoRetry,
204            ForwardedButUnconfirmed, IncompatibleDestination, NoInterworkingAvailable,
205            NoResponseFromSme, NoResponseFromSmeNoRetry, NotObtainable,
206            QualityOfServiceNotAvailable, QualityOfServiceNotAvailableNoRetry,
207            QualityOfServiceNotAvailablePermanent, ReceivedBySme, RemoteProcedureError,
208            ReplacedBySc, ServiceRejected, ServiceRejectedNoRetry, SmDeletedByOriginatingSme,
209            SmDeletedByScAdministration, SmDoesNotExist, SmValidityPeriodExpired, SmeBusy,
210            SmeBusyNoRetry, Unknown,
211        };
212
213        match value {
214            // Transaction completed successfully
215            0x00 => ReceivedBySme,
216            0x01 => ForwardedButUnconfirmed,
217            0x02 => ReplacedBySc,
218
219            // Temporary errors, SC still trying
220            0x20 => Congestion,
221            0x21 => SmeBusy,
222            0x22 => NoResponseFromSme,
223            0x23 => ServiceRejected,
224            0x24 => QualityOfServiceNotAvailable,
225            0x25 => ErrorInSme,
226
227            // Permanent errors
228            0x40 => RemoteProcedureError,
229            0x41 => IncompatibleDestination,
230            0x42 => ConnectionRejectedBySme,
231            0x43 => NotObtainable,
232            0x44 => QualityOfServiceNotAvailablePermanent,
233            0x45 => NoInterworkingAvailable,
234            0x46 => SmValidityPeriodExpired,
235            0x47 => SmDeletedByOriginatingSme,
236            0x48 => SmDeletedByScAdministration,
237            0x49 => SmDoesNotExist,
238
239            // Temporary errors, SC not retrying
240            0x60 => CongestionNoRetry,
241            0x61 => SmeBusyNoRetry,
242            0x62 => NoResponseFromSmeNoRetry,
243            0x63 => ServiceRejectedNoRetry,
244            0x64 => QualityOfServiceNotAvailableNoRetry,
245            0x65 => ErrorInSmeNoRetry,
246
247            // All other values (reserved, SC-specific, or unknown)
248            _ => Unknown(value),
249        }
250    }
251}
252
253#[cfg(feature = "pdu")]
254impl From<sms_pdu::pdu::MessageStatus> for SmsDeliveryReportStatus {
255    fn from(status: sms_pdu::pdu::MessageStatus) -> Self {
256        Self::from(status as u8)
257    }
258}
259
260impl SmsDeliveryReportStatus {
261    /// Returns true if the SMS was successfully delivered to the SME
262    #[must_use]
263    pub fn is_successful(&self) -> bool {
264        matches!(
265            self,
266            Self::ReceivedBySme | Self::ForwardedButUnconfirmed | Self::ReplacedBySc
267        )
268    }
269
270    /// Returns true if this is a temporary error where SC is still trying
271    #[must_use]
272    pub fn is_temporary_retrying(&self) -> bool {
273        use SmsDeliveryReportStatus::{
274            Congestion, ErrorInSme, NoResponseFromSme, QualityOfServiceNotAvailable,
275            ServiceRejected, SmeBusy, Unknown,
276        };
277
278        matches!(
279            self,
280            Congestion
281                | SmeBusy
282                | NoResponseFromSme
283                | ServiceRejected
284                | QualityOfServiceNotAvailable
285                | ErrorInSme
286        ) || matches!(self, Unknown(val) if *val >= 0x20 && *val <= 0x3F)
287    }
288
289    /// Returns true if this is a permanent error (no more delivery attempts)
290    #[must_use]
291    pub fn is_permanent_error(&self) -> bool {
292        use SmsDeliveryReportStatus::{
293            ConnectionRejectedBySme, IncompatibleDestination, NoInterworkingAvailable,
294            NotObtainable, QualityOfServiceNotAvailablePermanent, RemoteProcedureError,
295            SmDeletedByOriginatingSme, SmDeletedByScAdministration, SmDoesNotExist,
296            SmValidityPeriodExpired, Unknown,
297        };
298
299        matches!(
300            self,
301            RemoteProcedureError
302                | IncompatibleDestination
303                | ConnectionRejectedBySme
304                | NotObtainable
305                | QualityOfServiceNotAvailablePermanent
306                | NoInterworkingAvailable
307                | SmValidityPeriodExpired
308                | SmDeletedByOriginatingSme
309                | SmDeletedByScAdministration
310                | SmDoesNotExist
311        ) || matches!(self, Unknown(val) if *val >= 0x40 && *val <= 0x5F)
312    }
313
314    /// Returns true if this is a temporary error where SC has stopped trying
315    #[must_use]
316    pub fn is_temporary_no_retry(&self) -> bool {
317        use SmsDeliveryReportStatus::{
318            CongestionNoRetry, ErrorInSmeNoRetry, NoResponseFromSmeNoRetry,
319            QualityOfServiceNotAvailableNoRetry, ServiceRejectedNoRetry, SmeBusyNoRetry, Unknown,
320        };
321
322        matches!(
323            self,
324            CongestionNoRetry
325                | SmeBusyNoRetry
326                | NoResponseFromSmeNoRetry
327                | ServiceRejectedNoRetry
328                | QualityOfServiceNotAvailableNoRetry
329                | ErrorInSmeNoRetry
330        ) || matches!(self, Unknown(val) if *val >= 0x60 && *val <= 0x7F)
331    }
332
333    /// Converts the status to a simplified status group for easier categorization
334    #[must_use]
335    pub fn to_status_group(&self) -> SmsDeliveryReportStatusGroup {
336        if self.is_successful() {
337            SmsDeliveryReportStatusGroup::Received
338        } else if self.is_temporary_retrying() {
339            SmsDeliveryReportStatusGroup::Sent
340        } else if self.is_permanent_error() || self.is_temporary_no_retry() {
341            // Both permanent errors and temporary errors with no retry are treated as failures
342            if self.is_permanent_error() {
343                SmsDeliveryReportStatusGroup::PermanentFailure
344            } else {
345                SmsDeliveryReportStatusGroup::TemporaryFailure
346            }
347        } else {
348            // For unknown status codes, classify based on their range.
349            match self {
350                Self::Unknown(val) if *val >= 0x20 && *val <= 0x3F => {
351                    SmsDeliveryReportStatusGroup::Sent
352                }
353                Self::Unknown(val) if *val >= 0x40 && *val <= 0x5F => {
354                    SmsDeliveryReportStatusGroup::PermanentFailure
355                }
356                Self::Unknown(val) if *val >= 0x60 && *val <= 0x7F => {
357                    SmsDeliveryReportStatusGroup::TemporaryFailure
358                }
359                _ => SmsDeliveryReportStatusGroup::PermanentFailure, // Default for truly unknown codes
360            }
361        }
362    }
363}
364
365/// Generalised group for message delivery status.
366#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
367pub enum SmsDeliveryReportStatusGroup {
368    /// Message was sent but delivery is still pending (temporary errors with retry)
369    Sent,
370    /// Message was successfully received by the destination.
371    Received,
372    /// Temporary delivery failure where SC has stopped retrying.
373    TemporaryFailure,
374    /// Permanent delivery failure - message will not be delivered.
375    PermanentFailure,
376}
377impl From<SmsDeliveryReportStatus> for SmsDeliveryReportStatusGroup {
378    fn from(status: SmsDeliveryReportStatus) -> Self {
379        status.to_status_group()
380    }
381}
382
383/// The sms message multipart header.
384#[derive(Debug, Clone)]
385pub struct SmsMultipartHeader {
386    /// Modem assigned message send reference (overflows).
387    pub message_reference: u8,
388
389    /// The total amount of messages within this multipart.
390    pub total: u8,
391
392    /// The current received message index.
393    pub index: u8,
394}
395impl TryFrom<Vec<u8>> for SmsMultipartHeader {
396    type Error = &'static str;
397
398    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
399        if data.len() != 3 {
400            return Err("Invalid user data length!");
401        }
402        Ok(Self {
403            message_reference: data[0],
404            total: data[1],
405            index: data[2],
406        })
407    }
408}
409