File indexing completed on 2024-05-05 17:32:29

0001 // SPDX-FileCopyrightText: 2021 Michael Lang <criticaltemp@protonmail.com>
0002 //
0003 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 #include "mms.h"
0006 #include "settingsmanager.h"
0007 
0008 #include <QBuffer>
0009 #include <QDebug>
0010 #include <QDir>
0011 #include <QImage>
0012 #include <QJsonArray>
0013 #include <QJsonDocument>
0014 #include <QJsonObject>
0015 #include <QMimeDatabase>
0016 #include <QMimeType>
0017 #include <QStandardPaths>
0018 #include <QUuid>
0019 
0020 #include <random>
0021 
0022 constexpr unsigned char MMS_CODE_END_STRING = 0x00;
0023 
0024 constexpr unsigned char MMS_CODE_VERSION = 0x93; // v1.3
0025 
0026 constexpr unsigned char MMS_CODE_NO = 0x80;
0027 constexpr unsigned char MMS_CODE_YES = 0x81;
0028 
0029 constexpr unsigned char MMS_CODE_MULTIPART_MIXED = 0xA3;
0030 constexpr unsigned char MMS_CODE_MULTIPART_RELATED = 0xB3;
0031 
0032 constexpr unsigned char MMS_CODE_ADDRESS_PRESENT_TOKEN = 0x80;
0033 constexpr unsigned char MMS_CODE_INSERT_ADDRESS_TOKEN = 0x81;
0034 
0035 constexpr unsigned char MMS_CODE_ABSOLUTE_TOKEN = 0x80;
0036 constexpr unsigned char MMS_CODE_RELATIVE_TOKEN = 0x81;
0037 
0038 constexpr unsigned char MMS_CODE_READ = 0x80;
0039 constexpr unsigned char MMS_CODE_DELETE_WITHOUT_READ = 0x81;
0040 
0041 constexpr unsigned char MMS_CODE_CANCEL_REQUEST_SUCCESSFULLY_RECEIVED = 0x80;
0042 
0043 constexpr unsigned char MMS_CODE_PRIORITY_LOW = 0x80;
0044 constexpr unsigned char MMS_CODE_PRIORITY_NORMAL = 0x81;
0045 constexpr unsigned char MMS_CODE_PRIORITY_HIGH = 0x82;
0046 
0047 constexpr unsigned char MMS_CODE_MESSAGE_CLASS_PERSONAL = 0x80;
0048 constexpr unsigned char MMS_CODE_MESSAGE_CLASS_ADVERTISEMENT = 0x81;
0049 constexpr unsigned char MMS_CODE_MESSAGE_CLASS_INFORMATIONAL = 0x82;
0050 constexpr unsigned char MMS_CODE_MESSAGE_CLASS_AUTO = 0x83;
0051 
0052 constexpr unsigned char MMS_HEADER_CANCEL_STATUS = 0xBF;
0053 constexpr unsigned char MMS_HEADER_CONTENT_LOCATION = 0x83;
0054 constexpr unsigned char MMS_HEADER_CONTENT_TYPE = 0x84;
0055 constexpr unsigned char MMS_HEADER_DATE = 0x85;
0056 constexpr unsigned char MMS_HEADER_DELIVERY_REPORT = 0x86;
0057 constexpr unsigned char MMS_HEADER_DELIVERY_TIME = 0x87;
0058 constexpr unsigned char MMS_HEADER_FROM = 0x89;
0059 constexpr unsigned char MMS_HEADER_PRIORITY = 0x8F;
0060 constexpr unsigned char MMS_HEADER_MESSAGE_CLASS = 0x8A;
0061 constexpr unsigned char MMS_HEADER_MESSAGE_TYPE = 0x8C;
0062 constexpr unsigned char MMS_HEADER_MESSAGE_ID = 0x8B;
0063 constexpr unsigned char MMS_HEADER_READ_STATUS = 0x9B;
0064 constexpr unsigned char MMS_HEADER_READ_REPORT = 0x90;
0065 constexpr unsigned char MMS_HEADER_REPORT_ALLOWED = 0x91;
0066 constexpr unsigned char MMS_HEADER_SENDER_VISIBILITY = 0x94;
0067 constexpr unsigned char MMS_HEADER_STATUS = 0x95;
0068 constexpr unsigned char MMS_HEADER_SUBJECT = 0x96;
0069 constexpr unsigned char MMS_HEADER_TO = 0x97;
0070 constexpr unsigned char MMS_HEADER_TRANSACTION_ID = 0x98;
0071 constexpr unsigned char MMS_HEADER_VERSION = 0x8D;
0072 
0073 constexpr unsigned char MMS_PARAM_NAME = 0x85;
0074 constexpr unsigned char MMS_PARAM_START = 0x8A;
0075 constexpr unsigned char MMS_PARAM_TYPE_SPEC = 0x89;
0076 
0077 constexpr unsigned char MMS_PDU_TYPE_PUSH = 0x06;
0078 
0079 Mms::Mms(QObject *parent)
0080     : QObject(parent)
0081 {
0082 }
0083 
0084 QByteArray Mms::encodeNotifyResponse(const QString &transactionId, const QString &status)
0085 {
0086     QByteArray data;
0087     data.append(encodeHeaderPrefix(SL("m-notifyresp-ind"), transactionId));
0088 
0089     data.append(MMS_HEADER_STATUS);
0090     data.append(encodeValueFromList(status, STATUS_VALUES));
0091 
0092     data.append(MMS_HEADER_REPORT_ALLOWED);
0093     data.append((SettingsManager::self()->shareDeliveryStatus() ? MMS_CODE_YES : MMS_CODE_NO));
0094 
0095     return data;
0096 }
0097 
0098 QByteArray Mms::encodeDeliveryAcknowledgement(const QString &transactionId)
0099 {
0100     QByteArray data;
0101     data.append(encodeHeaderPrefix(SL("m-acknowledge-ind"), transactionId));
0102 
0103     data.append(MMS_HEADER_REPORT_ALLOWED);
0104     data.append((SettingsManager::self()->shareDeliveryStatus() ? MMS_CODE_YES : MMS_CODE_NO));
0105 
0106     return data;
0107 }
0108 
0109 // confirms the read status of the MM to the MMS Proxy-Relay - send when marking mms messages as read
0110 QByteArray Mms::encodeReadReport(const QString &messageId)
0111 {
0112     QByteArray data;
0113     data.append(encodeHeaderPrefix(SL("m-read-rec-ind"), messageId, true));
0114 
0115     data.append(MMS_HEADER_TO);
0116     // data.append();
0117 
0118     data.append(MMS_HEADER_FROM);
0119     // data.append();
0120 
0121     data.append(MMS_HEADER_READ_STATUS);
0122     data.append(MMS_CODE_READ); // MMS_CODE_DELETE_WITHOUT_READ
0123 
0124     // TODO: finish this
0125     return data;
0126 }
0127 
0128 // TODO add forwardMessage method - m-forward-req with handling of m-forward-conf - from, to, contentLocation, reportAllowed, deliveryReport, readReport
0129 // TODO add deleteRequest method - m-delete-req with handling of m-delete-conf - contentLocation
0130 
0131 QByteArray Mms::encodeCancelResponse(const QString &transactionId)
0132 {
0133     QByteArray data;
0134     data.append(encodeHeaderPrefix(SL("m-cancel-conf"), transactionId));
0135 
0136     data.append(MMS_HEADER_CANCEL_STATUS);
0137     data.append(MMS_CODE_CANCEL_REQUEST_SUCCESSFULLY_RECEIVED);
0138 
0139     return data;
0140 }
0141 
0142 void Mms::decodeNotification(MmsMessage &message, const QByteArray &data)
0143 {
0144     // Save copy for testing/debugging purposes. Delete later
0145     const QString folderTemp = saveLocation(SL("notifications"));
0146     const QString pathTemp = folderTemp + SL("/") + generateRandomId();
0147     saveData(data, pathTemp);
0148 
0149     int pos = 0;
0150 
0151     if (data.at(++pos) != MMS_PDU_TYPE_PUSH)
0152         return;
0153 
0154     int headerLen = unsignedInt(data, pos);
0155 
0156     if (data.at(pos) == 40) {
0157         // skip the open parenthesis
0158         pos += 2;
0159     }
0160 
0161     message.contentType = contentTypeValue(data, pos, message);
0162     if (message.contentType.isEmpty()) {
0163         qDebug() << "unknown content type";
0164         return;
0165     }
0166 
0167     pos += headerLen - pos;
0168     while (decodeHeader(message, data, pos))
0169         ;
0170 
0171     // use the message transaction id if url does not contain an id
0172     if (message.contentLocation.endsWith(SL("="))) {
0173         message.contentLocation.append(message.transactionId);
0174     }
0175 
0176     // remove if successfully decoded
0177     if (!message.transactionId.isEmpty()) {
0178         QFile file(pathTemp);
0179         file.remove();
0180     }
0181 }
0182 
0183 void Mms::decodeConfirmation(MmsMessage &message, const QByteArray &data)
0184 {
0185     int pos = -1;
0186     while (decodeHeader(message, data, pos))
0187         ;
0188 
0189     // Save copy for testing/debugging purposes. Delete later
0190     if (!message.transactionId.isEmpty()) {
0191         const QString folder = saveLocation(SL("confirmations"));
0192         const QString path = folder + SL("/") + message.transactionId;
0193         saveData(data, path);
0194     }
0195 }
0196 
0197 void Mms::decodeMessage(MmsMessage &message, const QByteArray &data)
0198 {
0199     // Save copy for testing/debugging purposes. Delete later
0200     const QString folderTemp = saveLocation(SL("downloads"));
0201     const QString pathTemp = folderTemp + SL("/") + generateRandomId();
0202     saveData(data, pathTemp);
0203 
0204     int pos = -1;
0205     while (decodeHeader(message, data, pos))
0206         ;
0207 
0208     // add from number and normalize numbers
0209     QStringList numbers = message.to;
0210     if (PhoneNumber(message.from).isValid()) {
0211         numbers.append(message.from);
0212     }
0213     numbers.sort();
0214     message.phoneNumberList = PhoneNumberList(numbers.join(u'~'));
0215 
0216     // remove own number if present
0217     const int index = message.phoneNumberList.indexOf(message.ownNumber);
0218     if (index >= 0) {
0219         message.phoneNumberList.removeAt(index);
0220     }
0221 
0222     if (message.contentType == SL("application/vnd.wap.multipart.*") || message.contentType == SL("application/vnd.wap.multipart.mixed")
0223         || message.contentType == SL("application/vnd.wap.multipart.form-data") || message.contentType == SL("application/vnd.wap.multipart.byteranges")
0224         || message.contentType == SL("application/vnd.wap.multipart.alternative") || message.contentType == SL("application/vnd.wap.multipart.related")) {
0225         decodeMessageBody(message, data, pos);
0226     }
0227 
0228     // remove if successfully decoded or if is an outgoing message
0229     if (!message.messageId.isEmpty() || !message.ownNumber.isValid() || PhoneNumber(message.from) == message.ownNumber) {
0230         QFile file(pathTemp);
0231         file.remove();
0232     }
0233 }
0234 
0235 void Mms::encodeMessage(MmsMessage &message, QByteArray &data, const QStringList &files, qint64 totalSize)
0236 {
0237     auto settings = SettingsManager::self();
0238     int sizeLimit = settings->totalMaxAttachmentSize() * 1024;
0239     int parts = files.count() + (message.text.isEmpty() ? 0 : 1);
0240 
0241     data.append(encodeHeaderPrefix(SL("m-send-req"), generateTransactionId()));
0242 
0243     data.append(MMS_HEADER_DATE);
0244     data.append(encodeLongInteger(QDateTime::currentSecsSinceEpoch())); // optional
0245 
0246     data.append(MMS_HEADER_FROM);
0247     data.append(encodeFromValue(message.from.remove(SL("-")).remove(SL(" ")) + SL("/TYPE=PLMN")));
0248     message.from.clear();
0249 
0250     for (auto &number : message.to) {
0251         data.append(MMS_HEADER_TO);
0252         data.append(encodeEncodedStringValue(number.remove(SL("-")).remove(SL(" ")) + SL("/TYPE=PLMN")));
0253     }
0254     message.to.clear();
0255 
0256     // TODO add message setting for this
0257     // data.append(MMS_HEADER_SENDER_VISIBILITY);
0258     // data.append(MMS_CODE_YES);
0259 
0260     // TODO add message setting for this
0261     // data.append(MMS_HEADER_SUBJECT);
0262     // data.append(encodeEncodedStringValue(SL("NoSubject")));
0263 
0264     // TODO add message setting for this
0265     data.append(MMS_HEADER_MESSAGE_CLASS);
0266     data.append(MMS_CODE_MESSAGE_CLASS_PERSONAL);
0267 
0268     // TODO add message setting for this
0269     // data.append(MMS_HEADER_PRIORITY);
0270     // data.append(MMS_CODE_PRIORITY_NORMAL);
0271 
0272     // TODO add message setting for this
0273     // data.append(MMS_HEADER_DELIVERY_TIME);
0274     // data.append(encodeLongInteger(QDateTime::currentSecsSinceEpoch() + delaySeconds));
0275 
0276     data.append(MMS_HEADER_READ_REPORT);
0277     data.append((settings->requestReadReports() ? MMS_CODE_YES : MMS_CODE_NO));
0278 
0279     data.append(MMS_HEADER_DELIVERY_REPORT);
0280     data.append((settings->requestDeliveryReports() ? MMS_CODE_YES : MMS_CODE_NO));
0281 
0282     data.append(MMS_HEADER_CONTENT_TYPE);
0283     if (parts > 1 && settings->autoCreateSmil()) {
0284         data.append(0x1B);
0285         data.append(MMS_CODE_MULTIPART_RELATED);
0286 
0287         // add param fields
0288         data.append(MMS_PARAM_START);
0289         data.append(encodeTextValue(SL("<smil>"))); // start
0290         data.append(MMS_PARAM_TYPE_SPEC);
0291         data.append(encodeTextValue(SL("application/smil")));
0292 
0293         parts++; // for the added smil part
0294     } else {
0295         data.append(MMS_CODE_MULTIPART_MIXED);
0296     }
0297 
0298     // message body parts
0299     data.append(encodeUnsignedInt(parts));
0300 
0301     int smilInsertPos = data.length();
0302     QString smil;
0303 
0304     for (const auto &path : files) {
0305         QFile file(path.mid(6, path.length() - 6)); // remove "file://" prefix
0306         file.open(QIODevice::ReadOnly);
0307         QByteArray fileData = file.readAll();
0308         QString name = path.mid(path.lastIndexOf(SL("/")) + 1);
0309         QMimeDatabase db;
0310         QString mimeType = db.mimeTypeForData(fileData).name();
0311 
0312         /*
0313          * WSP Content Type codes do not have numeric identifiers for the
0314          * following media types. Use alternate supported media types instead.
0315          */
0316         if (mimeType.toLower() == SL("text/vcard")) {
0317             mimeType = SL("text/x-vCard");
0318         } else if (mimeType.toLower() == SL("text/calendar")) {
0319             mimeType = SL("text/x-vCalendar");
0320         }
0321 
0322         // resize/transcode if total size exceeds max size
0323         if (totalSize > sizeLimit) {
0324             if (mimeType.contains(SL("image")) && mimeType != SL("image/gif")) {
0325                 name = SL("Resized_") + name;
0326                 QImage image, imageModified;
0327 
0328                 // conversion to jpg should reduce the size enough in most cases
0329                 if (mimeType != SL("image/jpeg")) {
0330                     totalSize -= fileData.length();
0331                     imageModified.loadFromData(fileData);
0332                     fileData.clear();
0333                     QBuffer buffer(&fileData);
0334                     buffer.open(QIODevice::WriteOnly);
0335                     imageModified.save(&buffer, "JPG");
0336                     totalSize += fileData.length();
0337 
0338                     // update extension and mimeType
0339                     int extPos = name.lastIndexOf(SL("."));
0340                     name = name.remove(extPos, name.length() - extPos) + SL(".jpg");
0341                     mimeType = SL("image/jpeg");
0342                 }
0343 
0344                 const float ratio = (float)fileData.length() / totalSize;
0345                 while (fileData.length() * ratio > sizeLimit * ratio / files.count()) {
0346                     image.loadFromData(fileData);
0347                     float factor = (float)fileData.length() / sizeLimit > 8 ? 2 : 1.1;
0348                     imageModified = image.scaled(image.width() / factor, image.height() / factor, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0349 
0350                     fileData.clear();
0351                     QBuffer buffer(&fileData);
0352                     buffer.open(QIODevice::WriteOnly);
0353                     imageModified.save(&buffer, "JPG");
0354                 }
0355             }
0356             // TODO investigate if feasible to convert/transcode audio?
0357         }
0358 
0359         // shorten name and preserve extension
0360         if (name.length() > 27) {
0361             const int extPos = name.lastIndexOf(SL("."));
0362             const int extLen = name.length() - extPos;
0363             if (extLen < 5) {
0364                 name = name.left(27 - extLen - 1) + name.right(extLen);
0365             } else {
0366                 name = name.left(27);
0367             }
0368         }
0369 
0370         data.append(encodePart(encodeValueFromList(mimeType, CONTENT_TYPES), name, fileData));
0371 
0372         if (parts > 1 && settings->autoCreateSmil()) {
0373             smil.append(SL(R"(<par dur="5000ms">)"));
0374 
0375             if (mimeType.contains(SL("image"))) {
0376                 smil.append(SL(R"(<img src=")"));
0377                 smil.append(name);
0378                 smil.append(SL(R"(" region="Image" />)"));
0379             }
0380 
0381             smil.append(SL(R"(</par>)"));
0382         }
0383     }
0384 
0385     // add text as one of the parts
0386     if (!message.text.isEmpty()) {
0387         const QString name = SL("text01.txt");
0388         data.append(encodePart(encodeValueFromList(SL("text/plain"), CONTENT_TYPES), name, message.text.toUtf8()));
0389         message.text.clear();
0390 
0391         if (parts > 1 && settings->autoCreateSmil()) {
0392             smil.remove(smil.length() - 6, 6);
0393             smil.append(SL(R"(<text src=")"));
0394             smil.append(name);
0395             smil.append(SL(R"(" region="Text" />)"));
0396             smil.append(SL(R"(</par>)"));
0397         }
0398     }
0399 
0400     if (parts > 1 && settings->autoCreateSmil()) {
0401         // standard layout - image and text, other files currently not included/supported
0402         smil.prepend(SL(
0403             R"(<smil><head><layout><root-layout/><region id="Image" fit="meet" top="0" left="0" height="80%" width="100%"/><region id="Text" top="80%" left="0" height="20%" width="100%"/></layout></head><body>)"));
0404 
0405         smil.append(SL(R"(</body></smil>)"));
0406 
0407         const QString name = SL("smil.xml");
0408         QByteArray params;
0409         params.append((unsigned char)0xC0);
0410         params.append(R"(")");
0411         params.append(encodeTextValue(SL("<smil>")));
0412         params.append((unsigned char)0x8E);
0413         params.append(encodeTextValue(SL("smil.xml")));
0414         QByteArray smilData = encodePart(encodeTextValue(SL("application/smil")), name, smil.toUtf8(), params);
0415         data.insert(smilInsertPos, smilData);
0416     }
0417 
0418     // decode message here so attachments get formatted the same as a message being sent
0419     decodeMessage(message, data);
0420 }
0421 
0422 bool Mms::decodeHeader(MmsMessage &message, const QByteArray &data, int &pos)
0423 {
0424     if ((pos + 1) >= data.length()) {
0425         return false;
0426     }
0427 
0428     unsigned char field = data.at(++pos);
0429     for (const auto &headerField : HEADER_FIELDS) {
0430         if (headerField.id == field) {
0431             QVariant val;
0432             QStringView type = headerField.type;
0433             if (type == u"")
0434                 val = data.at(++pos);
0435             else if (type == u"unsignedInt")
0436                 val = unsignedInt(data, pos);
0437             else if (type == u"longInteger")
0438                 val = longInteger(data, pos);
0439             else if (type == u"shortInteger")
0440                 val = shortInteger(data, pos);
0441             else if (type == u"integerValue")
0442                 val = integerValue(data, pos);
0443             else if (type == u"textString")
0444                 val = textString(data, pos);
0445             else if (type == u"encodedStringValue")
0446                 val = encodedStringValue(data, pos);
0447             else if (type == u"fromValue")
0448                 val = fromValue(data, pos);
0449             else if (type == u"contentTypeValue")
0450                 val = contentTypeValue(data, pos, message);
0451             else if (type == u"messageTypeValue")
0452                 val = messageTypeValue(data, pos);
0453             else if (type == u"dateValue")
0454                 val = dateValue(data, pos);
0455             else if (type == u"messageClassValue")
0456                 val = messageClassValue(data, pos);
0457             else if (type == u"mmsVersion")
0458                 val = mmsVersion(data, pos);
0459 
0460             // capture relevant values
0461             QStringView name = headerField.name;
0462             if (name == u"messageType")
0463                 message.messageType = val.toString();
0464             else if (name == u"transactionId")
0465                 message.transactionId = val.toString();
0466             else if (name == u"messageId")
0467                 message.messageId = val.toString();
0468             else if (name == u"messageSize")
0469                 message.messageSize = val.toInt();
0470             else if (name == u"expiry")
0471                 message.expiry = val.toDateTime();
0472             else if (name == u"date")
0473                 message.date = val.toDateTime();
0474             else if (name == u"to")
0475                 message.to.append(PhoneNumber(val.toString().remove(QLatin1String("/TYPE=PLMN"))).toInternational());
0476             else if (name == u"from")
0477                 message.from = PhoneNumber(val.toString().remove(QLatin1String("/TYPE=PLMN"))).toInternational();
0478             else if (name == u"subject")
0479                 message.subject = val.toString();
0480             else if (name == u"contentType")
0481                 message.contentType = val.toString();
0482             else if (name == u"contentLocation")
0483                 message.contentLocation = val.toString();
0484             else if (name == u"responseStatus")
0485                 message.responseStatus = val.toInt();
0486             else if (name == u"responseText")
0487                 message.responseText = val.toString();
0488             // else qDebug() << headerField.name << ":" << val.toString();
0489 
0490             return true;
0491         }
0492     }
0493 
0494     pos--;
0495     return false;
0496 }
0497 
0498 QByteArray Mms::encodeHeaderPrefix(const QString &type, const QString &id, bool msgId)
0499 {
0500     QByteArray data;
0501     data.append(MMS_HEADER_MESSAGE_TYPE);
0502     data.append(encodeValueFromList(type, MESSAGE_TYPES));
0503 
0504     data.append(msgId ? MMS_HEADER_MESSAGE_ID : MMS_HEADER_TRANSACTION_ID);
0505     data.append(encodeTextValue(id));
0506 
0507     data.append(MMS_HEADER_VERSION);
0508     data.append(MMS_CODE_VERSION);
0509 
0510     return data;
0511 }
0512 
0513 bool Mms::decodeMessageBody(MmsMessage &message, const QByteArray &data, int &pos)
0514 {
0515     if ((pos + 1) >= data.length()) {
0516         return false;
0517     }
0518 
0519     int parts = unsignedInt(data, pos);
0520 
0521     // allow extra 2 parts for text and smil
0522     if (parts > SettingsManager::self()->maxAttachments() + 2) {
0523         qDebug() << "Max attachments exceeded! Parts:" << parts;
0524         return false;
0525     }
0526 
0527     const QString folder = QString::number(hash(message.phoneNumberList.toString()));
0528     const QString attachmentsFolder = saveLocation(SL("attachments"), folder);
0529 
0530     QJsonArray attachments;
0531 
0532     // the body header should not be confused with the actual mms header
0533     for (int i = 0; i < parts; i++) {
0534         int headerLen = unsignedInt(data, pos);
0535         int dataLen = unsignedInt(data, pos);
0536 
0537         int temp = pos;
0538         QString contentType = contentTypeValue(data, pos, message);
0539         pos = temp + 1;
0540 
0541         QByteArray fileData = data.mid(pos + headerLen, dataLen);
0542         pos += headerLen + dataLen - 1;
0543 
0544         if (contentType == SL("application/smil")) {
0545             message.smil += QString::fromUtf8(fileData);
0546             continue;
0547         } else {
0548             QMimeDatabase db;
0549             QMimeType mime = db.mimeTypeForData(fileData);
0550             QString fileName;
0551             QString path;
0552             QString text;
0553             QString name;
0554 
0555             if (contentType == SL("text/plain")) {
0556                 if (parts == 1 || (parts == 2 && !message.smil.isEmpty())) {
0557                     // no other content so just treat like regular text
0558                     message.text.append(QString::fromUtf8(fileData));
0559                     if (parts == 2) {
0560                         message.smil.clear();
0561                     }
0562                     continue;
0563                 } else {
0564                     text = QString::fromUtf8(fileData);
0565                 }
0566             } else {
0567                 // save file
0568                 fileName = generateRandomId() + SL(".") + mime.preferredSuffix();
0569                 path = attachmentsFolder + SL("/") + fileName;
0570                 saveData(fileData, path);
0571             }
0572 
0573             name = message.partNames.length() > i ? message.partNames[i] : fileName;
0574 
0575             // json data for use in message page
0576             QJsonObject object{
0577                 {SL("name"), name},
0578                 {SL("fileName"), fileName},
0579                 {SL("size"), dataLen},
0580                 {SL("mimeType"), mime.name()},
0581                 {SL("iconName"), mime.iconName()},
0582                 {SL("text"), text},
0583             };
0584 
0585             attachments.append(object);
0586         }
0587     }
0588 
0589     if (attachments.size() > 0) {
0590         QJsonDocument jsonDoc;
0591         jsonDoc.setArray(attachments);
0592         QByteArray fileText = jsonDoc.toJson(QJsonDocument::Compact);
0593         message.attachments = QString::fromUtf8(fileText);
0594     }
0595 
0596     return false;
0597 }
0598 
0599 QString Mms::generateRandomId()
0600 {
0601     QString intermediateId = SL("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");
0602     std::shuffle(intermediateId.begin(), intermediateId.end(), std::mt19937(std::random_device()()));
0603     intermediateId.truncate(15);
0604 
0605     return intermediateId;
0606 }
0607 
0608 QString Mms::generateTransactionId()
0609 {
0610     return SL("m-") + QUuid::createUuid().toString(QUuid::WithoutBraces);
0611 }
0612 
0613 QString Mms::saveLocation(const QString &folderName, const QString &subFolderName)
0614 {
0615     QString fileLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
0616     fileLocation.append(SL("/spacebar/") + folderName);
0617     if (!subFolderName.isEmpty()) {
0618         fileLocation.append(SL("/") + subFolderName);
0619     }
0620 
0621     if (!QDir().mkpath(fileLocation)) {
0622         qDebug() << "Could not create the storage directory at" << fileLocation;
0623     }
0624 
0625     return fileLocation;
0626 }
0627 
0628 bool Mms::saveData(const QByteArray &data, const QString &path)
0629 {
0630     QFile file(path);
0631     file.open(QIODevice::WriteOnly);
0632     file.write(data);
0633     file.close();
0634 
0635     return true;
0636 }
0637 
0638 QString Mms::contentTypeValue(const QByteArray &data, int &pos, MmsMessage &message)
0639 {
0640     QString type;
0641     if (data.at(pos + 1) & 0x80) {
0642         type = lookupValString(data, pos, CONTENT_TYPES);
0643     } else if (data.at(pos + 1) == 0 || data.at(pos + 1) >= 0x20) {
0644         type = textString(data, pos);
0645     } else {
0646         int len = valueLength(data, pos);
0647         int end = pos + len;
0648 
0649         // check for extension-media, next byte is between 32-127
0650         if (data.at(pos + 1) == 0 || (data.at(pos + 1) >= 32 && (unsigned char)data.at(pos + 1) <= 127)) {
0651             type = encodedStringValue(data, pos);
0652         } else if ((data.at(pos + 1) & 0x80) || data.at(pos + 1) <= 30) {
0653             type = lookupValString(data, pos, CONTENT_TYPES);
0654         }
0655 
0656         while (pos < end) {
0657             unsigned char field = data.at(++pos) & 0x7F;
0658             int len = PARAM_FIELDS.size();
0659             for (int i = 0; i < len; i++) {
0660                 if (PARAM_FIELDS[i].id == field) {
0661                     QVariant val;
0662                     QStringView type = PARAM_FIELDS[i].type;
0663                     if (type == u"unsignedInt")
0664                         val = unsignedInt(data, pos);
0665                     else if (type == u"longInteger")
0666                         val = longInteger(data, pos);
0667                     else if (type == u"shortInteger")
0668                         val = shortInteger(data, pos);
0669                     else if (type == u"integerValue")
0670                         val = integerValue(data, pos);
0671                     else if (type == u"textString")
0672                         val = textString(data, pos);
0673                     else if (type == u"encodedStringValue")
0674                         val = encodedStringValue(data, pos);
0675                     else if (type == u"fromValue")
0676                         val = fromValue(data, pos);
0677                     else
0678                         val = data.at(++pos);
0679 
0680                     // store relevant values
0681                     QStringView name = PARAM_FIELDS[i].name;
0682                     if (name == u"name")
0683                         message.partNames.append(val.toString());
0684                     // else qDebug() << param_fields[i].name << ":" << val.toString();
0685 
0686                     i = len;
0687                 }
0688             }
0689         }
0690     }
0691 
0692     return type;
0693 }
0694 
0695 QString Mms::messageTypeValue(const QByteArray &data, int &pos)
0696 {
0697     return lookupValString(data, pos, MESSAGE_TYPES);
0698 }
0699 
0700 unsigned int Mms::unsignedInt(const QByteArray &data, int &pos)
0701 {
0702     unsigned int unint = 0;
0703     unsigned int octet;
0704     char cont = 1;
0705 
0706     while (cont != 0) {
0707         unint = unint << 7;
0708         octet = data.at(++pos);
0709         unint += (octet & 0x7F);
0710         cont = (octet & 0x80);
0711     }
0712 
0713     return unint;
0714 }
0715 
0716 quint64 Mms::longInteger(const QByteArray &data, int &pos)
0717 {
0718     int octetcount = data.at(++pos);
0719 
0720     if (octetcount > 30) {
0721         return 0;
0722     }
0723 
0724     quint64 value = 0;
0725     for (int i = 0; i < octetcount; i++) {
0726         value = value << 8;
0727         value += (unsigned char)data.at(++pos);
0728     }
0729 
0730     return value;
0731 }
0732 
0733 int Mms::shortInteger(const QByteArray &data, int &pos)
0734 {
0735     return data.at(++pos) & 0x7F;
0736 }
0737 
0738 int Mms::integerValue(const QByteArray &data, int &pos)
0739 {
0740     if (data.at(pos + 1) < 31) {
0741         return longInteger(data, pos);
0742     } else if ((unsigned char)data.at(pos + 1) > 127) {
0743         return shortInteger(data, pos);
0744     } else {
0745         pos++;
0746         qDebug() << "parse int error";
0747         return 0;
0748     }
0749 }
0750 
0751 int Mms::valueLength(const QByteArray &data, int &pos)
0752 {
0753     if (data.at(pos + 1) < 31) {
0754         return data.at(++pos);
0755     } else if (data.at(pos + 1) == 31) {
0756         // length is an Uint
0757         pos++;
0758         return unsignedInt(data, pos);
0759     } else {
0760         qDebug() << "parse error";
0761         return 0;
0762     }
0763 }
0764 
0765 QString Mms::textString(const QByteArray &data, int &pos, int len)
0766 {
0767     int length = 0, start = pos + 1;
0768     int size = len >= 0 ? len + pos + 0 : data.length();
0769     while ((pos + 1) < size && data.at(++pos))
0770         ++length;
0771 
0772     QByteArray text = data.mid(start, length);
0773     return QString::fromUtf8(text);
0774 }
0775 
0776 QString Mms::encodedStringValue(const QByteArray &data, int &pos)
0777 {
0778     if (data.at(pos + 1) <= 31) {
0779         int len = valueLength(data, pos);
0780         pos++;
0781         return textString(data, pos, len);
0782     } else {
0783         return textString(data, pos);
0784     }
0785 }
0786 
0787 QString Mms::fromValue(const QByteArray &data, int &pos)
0788 {
0789     int len = valueLength(data, pos);
0790 
0791     if ((unsigned char)data.at(pos + 1) == MMS_CODE_ADDRESS_PRESENT_TOKEN) {
0792         pos++;
0793         return encodedStringValue(data, pos);
0794     } else if ((unsigned char)data.at(pos + 1) == MMS_CODE_INSERT_ADDRESS_TOKEN) {
0795         pos++;
0796         return QString();
0797     } else {
0798         qDebug() << "No from token found";
0799         pos += (len == 0 ? 1 : (len & 0x7F));
0800         return QString();
0801     }
0802 }
0803 
0804 QDateTime Mms::dateValue(const QByteArray &data, int &pos)
0805 {
0806     int len = data.at(++pos);
0807     quint64 value = 0;
0808 
0809     if ((unsigned char)data.at(pos + 1) == MMS_CODE_RELATIVE_TOKEN) {
0810         // relative token
0811         pos++;
0812         value = integerValue(data, pos);
0813         value = QDateTime::currentSecsSinceEpoch() + value;
0814     } else {
0815         int end = pos + len;
0816         // absolute token
0817         if ((unsigned char)data.at(pos + 1) == MMS_CODE_ABSOLUTE_TOKEN) {
0818             pos++;
0819         }
0820         while (pos < end) {
0821             value = value << 8;
0822             value += data.at(++pos);
0823         }
0824     }
0825 
0826     return QDateTime::fromSecsSinceEpoch(value);
0827 }
0828 
0829 QString Mms::messageClassValue(const QByteArray &data, int &pos)
0830 {
0831     unsigned char value = data.at(++pos);
0832     if (value > 127 && value < 132) {
0833         // 128=personal (default), 129=advertisement, 130=informational, 131=auto
0834         return QString::number(value);
0835     } else {
0836         qDebug() << "Token-text";
0837         QVector<unsigned char> separators = {11, 32, 40, 41, 44, 47, 58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 123, 125};
0838 
0839         QByteArray token;
0840         while (value > 31 && !separators.contains(value)) {
0841             token.append(value);
0842             value = data.at(++pos);
0843         }
0844 
0845         return QString::fromUtf8(token);
0846     }
0847 }
0848 
0849 QString Mms::mmsVersion(const QByteArray &data, int &pos)
0850 {
0851     QString major = QString::number((data.at(pos + 1) & 0x70) >> 4);
0852     QString minor = QString::number(data.at(++pos) & 0x0F);
0853     return major + SL(".") + minor;
0854 }
0855 
0856 QString Mms::lookupValString(const QByteArray &data, int &pos, std::span<const QStringView> list)
0857 {
0858     return list[shortInteger(data, pos)].toString();
0859 }
0860 
0861 QByteArray Mms::encodeUnsignedInt(unsigned int value)
0862 {
0863     QByteArray bytes;
0864 
0865     bytes.append(value & 0x7F);
0866     value = value >> 7;
0867     while (value > 0) {
0868         bytes.prepend(0x80 | (value & 0x7F));
0869         value = value >> 7;
0870     }
0871 
0872     return bytes;
0873 }
0874 
0875 QByteArray Mms::encodeLongInteger(quint64 value)
0876 {
0877     QByteArray bytes;
0878     while (value > 0) {
0879         bytes.prepend(value);
0880         value = value >> 8;
0881     }
0882     bytes.prepend(bytes.length());
0883 
0884     return bytes;
0885 }
0886 
0887 QByteArray Mms::encodeTextValue(const QString &value)
0888 {
0889     QByteArray bytes;
0890     bytes.append(value.toUtf8());
0891     bytes.append(MMS_CODE_END_STRING);
0892 
0893     return bytes;
0894 }
0895 
0896 QByteArray Mms::encodeEncodedStringValue(const QString &value)
0897 {
0898     QByteArray bytes;
0899     bytes.append(value.length() + 2);
0900     bytes.append(MMS_HEADER_CONTENT_LOCATION);
0901     bytes.append(value.toUtf8());
0902     bytes.append(MMS_CODE_END_STRING);
0903 
0904     return bytes;
0905 }
0906 
0907 QByteArray Mms::encodeFromValue(const QString &value)
0908 {
0909     QByteArray bytes;
0910     if (value.length() == 0) {
0911         bytes.append(1);
0912         bytes.append(MMS_CODE_INSERT_ADDRESS_TOKEN);
0913     } else {
0914         QByteArray encodedAddress = encodeEncodedStringValue(value);
0915         bytes.append(encodedAddress.length() + 1);
0916         bytes.append(MMS_CODE_ADDRESS_PRESENT_TOKEN);
0917         bytes.append(encodedAddress);
0918     }
0919 
0920     return bytes;
0921 }
0922 
0923 QByteArray Mms::encodeValueFromList(const QString &value, std::span<const QStringView> list)
0924 {
0925     QByteArray type;
0926     int index = list.size();
0927     while (index > 0) {
0928         index--;
0929         if (list[index] == value || index == 0) {
0930             type.append(index + 128);
0931             break;
0932         }
0933     }
0934 
0935     return type;
0936 }
0937 
0938 QByteArray Mms::encodePart(const QByteArray &type, const QString &name, const QByteArray &body, const QByteArray &params)
0939 {
0940     QByteArray header;
0941     header.append(type);
0942     header.append(MMS_PARAM_NAME);
0943     header.append(encodeTextValue(name));
0944     header.prepend(encodeUnsignedInt(header.length()));
0945     header.append(params);
0946 
0947     QByteArray data;
0948     data.append(encodeUnsignedInt(header.length()));
0949     data.append(encodeUnsignedInt(body.length()));
0950     data.append(header);
0951     data.append(body);
0952 
0953     return data;
0954 }