From da8eee0e691ce1cf553c50f1dbea151a7475f75a Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Mon, 28 Mar 2022 22:22:36 +0300 Subject: [PATCH] message: support cooked/raw filenames Supported a "cooked" mode for attachment filenames, which gets rid of any unacceptable characters. Add "raw_filename" to get the filename as specified in the part. Update tests. --- lib/message/mu-message-part.cc | 48 +++++++++++++++++++++++++++++++++- lib/message/mu-message-part.hh | 26 +++++++++++++++--- lib/message/mu-message.cc | 13 ++++----- 3 files changed, 77 insertions(+), 10 deletions(-) diff --git a/lib/message/mu-message-part.cc b/lib/message/mu-message-part.cc index 33d548d8..c96e1ef1 100644 --- a/lib/message/mu-message-part.cc +++ b/lib/message/mu-message-part.cc @@ -19,6 +19,7 @@ #include "mu-message-part.hh" +#include "glibconfig.h" #include "mu-mime-object.hh" #include "utils/mu-utils.hh" @@ -36,7 +37,41 @@ MessagePart::~MessagePart() = default; Option -MessagePart::filename() const noexcept +MessagePart::cooked_filename() const noexcept +{ + // make a bit more pallatble. + auto cleanup = [](const std::string& name)->std::string { + std::string clean; + clean.reserve(name.length()); + for (auto& c: name) { + auto taboo{(::iscntrl(c) || c == G_DIR_SEPARATOR || + c == ' ' || c == '\\' || c == ':')}; + clean += (taboo ? '-' : c); + } + if (clean.size() > 1 && clean[0] == '-') + clean.erase(0, 1); + + return clean; + }; + + // a MimePart... use the name if there is one. + if (mime_obj->is_part()) + return MimePart(*mime_obj).filename().map(cleanup); + + // MimeMessagepart. Construct a name based on subject. + if (mime_obj->is_message_part()) { + auto msg{MimeMessagePart(*mime_obj).get_message()}; + return msg.subject() + .map(cleanup) + .value_or("no-subject") + ".eml"; + } + +return Nothing; + +} + +Option +MessagePart::raw_filename() const noexcept { if (!mime_obj->is_part()) return Nothing; @@ -44,6 +79,8 @@ MessagePart::filename() const noexcept return MimePart(*mime_obj).filename(); } + + Option MessagePart::mime_type() const noexcept { @@ -62,6 +99,15 @@ MessagePart::size() const noexcept return MimePart(*mime_obj).size(); } +bool +MessagePart::is_attachment() const noexcept +{ + if (!mime_obj->is_part()) + return false; + else + return MimePart(*mime_obj).is_attachment(); +} + Option MessagePart::to_string() const noexcept diff --git a/lib/message/mu-message-part.hh b/lib/message/mu-message-part.hh index a5c31bcf..61a30eb3 100644 --- a/lib/message/mu-message-part.hh +++ b/lib/message/mu-message-part.hh @@ -53,11 +53,23 @@ public: ~MessagePart(); /** - * Filename for the mime-part + * Filename for the mime-part file. This is a "cooked" filename with + * unallowed characters removed. If there's no filename specified, + * construct one (such as in the case of MimeMessagePart). + * + * @see raw_filename() + * + * @return the name + */ + Option cooked_filename() const noexcept; + + /** + * Name for the mime-part file, i.e., MimePart::filename * * @return the filename or Nothing if there is none */ - Option filename() const noexcept; + Option raw_filename() const noexcept; + /** * Mime-type for the mime-part (e.g. "text/plain") @@ -73,6 +85,15 @@ public: */ size_t size() const noexcept; + /** + * Does this part have an "attachment" disposition? Otherwise it is + * "inline". Note that does *not* map 1:1 to a message's HasAttachment + * flag. + * + * @return true or false. + */ + bool is_attachment() const noexcept; + /** * Write (decoded) mime-part contents to string * @@ -80,7 +101,6 @@ public: */ Option to_string() const noexcept; - /** * Write (decoded) mime part to a file * diff --git a/lib/message/mu-message.cc b/lib/message/mu-message.cc index 493813da..3b325d4b 100644 --- a/lib/message/mu-message.cc +++ b/lib/message/mu-message.cc @@ -438,7 +438,7 @@ fill_document(Message::Private& priv) break; case Field::Id::File: for (auto&& part: priv.parts) - doc.add(field.id, part.filename()); + doc.add(field.id, part.raw_filename()); break; case Field::Id::Flags: doc.add(priv.flags); @@ -684,7 +684,7 @@ Content-Description: test file 1 MDAwAQID --=-=-= Content-Type: audio/ogg -Content-Disposition: inline; filename=file-02.bin +Content-Disposition: inline; filename=/tmp/file-02.bin Content-Transfer-Encoding: base64 MDA0BQYH @@ -721,27 +721,28 @@ R"(Hello,World!)"); g_assert_cmpuint(message->parts().size(),==,4); { auto&& part{message->parts().at(0)}; - g_assert_false(!!part.filename()); + g_assert_false(!!part.raw_filename()); assert_equal(part.mime_type().value(), "text/plain"); assert_equal(part.to_string().value(), "Hello,"); } { auto&& part{message->parts().at(1)}; - assert_equal(part.filename().value(), "file-01.bin"); + assert_equal(part.raw_filename().value(), "file-01.bin"); assert_equal(part.mime_type().value(), "image/jpeg"); // file consist of 6 bytes "000" 0x01,0x02.0x03. assert_equal(part.to_string().value(), "000\001\002\003"); } { auto&& part{message->parts().at(2)}; - assert_equal(part.filename().value(), "file-02.bin"); + assert_equal(part.raw_filename().value(), "/tmp/file-02.bin"); + assert_equal(part.cooked_filename().value(), "tmp-file-02.bin"); assert_equal(part.mime_type().value(), "audio/ogg"); // file consist of the string "004" followed by 0x5,0x6,0x7. assert_equal(part.to_string().value(), "004\005\006\007"); } { auto&& part{message->parts().at(3)}; - g_assert_false(!!part.filename()); + g_assert_false(!!part.raw_filename()); g_assert_true(!!part.mime_type()); assert_equal(part.mime_type().value(), "text/plain"); assert_equal(part.to_string().value(), "World!");