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.
This commit is contained in:
Dirk-Jan C. Binnema 2022-03-28 22:22:36 +03:00
parent 9f062ae482
commit da8eee0e69
3 changed files with 77 additions and 10 deletions

View File

@ -19,6 +19,7 @@
#include "mu-message-part.hh" #include "mu-message-part.hh"
#include "glibconfig.h"
#include "mu-mime-object.hh" #include "mu-mime-object.hh"
#include "utils/mu-utils.hh" #include "utils/mu-utils.hh"
@ -36,7 +37,41 @@ MessagePart::~MessagePart() = default;
Option<std::string> Option<std::string>
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<std::string>
MessagePart::raw_filename() const noexcept
{ {
if (!mime_obj->is_part()) if (!mime_obj->is_part())
return Nothing; return Nothing;
@ -44,6 +79,8 @@ MessagePart::filename() const noexcept
return MimePart(*mime_obj).filename(); return MimePart(*mime_obj).filename();
} }
Option<std::string> Option<std::string>
MessagePart::mime_type() const noexcept MessagePart::mime_type() const noexcept
{ {
@ -62,6 +99,15 @@ MessagePart::size() const noexcept
return MimePart(*mime_obj).size(); 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<std::string> Option<std::string>
MessagePart::to_string() const noexcept MessagePart::to_string() const noexcept

View File

@ -53,11 +53,23 @@ public:
~MessagePart(); ~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<std::string> cooked_filename() const noexcept;
/**
* Name for the mime-part file, i.e., MimePart::filename
* *
* @return the filename or Nothing if there is none * @return the filename or Nothing if there is none
*/ */
Option<std::string> filename() const noexcept; Option<std::string> raw_filename() const noexcept;
/** /**
* Mime-type for the mime-part (e.g. "text/plain") * Mime-type for the mime-part (e.g. "text/plain")
@ -73,6 +85,15 @@ public:
*/ */
size_t size() const noexcept; 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 * Write (decoded) mime-part contents to string
* *
@ -80,7 +101,6 @@ public:
*/ */
Option<std::string> to_string() const noexcept; Option<std::string> to_string() const noexcept;
/** /**
* Write (decoded) mime part to a file * Write (decoded) mime part to a file
* *

View File

@ -438,7 +438,7 @@ fill_document(Message::Private& priv)
break; break;
case Field::Id::File: case Field::Id::File:
for (auto&& part: priv.parts) for (auto&& part: priv.parts)
doc.add(field.id, part.filename()); doc.add(field.id, part.raw_filename());
break; break;
case Field::Id::Flags: case Field::Id::Flags:
doc.add(priv.flags); doc.add(priv.flags);
@ -684,7 +684,7 @@ Content-Description: test file 1
MDAwAQID MDAwAQID
--=-=-= --=-=-=
Content-Type: audio/ogg Content-Type: audio/ogg
Content-Disposition: inline; filename=file-02.bin Content-Disposition: inline; filename=/tmp/file-02.bin
Content-Transfer-Encoding: base64 Content-Transfer-Encoding: base64
MDA0BQYH MDA0BQYH
@ -721,27 +721,28 @@ R"(Hello,World!)");
g_assert_cmpuint(message->parts().size(),==,4); g_assert_cmpuint(message->parts().size(),==,4);
{ {
auto&& part{message->parts().at(0)}; 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.mime_type().value(), "text/plain");
assert_equal(part.to_string().value(), "Hello,"); assert_equal(part.to_string().value(), "Hello,");
} }
{ {
auto&& part{message->parts().at(1)}; 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"); assert_equal(part.mime_type().value(), "image/jpeg");
// file consist of 6 bytes "000" 0x01,0x02.0x03. // file consist of 6 bytes "000" 0x01,0x02.0x03.
assert_equal(part.to_string().value(), "000\001\002\003"); assert_equal(part.to_string().value(), "000\001\002\003");
} }
{ {
auto&& part{message->parts().at(2)}; 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"); assert_equal(part.mime_type().value(), "audio/ogg");
// file consist of the string "004" followed by 0x5,0x6,0x7. // file consist of the string "004" followed by 0x5,0x6,0x7.
assert_equal(part.to_string().value(), "004\005\006\007"); assert_equal(part.to_string().value(), "004\005\006\007");
} }
{ {
auto&& part{message->parts().at(3)}; auto&& part{message->parts().at(3)};
g_assert_false(!!part.filename()); g_assert_false(!!part.raw_filename());
g_assert_true(!!part.mime_type()); g_assert_true(!!part.mime_type());
assert_equal(part.mime_type().value(), "text/plain"); assert_equal(part.mime_type().value(), "text/plain");
assert_equal(part.to_string().value(), "World!"); assert_equal(part.to_string().value(), "World!");