From 184ce4229ef760e2d91c4b1157ed6d9a3094acaf Mon Sep 17 00:00:00 2001
From: Nathan Williams <njw@chromium.org>
Date: Tue, 30 Aug 2011 11:00:53 -0400
Subject: [PATCH] Multipart SMS support.

Keep a local cache of SMS message fragments when we perform a List or
Get command on the modem; use this cache to reassemble fragments into
complete messages, which are then what is returned by the Get and List
DBus commands. Similarly, cause Delete to delete all known parts of a
multipart message.

While here, remove some extra whitespace in the SMS commands we send
to the modem.
---
 src/mm-generic-gsm.c |  469 +++++++++++++++++++++++++++++++++++++++++++++++---
 src/mm-sms-utils.c   |   98 +++++++----
 2 files changed, 511 insertions(+), 56 deletions(-)

diff --git a/src/mm-generic-gsm.c b/src/mm-generic-gsm.c
index 82e51f6..fdc29eb 100644
--- a/src/mm-generic-gsm.c
+++ b/src/mm-generic-gsm.c
@@ -129,6 +129,15 @@ typedef struct {
 
     /* SMS */
     GHashTable *sms_present;
+    /* Map from SMS index numbers to parsed PDUs (themselves as hash tables) */
+    GHashTable *sms_contents;
+    /*
+     * Map from multipart SMS reference numbers to SMSMultiPartMessage
+     * structures.
+     */
+    GHashTable *sms_parts;
+
+    guint sms_fetch_pending;
 } MMGenericGsmPrivate;
 
 static void get_registration_status (MMAtSerialPort *port, MMCallbackInfo *info);
@@ -1324,6 +1333,248 @@ ciev_received (MMAtSerialPort *port,
     /* FIXME: handle roaming and service indicators */
 }
 
