// For license of this file, see /LICENSE.md. #include "services/gmail/gmailnetworkfactory.h" #include "3rd-party/boolinq/boolinq.h" #include "database/databasequeries.h" #include "definitions/definitions.h" #include "exceptions/applicationexception.h" #include "exceptions/networkexception.h" #include "miscellaneous/application.h" #include "miscellaneous/settings.h" #include "miscellaneous/textfactory.h" #include "network-web/networkfactory.h" #include "network-web/oauth2service.h" #include "services/abstract/labelsnode.h" #include "services/gmail/definitions.h" #include "services/gmail/gmailserviceroot.h" #include #include #include #include #include GmailNetworkFactory::GmailNetworkFactory(QObject* parent) : QObject(parent), m_service(nullptr), m_username(QString()), m_batchSize(GMAIL_DEFAULT_BATCH_SIZE), m_downloadOnlyUnreadMessages(false), m_oauth2(new OAuth2Service(QSL(GMAIL_OAUTH_AUTH_URL), QSL(GMAIL_OAUTH_TOKEN_URL), {}, {}, QSL(GMAIL_OAUTH_SCOPE), this)) { initializeOauth(); } void GmailNetworkFactory::setService(GmailServiceRoot* service) { m_service = service; } OAuth2Service* GmailNetworkFactory::oauth() const { return m_oauth2; } QString GmailNetworkFactory::username() const { return m_username; } int GmailNetworkFactory::batchSize() const { return m_batchSize; } void GmailNetworkFactory::setBatchSize(int batch_size) { m_batchSize = batch_size; } QString GmailNetworkFactory::sendEmail(Mimesis::Message msg, const QNetworkProxy& custom_proxy, Message* reply_to_message) { QString bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { // throw ApplicationException(tr("you aren't logged in")); } if (reply_to_message != nullptr) { // We need to obtain some extra information. auto metadata = getMessageMetadata(reply_to_message->m_customId, {QSL("References"), QSL("Message-ID")}, custom_proxy); if (metadata.contains(QSL("Message-ID"))) { msg["References"] = metadata.value(QSL("Message-ID")).toStdString(); msg["In-Reply-To"] = metadata.value(QSL("Message-ID")).toStdString(); } } QString rfc_email = QString::fromStdString(msg.to_string()); QByteArray input_data = rfc_email.toUtf8(); QList> headers; headers.append(QPair(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), m_oauth2->bearer().toLocal8Bit())); headers.append(QPair(QSL(HTTP_HEADERS_CONTENT_TYPE).toLocal8Bit(), QSL("message/rfc822").toLocal8Bit())); QByteArray out; auto result = NetworkFactory::performNetworkOperation(QSL(GMAIL_API_SEND_MESSAGE), DOWNLOAD_TIMEOUT, input_data, out, QNetworkAccessManager::Operation::PostOperation, headers, false, {}, {}, custom_proxy); if (result.m_networkError != QNetworkReply::NetworkError::NoError) { if (!out.isEmpty()) { QJsonDocument doc = QJsonDocument::fromJson(out); auto json_message = doc.object()[QSL("error")].toObject()[QSL("message")].toString(); throw ApplicationException(json_message); } else { throw ApplicationException(QString::fromUtf8(out)); } } else { QJsonDocument doc = QJsonDocument::fromJson(out); auto msg_id = doc.object()[QSL("id")].toString(); return msg_id; } } void GmailNetworkFactory::initializeOauth() { #if defined(GMAIL_OFFICIAL_SUPPORT) m_oauth2->setClientSecretId(TextFactory::decrypt(QSL(GMAIL_CLIENT_ID), OAUTH_DECRYPTION_KEY)); m_oauth2->setClientSecretSecret(TextFactory::decrypt(QSL(GMAIL_CLIENT_SECRET), OAUTH_DECRYPTION_KEY)); #endif m_oauth2->setRedirectUrl(QSL(OAUTH_REDIRECT_URI) + QL1C(':') + QString::number(GMAIL_OAUTH_REDIRECT_URI_PORT), true); connect(m_oauth2, &OAuth2Service::tokensRetrieveError, this, &GmailNetworkFactory::onTokensError); connect(m_oauth2, &OAuth2Service::authFailed, this, &GmailNetworkFactory::onAuthFailed); connect(m_oauth2, &OAuth2Service::tokensRetrieved, this, [this](QString access_token, QString refresh_token, int expires_in) { Q_UNUSED(expires_in) Q_UNUSED(access_token) if (m_service != nullptr && !refresh_token.isEmpty()) { QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className()); DatabaseQueries::storeNewOauthTokens(database, refresh_token, m_service->accountId()); } }); } bool GmailNetworkFactory::downloadOnlyUnreadMessages() const { return m_downloadOnlyUnreadMessages; } void GmailNetworkFactory::setDownloadOnlyUnreadMessages(bool download_only_unread_messages) { m_downloadOnlyUnreadMessages = download_only_unread_messages; } QList GmailNetworkFactory::labels(bool only_user_labels, const QNetworkProxy& custom_proxy) { QString bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { throw NetworkException(QNetworkReply::NetworkError::AuthenticationRequiredError); } QList lbls; QList> headers; headers.append(QPair(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), m_oauth2->bearer().toLocal8Bit())); headers.append(QPair(QSL(HTTP_HEADERS_CONTENT_TYPE).toLocal8Bit(), QSL(GMAIL_CONTENT_TYPE_JSON).toLocal8Bit())); int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); QByteArray output; NetworkResult result = NetworkFactory::performNetworkOperation(QSL(GMAIL_API_LABELS_LIST), timeout, {}, output, QNetworkAccessManager::Operation::GetOperation, headers, false, {}, {}, custom_proxy); if (result.m_networkError != QNetworkReply::NetworkError::NoError) { throw NetworkException(result.m_networkError, tr("failed to download list of labels")); } QJsonObject obj = QJsonDocument::fromJson(output).object(); QJsonArray lbls_arr = obj[QSL("labels")].toArray(); for (const QJsonValue& lbl_val : lbls_arr) { QJsonObject lbl_obj = lbl_val.toObject(); if (only_user_labels && lbl_obj[QSL("type")].toString() != QSL(GMAIL_LABEL_TYPE_USER)) { continue; } Label* lbl = new Label(lbl_obj[QSL("name")].toString(), TextFactory::generateColorFromText(lbl_obj[QSL("name")].toString())); lbl->setCustomId(lbl_obj[QSL("id")].toString()); lbls.append(lbl); } return lbls; } QNetworkRequest GmailNetworkFactory::requestForAttachment(const QString& email_id, const QString& attachment_id) { QString target_url = QSL(GMAIL_API_GET_ATTACHMENT).arg(email_id, attachment_id); QNetworkRequest req(target_url); QByteArray bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { throw NetworkException(QNetworkReply::NetworkError::AuthenticationRequiredError); } req.setRawHeader(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer); return req; } void GmailNetworkFactory::setOauth(OAuth2Service* oauth) { m_oauth2 = oauth; } void GmailNetworkFactory::setUsername(const QString& username) { m_username = username; } QList GmailNetworkFactory::messages(const QString& stream_id, const QHash& stated_messages, Feed::Status& error, const QNetworkProxy& custom_proxy) { QString bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { error = Feed::Status::AuthError; return {}; } const bool is_spam_feed = QString::compare(stream_id, QSL(GMAIL_SYSTEM_LABEL_SPAM), Qt::CaseSensitivity::CaseInsensitive) == 0; // 1. Get unread IDs for a feed. // 2. Get read IDs for a feed. // 3. Get starred IDs for a feed. // 4. Download messages/contents for missing or changed IDs. QStringList remote_read_ids_list, remote_unread_ids_list, remote_starred_ids_list; try { remote_starred_ids_list = list(stream_id, {}, 0, is_spam_feed, QSL("is:starred"), custom_proxy); remote_unread_ids_list = list(stream_id, {}, batchSize(), is_spam_feed, QSL("is:unread"), custom_proxy); if (!downloadOnlyUnreadMessages()) { remote_read_ids_list = list(stream_id, {}, batchSize(), is_spam_feed, QSL("is:read"), custom_proxy); } } catch (const NetworkException& net_ex) { qCriticalNN << LOGSEC_GMAIL << "Failed to get list of e-mail IDs:" << QUOTE_W_SPACE_DOT(net_ex.message()); return {}; } // 1. auto local_unread_ids_list = stated_messages.value(ServiceRoot::BagOfMessages::Unread); QSet remote_unread_ids = FROM_LIST_TO_SET(QSet, remote_unread_ids_list); QSet local_unread_ids = FROM_LIST_TO_SET(QSet, local_unread_ids_list); // 2. auto local_read_ids_list = stated_messages.value(ServiceRoot::BagOfMessages::Read); QSet remote_read_ids = FROM_LIST_TO_SET(QSet, remote_read_ids_list); QSet local_read_ids = FROM_LIST_TO_SET(QSet, local_read_ids_list); // 3. auto local_starred_ids_list = stated_messages.value(ServiceRoot::BagOfMessages::Starred); QSet remote_starred_ids = FROM_LIST_TO_SET(QSet, remote_starred_ids_list); QSet local_starred_ids = FROM_LIST_TO_SET(QSet, local_starred_ids_list); // 4. QSet to_download; // Undownloaded unread e-mails. to_download += remote_unread_ids - local_unread_ids; // Undownloaded read e-mails. if (!m_downloadOnlyUnreadMessages) { to_download += remote_read_ids - local_read_ids; } // Undownloaded starred e-mails. to_download += remote_starred_ids - local_starred_ids; // Read e-mails newly marked as unread in service. auto moved_read = local_read_ids.intersect(remote_unread_ids); to_download += moved_read; // Unread e-mails newly marked as read in service. if (!m_downloadOnlyUnreadMessages) { auto moved_unread = local_unread_ids.intersect(remote_read_ids); to_download += moved_unread; } qDebugNN << LOGSEC_GMAIL << "Will download" << QUOTE_W_SPACE(to_download.size()) << "e-mails."; auto messages = obtainAndDecodeFullMessages(QList(to_download.values()), stream_id, custom_proxy); error = Feed::Status::Normal; return messages; } QNetworkReply::NetworkError GmailNetworkFactory::batchModify(const QString& label, const QStringList& custom_ids, bool assign, const QNetworkProxy& custom_proxy) { QString bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { return QNetworkReply::NetworkError::AuthenticationRequiredError; } QList> headers; headers.append(QPair(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), m_oauth2->bearer().toLocal8Bit())); headers.append(QPair(QSL(HTTP_HEADERS_CONTENT_TYPE).toLocal8Bit(), QSL(GMAIL_CONTENT_TYPE_JSON).toLocal8Bit())); int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); QJsonObject param_obj; QJsonArray param_add, param_remove; if (assign) { param_add.append(label); } else { param_remove.append(label); } param_obj[QSL("addLabelIds")] = param_add; param_obj[QSL("removeLabelIds")] = param_remove; // We need to operate withing allowed batches. for (int i = 0; i < custom_ids.size(); i += GMAIL_MAX_BATCH_SIZE) { auto batch = custom_ids.mid(i, GMAIL_MAX_BATCH_SIZE); param_obj[QSL("ids")] = QJsonArray::fromStringList(batch); QJsonDocument param_doc(param_obj); QByteArray output; auto result = NetworkFactory::performNetworkOperation(QSL(GMAIL_API_BATCH_UPD_LABELS), timeout, param_doc.toJson(QJsonDocument::JsonFormat::Compact), output, QNetworkAccessManager::Operation::PostOperation, headers, false, {}, {}, custom_proxy) .m_networkError; if (result != QNetworkReply::NetworkError::NoError) { return result; } } return QNetworkReply::NetworkError::NoError; } QNetworkReply::NetworkError GmailNetworkFactory::markMessagesRead(RootItem::ReadStatus status, const QStringList& custom_ids, const QNetworkProxy& custom_proxy) { return batchModify(QSL(GMAIL_SYSTEM_LABEL_UNREAD), custom_ids, status != RootItem::ReadStatus::Read, custom_proxy); } QNetworkReply::NetworkError GmailNetworkFactory::markMessagesStarred(RootItem::Importance importance, const QStringList& custom_ids, const QNetworkProxy& custom_proxy) { return batchModify(QSL(GMAIL_SYSTEM_LABEL_STARRED), custom_ids, importance == RootItem::Importance::Important, custom_proxy); } QStringList GmailNetworkFactory::list(const QString& stream_id, const QStringList& label_ids, int max_results, bool include_spam, const QString& query, const QNetworkProxy& custom_proxy) { QList message_ids; QString next_page_token; QString bearer = m_oauth2->bearer().toLocal8Bit(); int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); do { QString target_url = QSL(GMAIL_API_MSGS_LIST); target_url += QSL("?labelIds=%1").arg(stream_id); if (!label_ids.isEmpty()) { for (const QString& label_id : label_ids) { target_url += QSL("&labelIds=%1").arg(label_id); } } if (!query.isEmpty()) { target_url += QSL("&q=%1").arg(query); } if (include_spam) { target_url += QSL("&includeSpamTrash=true"); } int remaining = max_results - message_ids.size(); if (max_results <= 0 || remaining > 500) { target_url += QSL("&maxResults=500"); } else { target_url += QSL("&maxResults=%1").arg(remaining); } if (!next_page_token.isEmpty()) { target_url += QSL("&pageToken=%1").arg(next_page_token); } QByteArray messages_raw_data; auto netw = NetworkFactory::performNetworkOperation(target_url, timeout, {}, messages_raw_data, QNetworkAccessManager::Operation::GetOperation, {{QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit()}}, false, {}, {}, custom_proxy); if (netw.m_networkError == QNetworkReply::NetworkError::NoError) { // We parse this chunk. QString messages_data = QString::fromUtf8(messages_raw_data); message_ids << decodeLiteMessages(messages_data, next_page_token); } else { throw NetworkException(netw.m_networkError, tr("failed to download IDs of e-mail messages")); } } while (!next_page_token.isEmpty() && (max_results <= 0 || message_ids.size() < max_results)); return message_ids; } QVariantHash GmailNetworkFactory::getProfile(const QNetworkProxy& custom_proxy) { QString bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { throw ApplicationException(tr("you are not logged in")); } QList> headers; headers.append(QPair(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), m_oauth2->bearer().toLocal8Bit())); int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); QByteArray output; auto result = NetworkFactory::performNetworkOperation(QSL(GMAIL_API_GET_PROFILE), timeout, {}, output, QNetworkAccessManager::Operation::GetOperation, headers, false, {}, {}, custom_proxy) .m_networkError; if (result != QNetworkReply::NetworkError::NoError) { throw NetworkException(result, output); } else { QJsonDocument doc = QJsonDocument::fromJson(output); return doc.object().toVariantHash(); } } void GmailNetworkFactory::onTokensError(const QString& error, const QString& error_description) { Q_UNUSED(error) qApp->showGuiMessage(Notification::Event::LoginFailure, {tr("Gmail: authentication error"), tr("Click this to login again. Error is: '%1'").arg(error_description), QSystemTrayIcon::MessageIcon::Critical}, {}, {tr("Login"), [this]() { m_oauth2->setAccessToken(QString()); m_oauth2->setRefreshToken(QString()); m_oauth2->login(); }}); } void GmailNetworkFactory::onAuthFailed() { qApp->showGuiMessage(Notification::Event::LoginFailure, {tr("Gmail: authorization denied"), tr("Click this to login again."), QSystemTrayIcon::MessageIcon::Critical}, {}, {tr("Login"), [this]() { m_oauth2->login(); }}); } bool GmailNetworkFactory::fillFullMessage(Message& msg, const QJsonObject& json, const QString& feed_id) const { // Assign correct main labels/states. auto labelids = json[QSL("labelIds")].toArray().toVariantList(); // Every message which is in INBOX, must be in INBOX, even if Gmail API returns more labels for the message. // I have to always decide which single label is most important one. if (labelids.contains(QSL(GMAIL_SYSTEM_LABEL_INBOX)) && feed_id != QSL(GMAIL_SYSTEM_LABEL_INBOX)) { // This message is in INBOX label too, but this updated feed is not INBOX, // we want to leave this message in INBOX and not duplicate it to other feed/label. return false; } if (labelids.contains(QSL(GMAIL_SYSTEM_LABEL_TRASH)) && feed_id != QSL(GMAIL_SYSTEM_LABEL_TRASH)) { // This message is in trash, but this updated feed is not recycle bin, we do not want // this message to appear anywhere. return false; } QHash headers; auto json_headers = json[QSL("payload")].toObject()[QSL("headers")].toArray(); for (const QJsonValue& header : qAsConst(json_headers)) { headers.insert(header.toObject()[QSL("name")].toString(), header.toObject()["value"].toString()); } msg.m_isRead = true; msg.m_rawContents = QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact); auto active_labels = m_service->labelsNode() != nullptr ? m_service->labelsNode()->labels() : QList(); auto active_labels_linq = boolinq::from(active_labels); for (const QVariant& label : qAsConst(labelids)) { QString lbl = label.toString(); if (lbl == QSL(GMAIL_SYSTEM_LABEL_UNREAD)) { msg.m_isRead = false; } else if (lbl == QSL(GMAIL_SYSTEM_LABEL_STARRED)) { msg.m_isImportant = true; } else { auto* active_lb = active_labels_linq.firstOrDefault([lbl](Label* lb) { return lb->customId() == lbl; }); if (active_lb != nullptr) { msg.m_assignedLabels.append(active_lb); } } } msg.m_author = sanitizeEmailAuthor(headers[QSL("From")]); msg.m_title = headers[QSL("Subject")]; // NOTE: Provide link to web-based GUI for the message. msg.m_url = QSL("https://mail.google.com/mail/u/0/#all/%1").arg(msg.m_customId); msg.m_createdFromFeed = true; msg.m_created = TextFactory::parseDateTime(headers[QSL("Date")]); if (!msg.m_created.isValid()) { msg.m_created = TextFactory::parseDateTime(headers[QSL("date")]); } if (msg.m_title.isEmpty()) { msg.m_title = tr("No subject"); } QString backup_contents; QList parts_to_process, parts; parts_to_process.append(json[QSL("payload")].toObject()); while (!parts_to_process.isEmpty()) { auto this_part = parts_to_process.takeFirst(); auto nested_parts = this_part[QSL("parts")].toArray(); for (const QJsonValue& prt : qAsConst(nested_parts)) { auto prt_obj = prt.toObject(); parts.append(prt_obj); parts_to_process.append(prt_obj); } } if (json[QSL("payload")].toObject().contains(QSL("body"))) { parts.prepend(json[QSL("payload")].toObject()); } for (const QJsonObject& part : qAsConst(parts)) { QJsonObject body = part[QSL("body")].toObject(); QString mime = part[QSL("mimeType")].toString(); QString filename = part[QSL("filename")].toString(); if (filename.isEmpty() && mime.startsWith(QSL("text/"))) { // We have textual data of e-mail. // We check if it is HTML. if (msg.m_contents.isEmpty()) { if (mime.contains(QL1S("text/html"))) { msg.m_contents = QByteArray::fromBase64(body[QSL("data")].toString().toUtf8(), QByteArray::Base64Option::Base64UrlEncoding); if (msg.m_contents.contains(QSL(""))) { int strt = msg.m_contents.indexOf(QSL("")); int end = msg.m_contents.indexOf(QSL("")); if (strt > 0 && end > strt) { msg.m_contents = msg.m_contents.mid(strt + 6, end - strt - 6); } } } else if (backup_contents.isEmpty()) { backup_contents = QByteArray::fromBase64(body[QSL("data")].toString().toUtf8(), QByteArray::Base64Option::Base64UrlEncoding); backup_contents = backup_contents.replace(QSL("\r\n"), QSL("\n")) .replace(QSL("\n"), QSL("\n")) .replace(QSL("\n"), QSL("
")); } } } else if (!filename.isEmpty()) { // We have attachment. msg.m_enclosures.append(Enclosure(filename + QSL(GMAIL_ATTACHMENT_SEP) + body[QSL("attachmentId")].toString(), filename + QSL(" (%1 KB)").arg(QString::number(body["size"].toInt() / 1000.0)))); } } if (msg.m_contents.isEmpty() && !backup_contents.isEmpty()) { msg.m_contents = backup_contents; } return true; } QMap GmailNetworkFactory::getMessageMetadata(const QString& msg_id, const QStringList& metadata, const QNetworkProxy& custom_proxy) { QString bearer = m_oauth2->bearer(); if (bearer.isEmpty()) { throw ApplicationException(tr("you are not logged in")); } QList> headers; QByteArray output; int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); headers.append(QPair(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit())); QString query = QString("%1/%2?format=metadata&metadataHeaders=%3") .arg(QSL(GMAIL_API_MSGS_LIST), msg_id, metadata.join(QSL("&metadataHeaders="))); NetworkResult res = NetworkFactory::performNetworkOperation(query, timeout, QByteArray(), output, QNetworkAccessManager::Operation::GetOperation, headers, false, {}, {}, custom_proxy); if (res.m_networkError == QNetworkReply::NetworkError::NoError) { QJsonDocument doc = QJsonDocument::fromJson(output); QMap result; auto json_headers = doc.object()[QSL("payload")].toObject()[QSL("headers")].toArray(); for (const auto& header : json_headers) { QJsonObject obj_header = header.toObject(); result.insert(obj_header[QSL("name")].toString(), obj_header[QSL("value")].toString()); } return result; } else { throw ApplicationException(tr("failed to get metadata")); } } QList GmailNetworkFactory::obtainAndDecodeFullMessages(const QStringList& message_ids, const QString& feed_id, const QNetworkProxy& custom_proxy) const { QHash msgs; int next_message = 0; QString bearer = m_oauth2->bearer(); if (bearer.isEmpty()) { return {}; } do { QHttpMultiPart multi; multi.setContentType(QHttpMultiPart::ContentType::MixedType); for (int window = next_message + 100; next_message < window && next_message < message_ids.size(); next_message++) { QString msg_id = message_ids[next_message]; Message msg; QHttpPart part; msg.m_feedId = feed_id; msg.m_customId = msg_id; part.setRawHeader(HTTP_HEADERS_CONTENT_TYPE, GMAIL_CONTENT_TYPE_HTTP); QString full_msg_endpoint = QSL("GET /gmail/v1/users/me/messages/%1\r\n").arg(msg_id); part.setBody(full_msg_endpoint.toUtf8()); multi.append(part); msgs.insert(msg_id, msg); } QList> headers; QList output; int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); headers.append(QPair(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit())); NetworkResult res = NetworkFactory::performNetworkOperation(GMAIL_API_BATCH, timeout, &multi, output, QNetworkAccessManager::Operation::PostOperation, headers, false, {}, {}, custom_proxy); if (res.m_networkError == QNetworkReply::NetworkError::NoError) { // We parse each part of HTTP response (it contains HTTP headers and payload with msg full data). for (const HttpResponse& part : qAsConst(output)) { QJsonObject msg_doc = QJsonDocument::fromJson(part.body().toUtf8()).object(); QString msg_id = msg_doc[QSL("id")].toString(); if (msgs.contains(msg_id)) { Message& msg = msgs[msg_id]; if (!fillFullMessage(msg, msg_doc, feed_id)) { qWarningNN << LOGSEC_GMAIL << "Failed to get (or deliberately skipped) full message for custom ID:" << QUOTE_W_SPACE_DOT(msg.m_customId); msgs.remove(msg_id); } } } } else { return {}; } } while (next_message < message_ids.size()); return msgs.values(); } QStringList GmailNetworkFactory::decodeLiteMessages(const QString& messages_json_data, QString& next_page_token) const { QList message_ids; QJsonObject top_object = QJsonDocument::fromJson(messages_json_data.toUtf8()).object(); QJsonArray json_msgs = top_object[QSL("messages")].toArray(); next_page_token = top_object[QSL("nextPageToken")].toString(); message_ids.reserve(json_msgs.count()); for (const QJsonValue& obj : json_msgs) { auto message_obj = obj.toObject(); message_ids << message_obj[QSL("id")].toString(); } return message_ids; } QString GmailNetworkFactory::sanitizeEmailAuthor(const QString& author) const { return author.mid(0, author.indexOf(QL1S(" <"))).replace(QL1S("\""), QString()); }