From 7c43ef6c8a99edd3a0f53518ec663f1d6269e5e9 Mon Sep 17 00:00:00 2001 From: Martin Rotter Date: Thu, 16 Jul 2020 11:17:29 +0200 Subject: [PATCH] Add mimesis library. --- resources/text/CHANGELOG | 1 - src/librssguard/3rd-party/mimesis/mimesis.cpp | 1271 +++++++++++++++++ src/librssguard/3rd-party/mimesis/mimesis.hpp | 181 +++ .../3rd-party/mimesis/quoted-printable.cpp | 61 + .../3rd-party/mimesis/quoted-printable.hpp | 25 + src/librssguard/gui/dialogs/formabout.ui | 4 +- src/librssguard/librssguard.pro | 4 + .../services/gmail/gui/formaddeditemail.cpp | 3 + .../gmail/network/gmailnetworkfactory.cpp | 4 +- 9 files changed, 1549 insertions(+), 5 deletions(-) create mode 100755 src/librssguard/3rd-party/mimesis/mimesis.cpp create mode 100755 src/librssguard/3rd-party/mimesis/mimesis.hpp create mode 100755 src/librssguard/3rd-party/mimesis/quoted-printable.cpp create mode 100755 src/librssguard/3rd-party/mimesis/quoted-printable.hpp diff --git a/resources/text/CHANGELOG b/resources/text/CHANGELOG index a1fd234fb..e86797d12 100644 --- a/resources/text/CHANGELOG +++ b/resources/text/CHANGELOG @@ -32,7 +32,6 @@ Fixed/changed: ▪ Purging of messages wasn't purgin important messages if chosen, now fixed. ▪ Fixed errors in internal SQL code when displaying empty message list. - 3.6.3 ————— diff --git a/src/librssguard/3rd-party/mimesis/mimesis.cpp b/src/librssguard/3rd-party/mimesis/mimesis.cpp new file mode 100755 index 000000000..7f812c0f0 --- /dev/null +++ b/src/librssguard/3rd-party/mimesis/mimesis.cpp @@ -0,0 +1,1271 @@ +// For license of this file, see /LICENSE.md. + +/* Mimesis -- a library for parsing and creating RFC2822 messages + Copyright © 2017 Guus Sliepen + + Mimesis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . + */ + +#include "mimesis.hpp" + +#include "quoted-printable.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace std; + +namespace Mimesis { + static const string base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + static const char base64_inverse[256] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + string base64_encode(string_view in) { + string out; + size_t outlen = ((in.size() + 2) / 3) * 4; + + out.reserve(outlen); + + size_t i; + const uint8_t* uin = (const uint8_t*)in.data(); + + for (i = 0; i < (in.size() / 3) * 3; i += 3) { + out.push_back(base64[ (uin[i + 0] >> 2)]); + out.push_back(base64[(uin[i + 0] << 4 & 63) | (uin[i + 1] >> 4)]); + out.push_back(base64[(uin[i + 1] << 2 & 63) | (uin[i + 2] >> 6)]); + out.push_back(base64[(uin[i + 2] << 0 & 63) ]); + } + + while (i++ < in.size()) + out.push_back('='); + + return out; + } + + string base64_decode(string_view in) { + string out; + size_t estimate = (in.size() / 4) * 3; + + out.reserve(estimate); + + int i = 0; + uint32_t triplet = 0; + + for(uint8_t c: in) { + auto d = base64_inverse[c]; + + if (d == -1) { + if (c == '=') + break; + else + continue; + } + + triplet <<= 6; + triplet |= d; + + if((i & 3) == 3) { + out.push_back(static_cast(triplet >> 16)); + out.push_back(static_cast(triplet >> 8)); + out.push_back(static_cast(triplet)); + } + + i++; + } + + if((i & 3) == 3) { + out.push_back(static_cast(triplet >> 10)); + out.push_back(static_cast(triplet >> 2)); + } + else if((i & 3) == 2) { + out.push_back(static_cast(triplet >> 4)); + } + + return out; + } + + static std::random_device rnd; + + static string unquote(const string& str) { + if (str.empty() || str[0] != '"') + return str; + + string unquoted; + int quotes_wanted = 2; + + for (auto&& c: str) { + if (c == '"') { + if (--quotes_wanted) + continue; + + break; + } + + if (c == '\\') + continue; + + unquoted.push_back(c); + } + + return unquoted; + } + + static string quote(const string& str) { + bool do_quote = false; + + for (auto&& c: str) { + if (isalnum(c) || strchr("!#$%&'*+-/=?^_`{|}~", c)) + continue; + + do_quote = true; + break; + } + + if (!do_quote) + return str; + + string quoted = "\""; + + for (auto&& c: str) { + if (c == '\"' || c == '\\') + quoted.push_back('\\'); + + quoted.push_back(c); + } + + quoted.push_back('"'); + + return quoted; + } + + static bool streqi(const string& a, const string& b) { + if (a.size() != b.size()) + return false; + + for (size_t i = 0; i < a.size(); i++) + if (tolower(a[i]) != tolower(b[i])) + return false; + + return true; + } + + static bool streqi(const string& a, size_t offset_a, size_t len_a, const string& b) { + if (min(a.size() - offset_a, len_a) != b.size()) + return false; + + for (size_t i = 0; i < len_a; i++) + if (tolower(a[i + offset_a]) != tolower(b[i])) + return false; + + return true; + } + + static bool streqi(const string& a, size_t offset_a, size_t len_a, const string& b, size_t offset_b, size_t len_b) { + if (min(a.size() - offset_a, len_a) != min(b.size() - offset_b, len_b)) + return false; + + for (size_t i = 0; i < min(a.size() - offset_a, len_a); i++) + if (tolower(a[i + offset_a]) != tolower(b[i + offset_b])) + return false; + + return true; + } + + static string generate_boundary() { + unsigned int nonce[24 / sizeof(unsigned int)]; + + for (auto& val: nonce) + val = rnd(); + + return base64_encode(string_view(reinterpret_cast(nonce), sizeof nonce)); + } + + static bool is_boundary(const std::string& line, const std::string& boundary) { + if (boundary.empty()) + return false; + + if (line.compare(0, 2, "--")) + return false; + + if (line.compare(2, boundary.size(), boundary)) + return false; + + return true; + } + + static bool is_final_boundary(const std::string& line, const std::string& boundary) { + if (line.compare(2 + boundary.size(), 2, "--")) + return false; + + return is_boundary(line, boundary); + } + + static bool types_match(const std::string& a, const std::string& b) { + auto a_slash = a.find('/'); + auto b_slash = b.find('/'); + + if (a_slash == string::npos || b_slash == string::npos) + return streqi(a, 0, a_slash, b, 0, b_slash); + else + return streqi(a, b); + } + + static void set_value(string& str, const string& value) { + size_t semicolon = str.find(';'); + + if (semicolon == string::npos) + str = value; + else + str.replace(0, semicolon, value); + } + + static string get_value(const string& str) { + return str.substr(0, str.find(';')); + } + + static pair get_parameter_value_range(const string& str, const string& parameter) { + size_t start = 0; + size_t end = string::npos; + + // Find a semicolon, which marks the start of a parameter. + while((start = str.find(';', start)) != string::npos) { + start++; + while (isspace(str[start])) + start++; + + if (!streqi(str, start, parameter.size(), parameter)) { + // It's not the wanted parameter. + start = str.find('=', start); + while (isspace(str[start])) + start++; + + if (str[start] != '=') + continue; + + while (isspace(str[start])) + start++; + + // If it's a quoted parameter, skip over the quoted text. + if (str[start] == '"') { + start++; + while (start < str.size() && str[start] != '"') { + if (str[start] == '\\' && str.size() > start - 1) + start++; + + start++; + } + } + + continue; + } + + // Skip until we get to the value. + start += parameter.size(); + while (isspace(str[start])) + start++; + + if (str[start] != '=') + continue; + + start++; + while (isspace(str[start])) + start++; + + end = start; + if (str[end] == '"') { + // It's a quoted parameter. + end++; + while (end < str.size() && str[end] != '"') { + if (str[end] == '\\' && str.size() > end - 1) + end++; + + end++; + } + } + else { + while (end < str.size() && str[end] != ';' && !isspace(str[end])) + end++; + } + + break; + } + + return make_pair(start, end); + } + + static void set_parameter(string& str, const string& parameter, const string& value) { + auto range = get_parameter_value_range(str, parameter); + auto start = range.first; + auto end = range.second; + + if (start == string::npos) + str += "; " + parameter + "=" + quote(value); + else + str.replace(start, end - start, quote(value)); + } + + static string get_parameter(const string& str, const string& parameter) { + auto range = get_parameter_value_range(str, parameter); + auto start = range.first; + auto end = range.second; + + if (start == string::npos) + return {}; + + return unquote(str.substr(start, end - start)); + } + + static const string ending[2] = {"\n", "\r\n"}; + + Part::Part() : + headers(), + preamble(), + body(), + epilogue(), + parts(), + boundary(), + multipart(false), + crlf(true), + message(false) + {} + +// Loading and saving a whole MIME message + + string Part::load(istream& in, const string& parent_boundary) { + string line; + int ncrlf = 0; + int nlf = 0; + + while (getline(in, line)) { + if (is_boundary(line, parent_boundary)) + return line; + + if (line.size() && line.back() == '\r') { + ncrlf++; + line.erase(line.size() - 1); + } + else { + nlf++; + } + + if (line.empty()) + break; + + if (isspace(line[0])) { + if (headers.empty()) + throw runtime_error("invalid header line"); + + headers.back().second.append(line); + continue; + } + + size_t colon = string::npos; + + for (size_t i = 0; i < line.size(); ++i) { + if (line[i] == ':') { + colon = i; + break; + } + + if (line[i] < 33 || static_cast(line[i]) > 127) { + if (i == 4 && line[i] == ' ' && line.compare(0, 4, "From") == 0 && headers.empty()) { + colon = i; + break; + } + + throw runtime_error("invalid header line " + line + std::to_string(i)); + } + } + + if (colon == 0 || colon == string::npos) + throw runtime_error("invalid header line"); + + if (line[colon] != ':') + continue; + + auto start = colon + 1; + + while (start < line.size() && isspace(line[start])) + start++; + + // Empty header values are allowed for most fields. + + auto field = line.substr(0, colon); + auto value = line.substr(start); + + headers.emplace_back(field, value); + } + + crlf = ncrlf > nlf; + + const string content_type = get_header("Content-Type"); + + if (types_match(get_value(content_type), "multipart")) { + boundary = get_parameter(content_type, "boundary"); + if (boundary.empty()) + throw runtime_error("multipart but no boundary specified"); + + multipart = true; + } + else { + multipart = false; + } + + if (!multipart) { + while (getline(in, line)) { + if (is_boundary(line, parent_boundary)) + return line; + + line.push_back('\n'); + body.append(line); + } + } + else { + while (getline(in, line)) { + if (is_boundary(line, parent_boundary)) + return line; + + if (is_boundary(line, boundary)) + break; + + line.push_back('\n'); + preamble.append(line); + } + + while (true) { + parts.emplace_back(); + string last_line = parts.back().load(in, boundary); + + if (!is_boundary(last_line, boundary)) + throw runtime_error("invalid boundary"); + + if (is_final_boundary(last_line, boundary)) + break; + } + + while (getline(in, line)) { + if (is_boundary(line, parent_boundary)) + return line; + + line.push_back('\n'); + epilogue.append(line); + } + } + + if (in.bad()) + throw runtime_error("error reading message"); + + return {}; + } + + void Part::save(ostream& out) const { + bool has_headers = false; + + for (auto& header: headers) { + if (!header.second.empty()) { + out << header.first << ": " << header.second << ending[crlf]; + has_headers = true; + } + } + + if (message && !has_headers) + throw runtime_error("no headers specified"); + + out << ending[crlf]; + + if (parts.empty()) { + out << body; + } + else { + out << preamble; + for (auto& part: parts) { + out << "--" << boundary << ending[crlf]; + part.save(out); + } + + out << "--" << boundary << "--" << ending[crlf]; + out << epilogue; + } + } + + void Part::load(const string& filename) { + ifstream in(filename); + + if (!in.is_open()) + throw runtime_error("could not open message file"); + + load(in); + } + + void Part::save(const string& filename) const { + ofstream out(filename); + + if (!out.is_open()) + throw runtime_error("could not open message file"); + + save(out); + out.close(); + if (out.fail()) + throw runtime_error("could not write message file"); + } + + void Part::from_string(const string& data) { + istringstream in(data); + + load(in); + } + + string Part::to_string() const { + ostringstream out; + + save(out); + return out.str(); + } + + void Part::set_crlf(bool value) { + crlf = value; + } + +// Low-level access + + string charset_decode(const string& charset, string_view in) { + QTextCodec* custom_codec = QTextCodec::codecForName(charset.c_str()); + auto unic = custom_codec->toUnicode(string(in).c_str()); + + std::string utf8_text = unic.toUtf8().constData(); + + return utf8_text; + } + + string Part::get_body() const { + string result; + auto encoding = get_header_value("Content-Transfer-Encoding"); + + if (streqi(encoding, "quoted-printable")) + result = quoted_printable_decode(body); + + if (streqi(encoding, "base64")) + result = base64_decode(body); + else + result = body; + + if (is_mime_type("text")) { + auto charset = get_header_parameter("Content-Type", "charset"); + + if (!charset.empty() && !streqi(charset, "utf-8") && !streqi(charset, "us-ascii") && !streqi(charset, "ascii")) { + result = charset_decode(charset, result); + } + } + + return result; + } + + string Part::get_preamble() const { + return preamble; + } + + string Part::get_epilogue() const { + return epilogue; + } + + string Part::get_boundary() const { + return boundary; + } + + vector& Part::get_parts() { + return parts; + } + + const vector& Part::get_parts() const { + return parts; + } + + vector>& Part::get_headers() { + return headers; + } + + const vector>& Part::get_headers() const { + return headers; + } + + bool Part::is_multipart() const { + return multipart; + } + + bool Part::is_multipart(const std::string& subtype) const { + return multipart && get_header_value("Content-Type") == "multipart/" + subtype; + } + + bool Part::is_singlepart() const { + return !multipart; + } + + bool Part::is_singlepart(const std::string& type) const { + return !multipart && types_match(get_header_value("Content-Type"), type); + } + + bool Part::is_attachment() const { + return get_header_value("Content-Disposition") == "attachment"; + } + + bool Part::is_inline() const { + return get_header_value("Content-Disposition") == "inline"; + } + + void Part::set_body(const string& value) { + if (multipart) + throw runtime_error("Cannot set body of a multipart message"); + + body = value; + } + + void Part::set_preamble(const string& value) { + if (!multipart) + throw runtime_error("Cannot set preamble of a non-multipart message"); + + preamble = value; + } + + void Part::set_epilogue(const string& value) { + if (!multipart) + throw runtime_error("Cannot set epilogue of a non-multipart message"); + + epilogue = value; + } + + void Part::set_boundary(const std::string& value) { + boundary = value; + if (has_mime_type()) + set_header_parameter("Content-Type", "boundary", boundary); + } + + void Part::set_parts(const vector& value) { + if (!multipart) + throw runtime_error("Cannot set parts of a non-multipart message"); + + parts = value; + } + + void Part::set_headers(const vector>& value) { + headers = value; + } + + void Part::clear() { + headers.clear(); + preamble.clear(); + body.clear(); + epilogue.clear(); + parts.clear(); + boundary.clear(); + multipart = false; + } + + void Part::clear_body() { + body.clear(); + } + +// Header manipulation + + static bool iequals(const string& a, const string& b) { + if (a.size() != b.size()) + return false; + + for (size_t i = 0; i < a.size(); ++i) + if (tolower(a[i]) != tolower(b[i])) + return false; + + return true; + } + + string Part::get_header(const string& field) const { + for (const auto& header: headers) + if (iequals(header.first, field)) + return header.second; + + return {}; + } + + void Part::set_header(const string& field, const string& value) { + for (auto& header: headers) { + if (iequals(header.first, field)) { + header.second = value; + return; + } + } + + append_header(field, value); + } + + string& Part::operator[](const string& field) { + for (auto& header: headers) + if (iequals(header.first, field)) + return header.second; + + append_header(field, {}); + return headers.back().second; + } + + const string& Part::operator[](const string& field) const { + for (auto& header: headers) + if (iequals(header.first, field)) + return header.second; + + static string empty_string; + + return empty_string; + } + + void Part::append_header(const string& field, const string& value) { + headers.push_back(make_pair(field, value)); + } + + void Part::prepend_header(const string& field, const string& value) { + headers.insert(begin(headers), make_pair(field, value)); + } + + void Part::erase_header(const string& field) { + headers.erase(remove_if(begin(headers), end(headers), [&](pair& header) { + return header.first == field; + }), end(headers)); + } + + void Part::clear_headers() { + headers.clear(); + } + + string Part::get_header_value(const string& field) const { + return get_value(get_header(field)); + } + + string Part::get_header_parameter(const string& field, const string& parameter) const { + return get_parameter(get_header(field), parameter); + } + + void Part::set_header_value(const string& field, const string& value) { + for (auto& header: headers) { + if (iequals(header.first, field)) { + set_value(header.second, value); + return; + } + } + + append_header(field, value); + } + + void Part::set_header_parameter(const string& field, const string& parameter, const string& value) { + for (auto& header: headers) { + if (iequals(header.first, field)) { + set_parameter(header.second, parameter, value); + return; + } + } + + append_header(field, "; " + parameter + "=" + value); + } + + static string get_date_string(const chrono::system_clock::time_point& date = chrono::system_clock::now()) { + + QLocale loc("C"); + QDateTime dat; + + dat.fromTime_t(date.time_since_epoch().count()); + + return loc.toString(dat, "ddd, MM MMM yyyy HH:mm:ss t").toStdString(); + + /*time_t t = chrono::system_clock::to_time_t(date); + struct tm tm{}; + localtime_r(&t, &tm); + char str[128]; + char *oldlocale = setlocale(LC_TIME, "C"); + size_t result = strftime(str, sizeof str, "%a, %d %b %Y %T %z", &tm); + setlocale(LC_TIME, oldlocale); + if (result == 0) + throw runtime_error("Could not convert date to string"); + return str; + */ + } + + void Part::add_received(const string& text, const chrono::system_clock::time_point& date) { + prepend_header("Received", text + "; " + get_date_string(date)); + } + + void Part::generate_msgid(const string& domain) { + auto now = chrono::system_clock::now(); + uint64_t buf[3]; + + buf[0] = ((uint64_t)rnd() << 32) | rnd(); + buf[1] = chrono::duration_cast(now.time_since_epoch()).count(); + buf[2] = ((uint64_t)rnd() << 32) | rnd(); + string msgid = "<" + base64_encode(string_view(reinterpret_cast(buf), sizeof buf)) + "@" + domain + ">"; + + set_header("Message-ID", msgid); + } + + void Part::set_date(const chrono::system_clock::time_point& date) { + set_header("Date", get_date_string(date)); + } + +// Part manipulation + + Part& Part::append_part(const Part& part) { + parts.push_back(part); + return parts.back(); + } + + Part& Part::prepend_part(const Part& part) { + parts.insert(begin(parts), part); + return parts.front(); + } + + void Part::clear_parts() { + parts.clear(); + } + + void Part::make_multipart(const string& subtype, const string& suggested_boundary) { + if (multipart) { + if (is_multipart(subtype)) + return; + + Part part; + + part.preamble = move(preamble); + part.epilogue = move(epilogue); + part.parts = move(parts); + part.boundary = move(boundary); + part.multipart = true; + part.set_header("Content-Type", get_header("Content-Type")); + part.set_header("Content-Disposition", get_header("Content-Disposition")); + erase_header("Content-Disposition"); + part.crlf = crlf; + parts.emplace_back(move(part)); + } + else { + multipart = true; + + if (message) + set_header("MIME-Version", "1.0"); + + if (!body.empty()) { + auto& part = append_part(); + + part.set_header("Content-Type", get_header("Content-Type")); + part.set_header("Content-Disposition", get_header("Content-Disposition")); + erase_header("Content-Disposition"); + part.body = move(body); + } + } + + if (!suggested_boundary.empty()) + set_boundary(suggested_boundary); + + if (boundary.empty()) + boundary = generate_boundary(); + + set_header("Content-Type", "multipart/" + subtype + "; boundary=" + boundary); + } + + bool Part::flatten() { + if (!multipart) + return true; + + if (parts.empty()) { + multipart = false; + return true; + } + + if (parts.size() > 1) + return false; + + auto& part = parts.front(); + + set_header("Content-Type", part.get_header("Content-Type")); + set_header("Content-Disposition", part.get_header("Content-Disposition")); + + if (part.multipart) { + parts = move(part.parts); + } + else { + multipart = false; + set_body(part.get_body()); + parts.clear(); + } + + return true; + } + +// Body and attachments + + string Part::get_mime_type() const { + return get_header_value("Content-Type"); + } + + void Part::set_mime_type(const std::string& type) { + return set_header_value("Content-Type", type); + } + + bool Part::is_mime_type(const std::string& type) const { + return types_match(get_mime_type(), type); + } + + bool Part::has_mime_type() const { + return !get_mime_type().empty(); + } + + const Part* Part::get_first_matching_part(function predicate) const { + if (!multipart) { + if (headers.empty() && body.empty()) + return nullptr; + + if (is_attachment()) + return nullptr; + } + + if (predicate(*this)) + return this; + + for (auto& part: parts) { + auto result = part.get_first_matching_part(predicate); + + if (result) + return result; + } + + return nullptr; + } + + Part* Part::get_first_matching_part(function predicate) { + auto result = ((const Part*)this)->get_first_matching_part(predicate); + + return const_cast(result); + } + + const Part* Part::get_first_matching_part(const string& type) const { + return get_first_matching_part([type](const Part& part) { + auto my_type = part.get_mime_type(); + return types_match(my_type.empty() ? "text/plain" : my_type, type); + }); + } + + Part* Part::get_first_matching_part(const string& type) { + auto result = ((const Part*)this)->get_first_matching_part(type); + + return const_cast(result); + } + + string Part::get_first_matching_body(const string& type) const { + const auto& part = get_first_matching_part(type); + + if (part) + return part->get_body(); + else + return {}; + } + + Part& Part::set_alternative(const string& subtype, const string& text) { + string type = "text/" + subtype; + Part* part = nullptr; + + // Try to put it in the body first. + if (!multipart) { + if (body.empty() || is_mime_type(type)) { + part = this; + } + else if (is_mime_type("text") && !is_attachment()) { + make_multipart("alternative"); + part = &append_part(); + } + else { + make_multipart("mixed"); + part = &prepend_part(); + } + } + else { + // If there is already a text/plain part, use that one. + part = get_first_matching_part(type); + if (part) { + part->set_mime_type(type); + part->set_body(text); + return *part; + } + + // If there is already a multipart/alternative with text, use that one. + part = get_first_matching_part([](const Part& part) { + return part.is_multipart("alternative") + && !part.parts.empty() + && part.get_first_matching_part("text"); + }); + if (part) + part = &part->append_part(); + + // If there is already inline text, make it multipart/alternative. + + if (!part && (part = get_first_matching_part("text"))) { + part->make_multipart("alternative"); + part = &part->append_part(); + } + + // Otherwise, assume we're multipart/mixed. + if (!part) + part = &prepend_part(); + } + + part->set_header("Content-Type", type); + part->set_body(text); + + return *part; + } + + void Part::set_plain(const string& text) { + set_alternative("plain", text); + } + + void Part::set_html(const string& html) { + set_alternative("html", html); + } + + string Part::get_plain() const { + return get_first_matching_body("text/plain"); + } + + string Part::get_html() const { + return get_first_matching_body("text/html"); + } + + string Part::get_text() const { + return get_first_matching_body("text"); + } + + Part& Part::attach(const Part& attachment) { + if (!multipart && body.empty()) { + if (attachment.message) { + set_header("Content-Type", "message/rfc822"); + body = attachment.to_string(); + } + else { + set_header("Content-Type", attachment.get_header("Content-Type")); + body = attachment.body; + } + + set_header("Content-Disposition", "attachment"); + return *this; + } + + make_multipart("mixed"); + auto& part = append_part(); + + if (attachment.message) { + part.set_header("Content-Type", "message/rfc822"); + part.body = attachment.to_string(); + } + else { + part.set_header("Content-Type", attachment.get_header("Content-Type")); + part.body = attachment.body; + } + + part.set_header("Content-Disposition", "attachment"); + return part; + } + + Part& Part::attach(const string& data, const string& type, const string& filename) { + if (!multipart && body.empty()) { + set_header("Content-Type", type.empty() ? "text/plain" : type); + set_header("Content-Disposition", "attachment"); + if (!filename.empty()) + set_header_parameter("Content-Disposition", "filename", filename); + + body = data; + return *this; + } + + make_multipart("mixed"); + auto& part = append_part(); + + part.set_header("Content-Type", type.empty() ? "text/plain" : type); + part.set_header("Content-Disposition", "attachment"); + if (!filename.empty()) + part.set_header_parameter("Content-Disposition", "filename", filename); + + part.set_body(data); + return part; + } + + Part& Part::attach(istream& in, const string& type, const string& filename) { + auto& part = attach("", type, filename); + char buffer[4096]; + + while (in.read(buffer, sizeof(buffer))) + part.body.append(buffer, sizeof(buffer)); + + part.body.append(buffer, in.gcount()); + return part; + } + + vector Part::get_attachments() const { + vector attachments; + + if (!multipart && get_header_value("Content-Disposition") == "attachment") { + attachments.push_back(this); + return attachments; + } + + for (auto& part: parts) { + auto sub = part.get_attachments(); + + attachments.insert(end(attachments), begin(sub), end(sub)); + } + + return attachments; + } + + void Part::simplify() { + if (!multipart) + return; + + for (auto& part: parts) + part.simplify(); + + parts.erase(remove_if(begin(parts), end(parts), [&](Part& part) { + return part.headers.empty() && part.body.empty(); + }), end(parts)); + + if (parts.empty()) { + if (message) { + erase_header("Content-Type"); + erase_header("Content-Disposition"); + multipart = false; + } + else { + clear(); + } + } + else if (parts.size() == 1) { + flatten(); + } + } + + void Part::clear_attachments() { + if (!multipart) { + if (get_header_value("Content-Disposition") == "attachment") { + if (message) { + erase_header("Content-Type"); + erase_header("Content-Disposition"); + body.clear(); + } + else { + clear(); + } + } + } + else { + for (auto& part: parts) + part.clear_attachments(); + + simplify(); + } + } + + void Part::clear_alternative(const string& type) { + bool cleared = false; + Part* part; + + while((part = get_first_matching_part(type))) { + part->clear(); + cleared = true; + } + + if (cleared) + simplify(); + } + + void Part::clear_text() { + clear_alternative("text"); + } + + void Part::clear_plain() { + clear_alternative("text/plain"); + } + + void Part::clear_html() { + clear_alternative("text/html"); + } + + bool Part::has_text() const { + return get_first_matching_part("text"); + } + + bool Part::has_plain() const { + return get_first_matching_part("text/plain"); + } + + bool Part::has_html() const { + return get_first_matching_part("text/html"); + } + + bool Part::has_attachments() const { + if (is_attachment()) + return true; + + for (auto& part: parts) + if (part.has_attachments()) + return true; + + return false; + } + +// RFC2822 messages + + Message::Message() { + message = true; + } + +// Comparison + + bool operator==(const Part& lhs, const Part& rhs) { + return lhs.crlf == rhs.crlf && lhs.multipart == rhs.multipart && + lhs.preamble == rhs.preamble && lhs.body == rhs.body && + lhs.epilogue == rhs.epilogue && lhs.boundary == rhs.boundary && + lhs.headers == rhs.headers && lhs.parts == rhs.parts; + } + + bool operator!=(const Part& lhs, const Part& rhs) { + return !(lhs == rhs); + } + +} diff --git a/src/librssguard/3rd-party/mimesis/mimesis.hpp b/src/librssguard/3rd-party/mimesis/mimesis.hpp new file mode 100755 index 000000000..686b23090 --- /dev/null +++ b/src/librssguard/3rd-party/mimesis/mimesis.hpp @@ -0,0 +1,181 @@ +// For license of this file, see /LICENSE.md. + +#pragma once + +/* Mimesis -- a library for parsing and creating RFC2822 messages + Copyright © 2017 Guus Sliepen + + Mimesis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +namespace Mimesis { + + std::string base64_encode(std::string_view in); + std::string base64_decode(std::string_view in); + + class Part { + std::vector> headers; + std::string preamble; + std::string body; + std::string epilogue; + std::vector parts; + std::string boundary; + bool multipart; + bool crlf; + + protected: + bool message; + + public: + Part(); + friend bool operator==(const Part& lhs, const Part& rhs); + friend bool operator!=(const Part& lhs, const Part& rhs); + + // Loading and saving a whole MIME message + std::string load(std::istream& in, const std::string& parent_boundary = {}); + void load(const std::string& filename); + void save(std::ostream& out) const; + void save(const std::string& filename) const; + void from_string(const std::string& data); + std::string to_string() const; + + // Low-level access + std::string get_body() const; + std::string get_preamble() const; + std::string get_epilogue() const; + std::string get_boundary() const; + std::vector& get_parts(); + const std::vector& get_parts() const; + std::vector>& get_headers(); + const std::vector>& get_headers() const; + + bool is_multipart() const; + bool is_multipart(const std::string& subtype) const; + bool is_singlepart() const; + bool is_singlepart(const std::string& type) const; + + void set_body(const std::string& body); + void set_preamble(const std::string& preamble); + void set_epilogue(const std::string& epilogue); + void set_boundary(const std::string& boundary); + void set_parts(const std::vector& parts); + void set_headers(const std::vector>& headers); + + void clear(); + void clear_body(); + + // Header manipulation + std::string get_header(const std::string& field) const; + void set_header(const std::string& field, const std::string& value); + std::string& operator[](const std::string& field); + const std::string& operator[](const std::string& field) const; + + void append_header(const std::string& field, const std::string& value); + void prepend_header(const std::string& field, const std::string& value); + void erase_header(const std::string& field); + void clear_headers(); + + // Specialized header functions + std::string get_multipart_type() const; + std::string get_header_value(const std::string& field) const; + std::string get_header_parameter(const std::string& field, const std::string& parameter) const; + + void set_header_value(const std::string& field, const std::string& value); + void set_header_parameter(const std::string& field, const std::string& paramter, const std::string& value); + + void add_received(const std::string& domain, const std::chrono::system_clock::time_point& date = std::chrono::system_clock::now()); + void generate_msgid(const std::string& domain); + void set_date(const std::chrono::system_clock::time_point& date = std::chrono::system_clock::now()); + + // Part manipulation + Part& append_part(const Part& part = {}); + Part& prepend_part(const Part& part = {}); + + void clear_parts(); + void make_multipart(const std::string& type, const std::string& boundary = {}); + bool flatten(); + + std::string get_mime_type() const; + void set_mime_type(const std::string& type); + bool is_mime_type(const std::string& type) const; + bool has_mime_type() const; + + // Body and attachments + Part& set_alternative(const std::string& subtype, const std::string& text); + + void set_plain(const std::string& text); + void set_html(const std::string& text); + + const Part* get_first_matching_part(std::function predicate) const; + + Part* get_first_matching_part(std::function predicate); + const Part* get_first_matching_part(const std::string& type) const; + + Part* get_first_matching_part(const std::string& type); + std::string get_first_matching_body(const std::string& type) const; + std::string get_text() const; + std::string get_plain() const; + std::string get_html() const; + + Part& attach(const Part& attachment); + Part& attach(const std::string& data, const std::string& mime_type, const std::string& filename = {}); + Part& attach(std::istream& in, const std::string& mime_type, const std::string& filename = {}); + + std::vector get_attachments() const; + + void clear_alternative(const std::string& subtype); + void clear_text(); + void clear_plain(); + void clear_html(); + void clear_attachments(); + + void simplify(); + + bool has_text() const; + bool has_plain() const; + bool has_html() const; + bool has_attachments() const; + bool is_attachment() const; + bool is_inline() const; + + // Format manipulation + void set_crlf(bool value = true); + }; + + class Message : public Part { + public: + Message(); + }; + + bool operator==(const Part& lhs, const Part& rhs); + bool operator!=(const Part& lhs, const Part& rhs); + +} + +inline std::ostream& operator<<(std::ostream& out, const Mimesis::Part& part) { + part.save(out); + return out; +} + +inline std::istream& operator>>(std::istream& in, Mimesis::Part& part) { + part.load(in); + return in; +} diff --git a/src/librssguard/3rd-party/mimesis/quoted-printable.cpp b/src/librssguard/3rd-party/mimesis/quoted-printable.cpp new file mode 100755 index 000000000..dabd55ed1 --- /dev/null +++ b/src/librssguard/3rd-party/mimesis/quoted-printable.cpp @@ -0,0 +1,61 @@ +// For license of this file, see /LICENSE.md. + +/* Mimesis -- a library for parsing and creating RFC2822 messages + Copyright © 2017 Guus Sliepen + + Mimesis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . + */ + +#include "quoted-printable.hpp" + +using namespace std; + +string quoted_printable_decode(string_view in) { + string out; + + out.reserve(in.size()); + + int decode = 0; + uint8_t val = 0; + + for (auto&& c: in) { + if (decode) { + if (c >= '0' && c <= '9') { + val <<= 4; + val |= c - '0'; + decode--; + } + else if (c >= 'A' && c <= 'F') { + val <<= 4; + val |= 10 + (c - 'A'); + decode--; + } + else { + decode = 0; + continue; + } + + if (decode == 0) + out.push_back(static_cast(val)); + } + else { + if (c == '=') + decode = 2; + else + out.push_back(c); + } + } + + return out; +} diff --git a/src/librssguard/3rd-party/mimesis/quoted-printable.hpp b/src/librssguard/3rd-party/mimesis/quoted-printable.hpp new file mode 100755 index 000000000..54c054af3 --- /dev/null +++ b/src/librssguard/3rd-party/mimesis/quoted-printable.hpp @@ -0,0 +1,25 @@ +// For license of this file, see /LICENSE.md. + +#pragma once + +/* Mimesis -- a library for parsing and creating RFC2822 messages + Copyright © 2017 Guus Sliepen + + Mimesis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . + */ + +#include +#include + +std::string quoted_printable_decode(std::string_view in); diff --git a/src/librssguard/gui/dialogs/formabout.ui b/src/librssguard/gui/dialogs/formabout.ui index a011b0dfa..84959f6dd 100644 --- a/src/librssguard/gui/dialogs/formabout.ui +++ b/src/librssguard/gui/dialogs/formabout.ui @@ -92,7 +92,7 @@ - 2 + 0 @@ -167,7 +167,7 @@ p, li { white-space: pre-wrap; } true - GNU GPL License (applies to RSS Guard source code) + GNU GPL License (applies to RSS Guard and mimesis source code) GNU GPL License diff --git a/src/librssguard/librssguard.pro b/src/librssguard/librssguard.pro index 3ba224694..80c25a86b 100644 --- a/src/librssguard/librssguard.pro +++ b/src/librssguard/librssguard.pro @@ -413,6 +413,10 @@ else { gui/newspaperpreviewer.ui } +# Add mimesis. +SOURCES += $$files(3rd-party/mimesis/*.cpp, false) +HEADERS += $$files(3rd-party/mimesis/*.hpp, false) + INCLUDEPATH += $$PWD/. \ $$PWD/gui \ $$PWD/gui/dialogs \ diff --git a/src/librssguard/services/gmail/gui/formaddeditemail.cpp b/src/librssguard/services/gmail/gui/formaddeditemail.cpp index db75da29f..5ec1ce261 100644 --- a/src/librssguard/services/gmail/gui/formaddeditemail.cpp +++ b/src/librssguard/services/gmail/gui/formaddeditemail.cpp @@ -2,6 +2,7 @@ #include "services/gmail/gui/formaddeditemail.h" +#include "gui/guiutilities.h" #include "miscellaneous/application.h" #include "miscellaneous/iconfactory.h" #include "services/gmail/gmailserviceroot.h" @@ -10,6 +11,8 @@ FormAddEditEmail::FormAddEditEmail(GmailServiceRoot* root, QWidget* parent) : QDialog(parent), m_root(root) { m_ui.setupUi(this); + GuiUtilities::applyDialogProperties(*this, qApp->icons()->fromTheme(QSL("mail-message-new"))); + m_ui.m_layoutAdder->setMargin(0); m_ui.m_layoutAdder->setContentsMargins(0, 0, 0, 0); diff --git a/src/librssguard/services/gmail/network/gmailnetworkfactory.cpp b/src/librssguard/services/gmail/network/gmailnetworkfactory.cpp index bbaef5d24..dac5a4cee 100644 --- a/src/librssguard/services/gmail/network/gmailnetworkfactory.cpp +++ b/src/librssguard/services/gmail/network/gmailnetworkfactory.cpp @@ -228,11 +228,11 @@ void GmailNetworkFactory::markMessagesStarred(RootItem::Importance importance, c QJsonArray param_add, param_remove; if (importance == RootItem::Importance::Important) { - // We add label UNREAD. + // We add label STARRED. param_add.append(GMAIL_SYSTEM_LABEL_STARRED); } else { - // We remove label UNREAD. + // We remove label STARRED. param_remove.append(GMAIL_SYSTEM_LABEL_STARRED); }