+typedef struct {
+    /*
+     * The key index number that refers to this multipart message -
+     * usually the index number of the first part received.
+     */
+    guint index;
+
+    /* Number of parts in the complete message */
+    guint numparts;
+
+    /* Number of parts missing from the message */
+    guint missing;
+
+    /* Array of (index numbers of) message parts, in order */
+    guint *parts;
+} SMSMultiPartMessage;
+
+static void
+sms_cache_insert (MMModem *modem, GHashTable *properties, guint idx)
+{
+    MMGenericGsmPrivate *priv = MM_GENERIC_GSM_GET_PRIVATE (modem);
+    GHashTable *old_properties;
+    GValue *ref;
+
+    ref = g_hash_table_lookup (properties, "concat-reference");
+    if (ref != NULL) {
+        GValue *max, *seq;
+        guint refnum, maxnum, seqnum;
+        SMSMultiPartMessage *mpm;
+
+        max = g_hash_table_lookup (properties, "concat-max");
+        seq = g_hash_table_lookup (properties, "concat-sequence");
+        if (max == NULL || seq == NULL) {
+            /* Internal error - not all required data present */
+            return;
+        }
+
+        refnum = g_value_get_uint (ref);
+        maxnum = g_value_get_uint (max);
+        seqnum = g_value_get_uint (seq);
+
+        if (seqnum > maxnum) {
+            /* Error - SMS says "part N of M", but N > M */
+            return;
+        }
+
+        mpm = g_hash_table_lookup (priv->sms_parts, GUINT_TO_POINTER (refnum));
+        if (mpm == NULL) {
+            /* Create a new one */
+            if (maxnum > 255)
+                maxnum = 255;
+            mpm = g_malloc0 (sizeof (*mpm));
+            mpm->index = idx;
+            mpm->numparts = maxnum;
+            mpm->missing = maxnum;
+            mpm->parts = g_malloc0 (maxnum * sizeof(*mpm->parts));
+            g_hash_table_insert (priv->sms_parts, GUINT_TO_POINTER (refnum),
+                                 mpm);
+        }
+
+        if (maxnum != mpm->numparts) {
+            /* Error - other messages with this refnum claim a different number of parts */
+            return;
+        }
+
+        if (mpm->parts[seqnum - 1] != 0) {
+            /* Error - two SMS segments have claimed to be the same part of the same message. */
+            return;
+        }
+
+        mpm->parts[seqnum - 1] = idx;
+        mpm->missing--;
+    }
+
+    old_properties = g_hash_table_lookup (priv->sms_contents, GUINT_TO_POINTER (idx));
+    if (old_properties != NULL)
+        g_hash_table_unref (old_properties);
+
+    g_hash_table_insert (priv->sms_contents, GUINT_TO_POINTER (idx),
+                         g_hash_table_ref (properties));
+}
+
+/*
+ * Takes a hash table representing a (possibly partial) SMS and
+ * determines if it is the key part of a complete SMS. The complete
+ * SMS, if any, is returned. If there is no such SMS (for example, not
+ * all parts are present yet), NULL is returned. The passed-in hash
+ * table is dereferenced, and the returned hash table is referenced.
+ */
+static GHashTable *
+sms_cache_lookup_full (MMModem *modem,
+                       GHashTable *properties,
+                       GError **error)
+{
+    MMGenericGsmPrivate *priv = MM_GENERIC_GSM_GET_PRIVATE (modem);
+    int i, refnum, indexnum;
+    SMSMultiPartMessage *mpm;
+    GHashTable *full, *part, *first;
+    GHashTableIter iter;
+    gpointer key, value;
+    char *fulltext;
+    char **textparts;
+    GValue *ref, *index, *text;
+
+    ref = g_hash_table_lookup (properties, "concat-reference");
+    if (ref == NULL)
+        return properties;
+    refnum = g_value_get_uint (ref);
+
+    index = g_hash_table_lookup (properties, "index");
+    if (index == NULL) {
+        g_hash_table_unref (properties);
+        return NULL;
+    }
+
+    indexnum = g_value_get_uint (index);
+    g_hash_table_unref (properties);
+
+    mpm = g_hash_table_lookup (priv->sms_parts,
+                                 GUINT_TO_POINTER (refnum));
+    if (mpm == NULL) {
+        *error = g_error_new (MM_MODEM_ERROR,
+                              MM_MODEM_ERROR_GENERAL,
+                              "Internal error - no multipart structure for multipart SMS");
+        return NULL;
+    }
+
+    /* Check that this is the key */
+    if (indexnum != mpm->index)
+        return NULL;
+
+    if (mpm->missing != 0)
+        return NULL;
+
+    /* Complete multipart message is present. Assemble it */
+    textparts = g_malloc0((1 + mpm->numparts) * sizeof (*textparts));
+    for (i = 0 ; i < mpm->numparts ; i++) {
+        part = g_hash_table_lookup (priv->sms_contents,
+                                    GUINT_TO_POINTER (mpm->parts[i]));
+        if (part == NULL) {
+            *error = g_error_new (MM_MODEM_ERROR,
+                                  MM_MODEM_ERROR_GENERAL,
+                                  "Internal error - part %d (index %d) is missing",
+                                  i, mpm->parts[i]);
+            g_free (textparts);
+            return NULL;
+        }
+        text = g_hash_table_lookup (part, "text");
+        if (text == NULL) {
+            *error = g_error_new (MM_MODEM_ERROR,
+                                  MM_MODEM_ERROR_GENERAL,
+                                  "Internal error - part %d (index %d) has no text element",
+                                  i, mpm->parts[i]);
+            g_free (textparts);
+            return NULL;
+        }
+        textparts[i] = g_value_dup_string (text);
+    }
+    textparts[i] = NULL;
+    fulltext = g_strjoinv (NULL, textparts);
+    g_strfreev (textparts);
+
+    first = g_hash_table_lookup (priv->sms_contents,
+                                 GUINT_TO_POINTER (mpm->parts[0]));
+    full = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
+                                  simple_free_gvalue);
+    g_hash_table_iter_init (&iter, first);
+    while (g_hash_table_iter_next (&iter, &key, &value)) {
+        const char *keystr = key;
+        if (strncmp (keystr, "concat-", 7) == 0)
+            continue;
+        if (strcmp (keystr, "text") == 0 ||
+            strcmp (keystr, "index") == 0)
+            continue;
+        if (strcmp (keystr, "class") == 0) {
+            GValue *val;
+            val = g_slice_new0 (GValue);
+            g_value_init (val, G_TYPE_UINT);
+            g_value_copy (value, val);
+            g_hash_table_insert (full, key, val);
+        } else {
+            GValue *val;
+            val = g_slice_new0 (GValue);
+            g_value_init (val, G_TYPE_STRING);
+            g_value_copy (value, val);
+            g_hash_table_insert (full, key, val);
+        }
+    }
+
+    g_hash_table_insert (full, "index", simple_uint_value (mpm->index));
+    g_hash_table_insert (full, "text", simple_string_value (fulltext));
+    g_free (fulltext);
+
+    return full;
+}
+
+static void
+cmti_received_has_sms (MMModemGsmSms *modem,
+                       GHashTable *properties,
+                       GError *error,
+                       gpointer user_data)
+{
+    MMGenericGsm *self = MM_GENERIC_GSM (user_data);
+    MMGenericGsmPrivate *priv = MM_GENERIC_GSM_GET_PRIVATE (self);
+    guint idx;
+    gboolean complete;
+
+    /*
+     * But how will the 'received', non-complete signal get sent?
+     * Maybe that should happen earlier.
+     */
+    if (properties == NULL)
+        return;
+
+    GValue *ref = g_hash_table_lookup (properties, "concat-reference");
+    if (ref == NULL) {
+        /* single-part message */
+        GValue *idxval = g_hash_table_lookup (properties, "index");
+        if (idxval == NULL)
+            return;
+        idx = g_value_get_uint (idxval);
+        complete = TRUE;
+    } else {
+        SMSMultiPartMessage *mpm;
+        mpm = g_hash_table_lookup (priv->sms_parts,
+                                     GUINT_TO_POINTER (g_value_get_uint (ref)));
+        if (mpm == NULL)
+            return;
+        idx = mpm->index;
+        complete = (mpm->missing == 0);
+    }
+
+    if (complete)
+        mm_modem_gsm_sms_completed (MM_MODEM_GSM_SMS (self), idx, TRUE);
+
+    mm_modem_gsm_sms_received (MM_MODEM_GSM_SMS (self), idx, complete);
+}
+
+static void sms_get_invoke (MMCallbackInfo *info);
+static void sms_get_done (MMAtSerialPort *port, GString *response,
+                          GError *error, gpointer user_data);
+
 static void
 cmti_received (MMAtSerialPort *port,
                GMatchInfo *info,
@@ -1331,8 +1582,9 @@ cmti_received (MMAtSerialPort *port,
 {
     MMGenericGsm *self = MM_GENERIC_GSM (user_data);
     MMGenericGsmPrivate *priv = MM_GENERIC_GSM_GET_PRIVATE (self);
+    MMCallbackInfo *cbinfo;
     guint idx = 0;
-    char *str;
+    char *str, *command;
 
     str = g_match_info_fetch (info, 2);
     if (str)
@@ -1346,12 +1598,20 @@ cmti_received (MMAtSerialPort *port,
     /* Nothing is currently stored in the hash table - presence is all that matters. */
     g_hash_table_insert (priv->sms_present, GINT_TO_POINTER (idx), NULL);
 
-    /* todo: parse pdu to know if the sms is complete */
-    mm_modem_gsm_sms_received (MM_MODEM_GSM_SMS (self),
-                               idx,
-                               TRUE);
+    /* Retrieve the message */
+    cbinfo = mm_callback_info_new_full (MM_MODEM (user_data),
+                                        sms_get_invoke,
+                                        G_CALLBACK (cmti_received_has_sms),
+                                        user_data);
 
-    /* todo: send mm_modem_gsm_sms_completed if complete */
+    if (priv->sms_fetch_pending != 0) {
+        mm_err("sms_fetch_pending is %d, not 0", priv->sms_fetch_pending);
+    }
+    priv->sms_fetch_pending = idx;
+
+    command = g_strdup_printf ("+CMGR=%d", idx);
+    mm_at_serial_port_queue_command (port, command, 10, sms_get_done, cbinfo);
+    /* Don't want to signal received here before we have the contents */
 }
 
 static void
@@ -4267,8 +4527,6 @@ mm_generic_gsm_get_charset (MMGenericGsm *self)
 /*****************************************************************************/
 /* MMModemGsmSms interface */
 
-
-
 static void
 sms_send_done (MMAtSerialPort *port,
                GString *response,
@@ -4318,8 +4576,6 @@ sms_send (MMModemGsmSms *modem,
     g_free (command);
 }
 
-
-
 static void
 sms_get_done (MMAtSerialPort *port,
               GString *response,
@@ -4327,10 +4583,15 @@ sms_get_done (MMAtSerialPort *port,
               gpointer user_data)
 {
     MMCallbackInfo *info = (MMCallbackInfo *) user_data;
+    MMGenericGsmPrivate *priv = MM_GENERIC_GSM_GET_PRIVATE (info->modem);
     GHashTable *properties;
     int rv, status, tpdu_len;
+    guint idx;
     char pdu[SMS_MAX_PDU_LEN + 1];
 
+    idx = priv->sms_fetch_pending;
+    priv->sms_fetch_pending = 0;
+
     /* If the modem has already been removed, return without
      * scheduling callback */
     if (mm_callback_info_check_modem_removed (info))
@@ -4357,8 +4618,19 @@ sms_get_done (MMAtSerialPort *port,
         goto out;
     }
 
-    mm_callback_info_set_data (info, GS_HASH_TAG, properties,
-                               (GDestroyNotify) g_hash_table_unref);
+    g_hash_table_insert (properties, "index",
+                         simple_uint_value (idx));
+    sms_cache_insert (info->modem, properties, idx);
+
+    /*
+     * If this is a standalone message, or the key part of a
+     * multipart message, pass it along, otherwise report that there's
+     * no such message.
+     */
+    properties = sms_cache_lookup_full (info->modem, properties, &info->error);
+    if (properties)
+        mm_callback_info_set_data (info, GS_HASH_TAG, properties,
+                                   (GDestroyNotify) g_hash_table_unref);
 
 out:
     mm_callback_info_schedule (info);
@@ -4383,6 +4655,25 @@ sms_get (MMModemGsmSms *modem,
     MMCallbackInfo *info;
     char *command;
     MMAtSerialPort *port;
+    MMGenericGsmPrivate *priv =
+            MM_GENERIC_GSM_GET_PRIVATE (MM_GENERIC_GSM (modem));
+    GHashTable *properties;
+    GError *error = NULL;
+
+    properties = g_hash_table_lookup (priv->sms_contents, GUINT_TO_POINTER (idx));
+    if (properties != NULL) {
+        g_hash_table_ref (properties);
+        properties = sms_cache_lookup_full (MM_MODEM (modem), properties, &error);
+        if (properties == NULL) {
+            error = g_error_new (MM_MODEM_ERROR,
+                                 MM_MODEM_ERROR_GENERAL,
+                                 "No SMS found");
+        }
+        callback (modem, properties, error, user_data);
+        if (properties != NULL)
+            g_hash_table_unref (properties);
+        return;
+    }
 
     info = mm_callback_info_new_full (MM_MODEM (modem),
                                       sms_get_invoke,
@@ -4395,10 +4686,18 @@ sms_get (MMModemGsmSms *modem,
         return;
     }
 
-    command = g_strdup_printf ("+CMGR=%d\r\n", idx);
+    command = g_strdup_printf ("+CMGR=%d", idx);
+    priv->sms_fetch_pending = idx;
     mm_at_serial_port_queue_command (port, command, 10, sms_get_done, info);
 }
 
+typedef struct {
+    MMGenericGsmPrivate *priv;
+    MMCallbackInfo *info;
+    SMSMultiPartMessage *mpm;
+    int deleting;
+} SMSDeleteProgress;
+
 static void
 sms_delete_done (MMAtSerialPort *port,
                  GString *response,
@@ -4419,6 +4718,50 @@ sms_delete_done (MMAtSerialPort *port,
 }
 
 static void
+sms_delete_multi_next (MMAtSerialPort *port,
+                       GString *response,
+                       GError *error,
+                       gpointer user_data)
+{
+    SMSDeleteProgress *progress = (SMSDeleteProgress *)user_data;
+
+    /* If the modem has already been removed, return without
+     * scheduling callback */
+    if (mm_callback_info_check_modem_removed (progress->info))
+        goto done;
+
+    if (error)
+        progress->info->error = g_error_copy (error);
+
+    for (progress->deleting++ ;
+         progress->deleting < progress->mpm->numparts ;
+         progress->deleting++)
+        if (progress->mpm->parts[progress->deleting] != 0)
+            break;
+    if (progress->deleting < progress->mpm->numparts) {
+        GHashTable *properties;
+        char *command;
+        guint idx;
+
+        idx = progress->mpm->parts[progress->deleting];
+        command = g_strdup_printf ("+CMGD=%d", idx);
+        mm_at_serial_port_queue_command (port, command, 10,
+                                         sms_delete_multi_next, progress);
+        properties = g_hash_table_lookup (progress->priv->sms_contents, GUINT_TO_POINTER (idx));
+        g_hash_table_remove (progress->priv->sms_contents, GUINT_TO_POINTER (idx));
+        g_hash_table_remove (progress->priv->sms_present, GUINT_TO_POINTER (idx));
+        g_hash_table_unref (properties);
+        return;
+    }
+
+    mm_callback_info_schedule (progress->info);
+done:
+    g_free (progress->mpm->parts);
+    g_free (progress->mpm);
+    g_free (progress);
+}
+
+static void
 sms_delete (MMModemGsmSms *modem,
             guint idx,
             MMModemFn callback,
@@ -4428,18 +4771,92 @@ sms_delete (MMModemGsmSms *modem,
     char *command;
     MMAtSerialPort *port;
     MMGenericGsmPrivate *priv = MM_GENERIC_GSM_GET_PRIVATE (MM_GENERIC_GSM (modem));
+    GHashTable *properties;
+    MMAtSerialResponseFn next_callback;
+    GValue *ref;
 
     info = mm_callback_info_new (MM_MODEM (modem), callback, user_data);
-    g_hash_table_remove (priv->sms_present, GINT_TO_POINTER (idx));
+
+    properties = g_hash_table_lookup (priv->sms_contents, GINT_TO_POINTER (idx));
+    if (properties == NULL) {
+        /*
+         * TODO(njw): This assumes our cache is valid. If we doubt this, we should just
+         * run the delete anyway and let that return the nonexistent-message error.
+         */
+        info->error = g_error_new (MM_MODEM_ERROR,
+                                   MM_MODEM_ERROR_GENERAL,
+                                   "No SMS to delete");
+        mm_callback_info_schedule (info);
+        return;
+    }
 
     port = mm_generic_gsm_get_best_at_port (MM_GENERIC_GSM (modem), &info->error);
     if (!port) {
         mm_callback_info_schedule (info);
+        g_hash_table_remove (priv->sms_contents, GINT_TO_POINTER (idx));
+        g_hash_table_unref (properties);
         return;
     }
 
-    command = g_strdup_printf ("+CMGD=%d\r\n", idx);
-    mm_at_serial_port_queue_command (port, command, 10, sms_delete_done, info);
+    user_data = info;
+    next_callback = sms_delete_done;
+    ref = g_hash_table_lookup (properties, "concat-reference");
+    if (ref != NULL) {
+        SMSMultiPartMessage *mpm;
+        SMSDeleteProgress *progress;
+        guint refnum;
+
+        refnum = g_value_get_uint (ref);
+        mpm = g_hash_table_lookup (priv->sms_parts, GUINT_TO_POINTER (refnum));
+        if (mpm == NULL) {
+            info->error = g_error_new (MM_MODEM_ERROR,
+                                       MM_MODEM_ERROR_GENERAL,
+                                       "Internal error - no part array for multipart SMS");
+            mm_callback_info_schedule (info);
+            g_hash_table_remove (priv->sms_contents, GINT_TO_POINTER (idx));
+            g_hash_table_unref (properties);
+            return;
+        }
+        /* Only allow the delete operation on the main index number. */
+        if (idx != mpm->index) {
+            info->error = g_error_new (MM_MODEM_ERROR,
+                                       MM_MODEM_ERROR_GENERAL,
+                                       "No SMS to delete");
+            mm_callback_info_schedule (info);
+            return;
+        }
+
+        g_hash_table_remove (priv->sms_parts, GUINT_TO_POINTER (refnum));
+        progress = g_malloc0 (sizeof(*progress));
+        progress->priv = priv;
+        progress->info = info;
+        progress->mpm = mpm;
+        for (progress->deleting = 0 ;
+             progress->deleting < mpm->numparts ;
+             progress->deleting++)
+            if (mpm->parts[progress->deleting] != 0)
+                break;
+        user_data = progress;
+        next_callback = sms_delete_multi_next;
+        idx = progress->mpm->parts[progress->deleting];
+        properties = g_hash_table_lookup (priv->sms_contents, GINT_TO_POINTER (idx));
+    }
+    g_hash_table_remove (priv->sms_contents, GUINT_TO_POINTER (idx));
+    g_hash_table_remove (priv->sms_present, GUINT_TO_POINTER (idx));
+    g_hash_table_unref (properties);
+
+    command = g_strdup_printf ("+CMGD=%d", idx);
+    mm_at_serial_port_queue_command (port, command, 10, next_callback,
+                                     user_data);
+}
+
+static void
+free_list_results (gpointer data)
+{
+    GPtrArray *results = (GPtrArray *) data;
+
+    g_ptr_array_foreach (results, (GFunc) g_hash_table_unref, NULL);
+    g_ptr_array_free (results, TRUE);
 }
 
 static void
@@ -4482,21 +4899,19 @@ sms_list_done (MMAtSerialPort *port,
             if (properties) {
                 g_hash_table_insert (properties, "index",
                                      simple_uint_value (idx));
-                g_ptr_array_add (results, properties);
+                sms_cache_insert (info->modem, properties, idx);
+                /* Only add complete messages to the results */
+                properties = sms_cache_lookup_full (info->modem, properties, &info->error);
+                if (properties)
+                    g_ptr_array_add (results, properties);
             } else {
                 /* Ignore the error */
                 g_clear_error(&local);
             }
         }
-        /*
-         * todo(njw): mm_gsm_destroy_scan_data does what we want
-         * (destroys a GPtrArray of g_hash_tables), but it should be
-         * renamed to describe that or there should be a function
-         * named for what we're doing here.
-         */
         if (results)
             mm_callback_info_set_data (info, "list-sms", results,
-                                       mm_gsm_destroy_scan_data);
+                                       free_list_results);
     }
 
     mm_callback_info_schedule (info);
@@ -4532,7 +4947,7 @@ sms_list (MMModemGsmSms *modem,
         return;
     }
 
-    command = g_strdup_printf ("+CMGL=4\r\n");
+    command = g_strdup_printf ("+CMGL=4");
     mm_at_serial_port_queue_command (port, command, 10, sms_list_done, info);
 }
 
@@ -5579,6 +5994,8 @@ mm_generic_gsm_init (MMGenericGsm *self)
     priv->reg_regex = mm_gsm_creg_regex_get (TRUE);
     priv->roam_allowed = TRUE;
     priv->sms_present = g_hash_table_new (g_direct_hash, g_direct_equal);
+    priv->sms_contents = g_hash_table_new (g_direct_hash, g_direct_equal);
+    priv->sms_parts = g_hash_table_new (g_direct_hash, g_direct_equal);
 
     mm_properties_changed_signal_register_property (G_OBJECT (self),
                                                     MM_MODEM_GSM_NETWORK_ALLOWED_MODE,
@@ -5812,6 +6229,8 @@ finalize (GObject *object)
     g_free (priv->oper_name);
     g_free (priv->simid);
     g_hash_table_destroy (priv->sms_present);
+    g_hash_table_destroy (priv->sms_contents);
+    g_hash_table_destroy (priv->sms_parts);
 
     G_OBJECT_CLASS (mm_generic_gsm_parent_class)->finalize (object);
 }
diff --git a/src/mm-sms-utils.c b/src/mm-sms-utils.c
index 1b32a79..df706ea 100644
--- a/src/mm-sms-utils.c
+++ b/src/mm-sms-utils.c
@@ -22,6 +22,7 @@
 #include "mm-errors.h"
 #include "mm-utils.h"
 #include "mm-sms-utils.h"
+#include "mm-log.h"
 
 #define SMS_TP_MTI_MASK               0x03
 #define  SMS_TP_MTI_SMS_DELIVER       0x00
@@ -247,18 +248,6 @@ simple_uint_value (guint32 i)
 }
 
 static GValue *
-simple_boolean_value (gboolean b)
-{
-    GValue *val;
-
-    val = g_slice_new0 (GValue);
-    g_value_init (val, G_TYPE_BOOLEAN);
-    g_value_set_boolean (val, b);
-
-    return val;
-}
-
-static GValue *
 simple_string_value (const char *str)
 {
     GValue *val;
@@ -352,18 +341,79 @@ sms_parse_pdu (const char *hexpdu, GError **error)
         return NULL;
     }
 
+    properties = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
+                                        simple_free_gvalue);
+
     smsc_addr = sms_decode_address (&pdu[1], 2 * (pdu[0] - 1));
+    g_hash_table_insert (properties, "smsc",
+                         simple_string_value (smsc_addr));
+    g_free (smsc_addr);
+
     sender_addr = sms_decode_address (&pdu[msg_start_offset + 2],
                                       pdu[msg_start_offset + 1]);
+    g_hash_table_insert (properties, "number",
+                         simple_string_value (sender_addr));
+    g_free (sender_addr);
+
     sc_timestamp = sms_decode_timestamp (&pdu[tp_dcs_offset + 1]);
+    g_hash_table_insert (properties, "timestamp",
+                         simple_string_value (sc_timestamp));
+    g_free (sc_timestamp);
+
     bit_offset = 0;
     if (pdu[msg_start_offset] & SMS_TP_UDHI) {
+        int udhl, end, offset;
+        udhl = pdu[user_data_offset] + 1;
+        end = user_data_offset + udhl;
+
+        for (offset = user_data_offset + 1; offset < end;) {
+            guint8 ie_id, ie_len;
+
+            ie_id = pdu[offset++];
+            ie_len = pdu[offset++];
+
+            switch (ie_id) {
+                case 0x00:
+                    /*
+                     * Ignore the IE if one of the following is true:
+                     *  - it claims to be part 0 of M
+                     *  - it claims to be part N of M, N > M
+                     */
+                    if (pdu[offset + 2] == 0 ||
+                        pdu[offset + 2] > pdu[offset + 1])
+                        break;
+
+                    g_hash_table_insert (properties, "concat-reference",
+                                         simple_uint_value (pdu[offset]));
+                    g_hash_table_insert (properties, "concat-max",
+                                         simple_uint_value (pdu[offset + 1]));
+                    g_hash_table_insert (properties, "concat-sequence",
+                                         simple_uint_value (pdu[offset + 2]));
+                    break;
+                case 0x08:
+                    /* Concatenated short message, 16-bit reference */
+                    if (pdu[offset + 3] == 0 ||
+                        pdu[offset + 3] > pdu[offset + 2])
+                        break;
+
+                    g_hash_table_insert (properties, "concat-reference",
+                                         simple_uint_value (
+                                             (pdu[offset] << 8)
+                                             | pdu[offset + 1]));
+                    g_hash_table_insert (properties, "concat-max",
+                                         simple_uint_value (pdu[offset + 2]));
+                    g_hash_table_insert (properties, "concat-sequence",
+                                         simple_uint_value (pdu[offset + 3]));
+                    break;
+            }
+
+            offset += ie_len;
+        }
+
         /*
-         * Skip over the user data headers to prevent it from being
+         * Move past the user data headers to prevent it from being
          * decoded into garbage text.
          */
-        int udhl;
-        udhl = pdu[user_data_offset] + 1;
         user_data_offset += udhl;
         if (user_data_encoding == MM_SMS_ENCODING_GSM7) {
             /*
@@ -378,29 +428,15 @@ sms_parse_pdu (const char *hexpdu, GError **error)
 
     msg_text = sms_decode_text (&pdu[user_data_offset], user_data_len,
                                 user_data_encoding, bit_offset);
-
-    properties = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
-                                        simple_free_gvalue);
-    g_hash_table_insert (properties, "number",
-                         simple_string_value (sender_addr));
     g_hash_table_insert (properties, "text",
                          simple_string_value (msg_text));
-    g_hash_table_insert (properties, "smsc",
-                         simple_string_value (smsc_addr));
-    g_hash_table_insert (properties, "timestamp",
-                         simple_string_value (sc_timestamp));
+    g_free (msg_text);
+
     if (pdu[tp_dcs_offset] & SMS_DCS_CLASS_VALID)
         g_hash_table_insert (properties, "class",
                              simple_uint_value (pdu[tp_dcs_offset] &
                                                 SMS_DCS_CLASS_MASK));
-    g_hash_table_insert (properties, "completed", simple_boolean_value (TRUE));
-
-    g_free (smsc_addr);
-    g_free (sender_addr);
-    g_free (sc_timestamp);
-    g_free (msg_text);
     g_free (pdu);
 
-
     return properties;
 }
-- 
1.7.3.1

