#include "message.h"
#include "libserver/html/html.h"
-#define msg_debug_images(...) rspamd_conditional_debug_fast(NULL, NULL, \
- rspamd_images_log_id, "images", task->task_pool->tag.uid, \
- G_STRFUNC, \
- __VA_ARGS__)
-
-INIT_LOG_MODULE(images)
-
#ifdef USABLE_GD
#include "gd.h"
#include "hash.h"
}
static void
-rspamd_image_process_part(struct rspamd_task *task, struct rspamd_mime_part *part)
+rspamd_image_add_content_id(struct rspamd_task *task,
+ struct rspamd_mime_part *part,
+ GHashTable *cid_images)
{
struct rspamd_mime_header *rh;
- struct rspamd_mime_text_part *tp;
- struct html_image *himg;
const char *cid;
- unsigned int cid_len, i;
+ gsize cid_len;
struct rspamd_image *img;
img = (struct rspamd_image *) part->specific.img;
cid_len--;
}
- PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, tp)
- {
- if (IS_TEXT_PART_HTML(tp) && tp->html != NULL) {
- himg = rspamd_html_find_embedded_image(tp->html, cid, cid_len);
-
- if (himg != NULL) {
- img->html_image = himg;
- himg->embedded_image = img;
-
- msg_debug_images("found linked image by cid: <%s>",
- cid);
-
- if (himg->height == 0) {
- himg->height = img->height;
- }
-
- if (himg->width == 0) {
- himg->width = img->width;
- }
- }
- }
- }
+ char *cid_copy = rspamd_mempool_strdup_len(task->task_pool,
+ cid, cid_len);
+ g_hash_table_replace(cid_images, cid_copy, img);
}
}
}
void rspamd_images_link(struct rspamd_task *task)
{
struct rspamd_mime_part *part;
+ struct rspamd_mime_text_part *tp;
unsigned int i;
+ GHashTable *cid_images;
+
+ cid_images = g_hash_table_new(g_str_hash, g_str_equal);
PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, part)
{
if (part->part_type == RSPAMD_MIME_PART_IMAGE) {
- rspamd_image_process_part(task, part);
+ rspamd_image_add_content_id(task, part, cid_images);
+ }
+ }
+
+ if (g_hash_table_size(cid_images) > 0) {
+ PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, tp)
+ {
+ if (IS_TEXT_PART_HTML(tp) && tp->html != NULL) {
+ rspamd_html_link_embedded_images(tp->html, cid_images);
+ }
}
}
+
+ g_hash_table_destroy(cid_images);
}
return nullptr;
}
+void rspamd_html_link_embedded_images(void *html_content,
+ GHashTable *cid_images)
+{
+ auto *hc = rspamd::html::html_content::from_ptr(html_content);
+
+ for (auto *html_image: hc->images) {
+ if (html_image->flags & RSPAMD_HTML_FLAG_IMAGE_EMBEDDED &&
+ html_image->src != nullptr) {
+ auto *parsed_image = static_cast<rspamd_image *>(
+ g_hash_table_lookup(cid_images, html_image->src));
+
+ if (parsed_image != nullptr) {
+ parsed_image->html_image = html_image;
+ html_image->embedded_image = parsed_image;
+
+ if (html_image->height == 0) {
+ html_image->height = parsed_image->height;
+ }
+
+ if (html_image->width == 0) {
+ html_image->width = parsed_image->width;
+ }
+ }
+ }
+ }
+}
+
bool rspamd_html_get_parsed_content(void *html_content, rspamd_ftok_t *dest)
{
auto *hc = rspamd::html::html_content::from_ptr(html_content);
struct html_image *rspamd_html_find_embedded_image(void *html_content,
const char *cid, gsize cid_len);
+/**
+ * Link all embedded HTML images using a Content-ID to rspamd_image hash table
+ * @param html_content
+ * @param cid_images
+ */
+void rspamd_html_link_embedded_images(void *html_content,
+ GHashTable *cid_images);
+
/**
* Stores parsed content in ftok_t structure
* @param html_content
task:destroy()
end)
+ test("Embedded image dimensions are linked by Content-ID", function()
+ local png = "\137PNG\r\n\26\n" ..
+ "\0\0\0\13IHDR" ..
+ "\0\0\1\64\0\0\0\240"
+ local msg = table.concat {
+ hdrs,
+ "Content-Type: multipart/related; boundary=LINKED-IMAGE\n",
+ "\n",
+ "--LINKED-IMAGE\n",
+ "Content-Type: text/html\n",
+ "\n",
+ "<img src=\"cid:shared-image\">\n",
+ "--LINKED-IMAGE\n",
+ "Content-Type: image/png\n",
+ "Content-ID: <shared-image>\n",
+ "Content-Transfer-Encoding: binary\n",
+ "\n",
+ png,
+ "\n--LINKED-IMAGE--\n",
+ }
+ local res, task = rspamd_task.load_from_string(msg, rspamd_config)
+ assert_true(res, "failed to load message")
+ task:process_message()
+
+ local text_parts = task:get_text_parts()
+ assert_equal(1, #text_parts)
+ local html_images = text_parts[1]:get_html():get_images()
+ assert_equal(1, #html_images)
+ assert_equal(320, html_images[1].width)
+ assert_equal(240, html_images[1].height)
+ task:destroy()
+ end)
+
test("Part URLs are not deduplicated across MIME parts", function()
local msg = table.concat {
hdrs,
#include "rspamd_cxx_unit_rfc2047.hxx"
#include "rspamd_cxx_unit_html_url_rewrite.hxx"
#include "rspamd_cxx_unit_html_cta.hxx"
+#include "rspamd_cxx_unit_images.hxx"
#include "rspamd_cxx_unit_upstream_token_bucket.hxx"
#include "rspamd_cxx_unit_upstream_ring_hash.hxx"
#include "rspamd_cxx_unit_upstream_round_robin.hxx"
--- /dev/null
+/*
+ * Copyright 2026 Vsevolod Stakhov
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef RSPAMD_RSPAMD_CXX_UNIT_IMAGES_HXX
+#define RSPAMD_RSPAMD_CXX_UNIT_IMAGES_HXX
+
+#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+#include "doctest/doctest.h"
+
+#include "libmime/images.h"
+#include "libserver/html/html.h"
+#include "libserver/html/html.hxx"
+#include "libutil/mem_pool.h"
+
+#include <string>
+
+TEST_SUITE("image_processing")
+{
+ TEST_CASE("bulk Content-ID linking covers repeated HTML images")
+ {
+ auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(), nullptr, 0);
+ std::string html;
+ html.reserve(1024 * 32);
+
+ for (unsigned int i = 0; i < 1024; i++) {
+ html += "<img src=\"cid:shared-image\">";
+ }
+
+ auto *input = g_byte_array_sized_new(html.size());
+ g_byte_array_append(input,
+ reinterpret_cast<const guint8 *>(html.data()), html.size());
+ auto *parsed = rspamd_html_process_part(pool, input);
+ g_byte_array_free(input, TRUE);
+
+ struct rspamd_image mime_image = {};
+ mime_image.width = 320;
+ mime_image.height = 240;
+ auto *cid_images = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(cid_images,
+ const_cast<char *>("shared-image"), &mime_image);
+
+ rspamd_html_link_embedded_images(parsed, cid_images);
+
+ auto *hc = rspamd::html::html_content::from_ptr(parsed);
+ REQUIRE(hc->images.size() == 1024);
+ for (const auto *html_image: hc->images) {
+ CHECK(html_image->embedded_image == &mime_image);
+ CHECK(html_image->width == mime_image.width);
+ CHECK(html_image->height == mime_image.height);
+ }
+
+ g_hash_table_destroy(cid_images);
+ rspamd_mempool_delete(pool);
+ }
+}
+
+#endif