Files
mu4e/scm/mu-scm-mime.cc
Dirk-Jan C. Binnema b02aa57686 mu-scm: implement mime-part handling, refact
Implement accessing the MIME-parts + docs  + test.

Implement saving attachments to file.

Implement creating messages from files.

Refactor / rename functions to be more uniform.
2025-07-07 10:57:13 +03:00

258 lines
6.9 KiB
C++

/*
** Copyright (C) 2025 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
** Free Software Foundation; either version 3, 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 General Public License
** along with this program; if not, write to the Free Software Foundation,
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/
#include "mu-scm-types.hh"
#include "message/mu-message.hh"
#include "message/mu-mime-object.hh"
#include <mutex>
#include <cstdio>
using namespace Mu;
using namespace Mu::Scm;
namespace {
static SCM mime_part_type;
static scm_t_port_type *mime_stream_port_type;
}
static GMimeStream*
from_scm_port(SCM port)
{
return GMIME_STREAM(reinterpret_cast<GMimeStream*>(SCM_STREAM(port)));
}
static size_t
mime_stream_read(SCM port, SCM dst, size_t start, size_t count)
{
auto stream{from_scm_port(port)};
const auto res = g_mime_stream_read(stream,
reinterpret_cast<char*>(SCM_BYTEVECTOR_CONTENTS (dst) + start),
count);
if (res < 0)
scm_misc_error("mime-object-read", "failed to read stream",
SCM_BOOL_F);
return static_cast<size_t>(res);
}
static scm_t_off
mime_stream_seek(SCM port, scm_t_off offset, int whence)
{
auto stream{from_scm_port(port)};
const auto gwhence = [](int w) {
switch(w) {
case SEEK_SET: return GMIME_STREAM_SEEK_SET;
case SEEK_CUR: return GMIME_STREAM_SEEK_CUR;
case SEEK_END: return GMIME_STREAM_SEEK_END;
default:
scm_misc_error("mime-stream-seek", "invalid whence",
SCM_BOOL_F);
return GMIME_STREAM_SEEK_SET; // never reached.
}
}(whence);
const auto res = g_mime_stream_seek(stream, offset, gwhence);
if (res < 0)
scm_misc_error("mime-stream-seek", "invalid seek",
SCM_BOOL_F);
return res;
}
static scm_t_port_type*
make_mime_stream_port_type()
{
auto ptype = scm_make_port_type(const_cast<char*>("mime-stream"), mime_stream_read, {});
scm_set_port_close(ptype, [](SCM port){g_mime_stream_close(from_scm_port(port));});
scm_set_port_needs_close_on_gc(ptype, true);
scm_set_port_seek(ptype, mime_stream_seek);
return ptype;
}
static GMimePart*
part_from_scm(SCM scm, const char *func, int pos)
{
if (!SCM_IS_A_P(scm, mime_part_type))
throw ScmError{ScmError::Id::WrongType, func, pos, scm, "mime-part"};
return GMIME_PART(reinterpret_cast<GMimePart*>(scm_foreign_object_ref(scm, 0)));
}
static GMimeStream*
get_decoded_stream(GMimePart *part)
{
auto wrapper{g_mime_part_get_content(part)};
if (!wrapper)
throw ScmError{"make-make-mime-stream-port",
"failed to create data-wrapper"};
auto stream{g_mime_stream_mem_new()};
if (!stream)
throw ScmError{"make-make-mime-stream-port",
"failed to create mem-stream"};
const auto res{g_mime_data_wrapper_write_to_stream(wrapper, stream)};
if (res < 0) {
g_object_unref(stream);
throw ScmError{"make-make-mime-stream-port",
"failed to write to stream"};
}
return stream;
}
static GMimeStream*
get_stream(GMimePart *part, bool content_only)
{
auto stream = g_mime_stream_mem_new();
if (!stream)
throw ScmError{"make-mime-stream-port",
"failed to create mem-stream"};
ssize_t res{};
if (content_only) // content-only
res = g_mime_object_write_content_to_stream(
GMIME_OBJECT(part), {}, stream);
else // with headers
res = g_mime_object_write_to_stream(
GMIME_OBJECT(part), {}, stream);
if (res < 0) {
g_object_unref(stream);
throw ScmError{"make-mime-stream-port",
"failed to write to stream"};
}
return stream;
}
/**
* Create a port for the mime-part
*
* @param mime_obj mime object (foreign object)
* @param content_only_scm whether to not include headers
* @param decode_scm whether to decode content
* (must be false if decode_scm is true)
*
* @return SCM for mime stream port
*/
static SCM
subr_make_mime_stream_port(SCM mime_part_scm, SCM content_only_scm,
SCM decode_scm)
{
constexpr auto func{"make-mime-stream-port"};
GMimeStream *stream{};
try {
auto part = part_from_scm(mime_part_scm, func, 1);
const auto decode{from_scm<bool>(decode_scm,
func, 2)};
const auto content_only{from_scm<bool>(content_only_scm,
func, 3)};
if (decode)
stream = get_decoded_stream(part);
else
stream = get_stream(part, content_only);
if (const auto res = g_mime_stream_reset(stream); res != 0)
throw ScmError{func, "failed to reset stream"};
return scm_c_make_port(mime_stream_port_type, SCM_RDNG,
reinterpret_cast<scm_t_bits>(stream));
} catch (const ScmError& err) {
if (stream)
g_object_unref(stream);
err.throw_scm();
}
return SCM_UNSPECIFIED;
}
SCM
Mu::Scm::to_scm(GMimePart *part)
{
return scm_make_foreign_object_1(mime_part_type, g_object_ref(part));
}
SCM
Mu::Scm::to_scm(size_t idx, const MessagePart& part)
{
static SCM sym_index{make_symbol("index")};
static SCM sym_content_type{make_symbol("content-type")};
static SCM sym_content_description{make_symbol("content-description")};
static SCM sym_size{make_symbol("size")};
static SCM sym_attachment{make_symbol("attachment?")};
static SCM sym_filename{make_symbol("filename")};
static SCM sym_signed{make_symbol("signed?")};
static SCM sym_encrypted{make_symbol("encrypted?")};
SCM alist = scm_acons(sym_index, to_scm(idx), SCM_EOL);
if (const auto ctype{part.mime_type()}; ctype)
alist = scm_acons(sym_content_type, to_scm(*ctype), alist);
if (const auto cdesc{part.content_description()}; cdesc)
alist = scm_acons(sym_content_description, to_scm(*cdesc), alist);
if (part.is_attachment())
alist = scm_acons(sym_attachment, SCM_BOOL_T, alist);
alist = scm_acons(sym_size, to_scm(part.size()), alist);
if (const auto fname{part.cooked_filename(true/*minmimal*/)}; fname)
alist = scm_acons(sym_filename, to_scm(*fname), alist);
if (part.is_signed())
alist = scm_acons(sym_signed, SCM_BOOL_T, alist);
if (part.is_encrypted())
alist = scm_acons(sym_encrypted, SCM_BOOL_T, alist);
return scm_reverse_x(alist, SCM_EOL); // slightly more convenient
}
static void
init_subrs()
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
scm_c_define_gsubr("cc-mime-make-stream-port",3/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_make_mime_stream_port));
#pragma GCC diagnostic pop
}
void
Mu::Scm::init_mime()
{
mime_part_type = scm_make_foreign_object_type(
make_symbol("g-mime-part"),
scm_list_1(make_symbol("data")),
[](SCM scm) { // finalizer
g_object_unref(
GMIME_PART(reinterpret_cast<GMimePart*>(
scm_foreign_object_ref(scm, 0))));
});
mime_stream_port_type = make_mime_stream_port_type();
init_subrs();
}