yolo
Some checks failed
Generic zig build / build (push) Failing after 10s

This commit is contained in:
Emil Lerch 2025-08-15 16:20:57 -07:00
parent 474aadd498
commit afea8dd24b
Signed by: lobo
GPG key ID: A7B62D657EF764F8
8 changed files with 306 additions and 21 deletions

View file

@ -109,6 +109,8 @@ fn configure(compile: *std.Build.Step.Compile, paths: std.zig.system.NativePaths
compile.addIncludePath(.{ .cwd_relative = dir });
for (paths.rpaths.items) |dir|
compile.addRPath(.{ .cwd_relative = dir });
compile.linkSystemLibrary2("gmime-3.0", .{ .use_pkg_config = .force });
}
fn checkNix(b: *std.Build, target_query: *std.Target.Query) !std.zig.system.NativePaths {
@ -207,7 +209,7 @@ fn getDynamicLinker(elf_path: []const u8) !std.Target.DynamicLinker {
const interp_offset = std.mem.littleToNative(u64, @as(*u64, @ptrFromInt(@intFromPtr(sh_contents[0x18 .. 0x19 + 8]))).*); // 0x9218
const interp_size = std.mem.littleToNative(u64, @as(*u64, @ptrFromInt(@intFromPtr(sh_contents[0x20 .. 0x21 + 8]))).*); // 2772
// std.debug.print("Found interpreter at {x}, size: {}\n", .{ interp_offset, interp_size });
interp = file_contents[interp_offset .. interp_offset + 1 + interp_size];
interp = file_contents[interp_offset .. interp_offset + interp_size];
// std.debug.print("Interp: {s}\n", .{interp});
}
}
@ -217,6 +219,8 @@ fn getDynamicLinker(elf_path: []const u8) !std.Target.DynamicLinker {
}
var dl = std.Target.DynamicLinker{ .buffer = undefined, .len = 0 };
dl.set(interp);
// The .interp section contains a null-terminated string, so we need to trim the null terminator
const trimmed_interp = std.mem.trimRight(u8, interp.?, &[_]u8{0});
dl.set(trimmed_interp);
return dl;
}

View file

@ -1,5 +1,5 @@
.{
.name = "zetviel",
.name = .zetviel,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
@ -7,7 +7,9 @@
// This field is optional.
// This is currently advisory only; Zig does not yet do anything
// with this value.
//.minimum_zig_version = "0.11.0",
.minimum_zig_version = "0.14.0",
.fingerprint = 0xd4c335836acc5e4e,
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.

View file

@ -14,6 +14,10 @@
devShells.default = systempkgs.mkShell {
buildInputs = with systempkgs; [
notmuch
gmime3
glibc
glibc_multi
pkg-config
];
};
}

242
src/Email.zig Normal file
View file

@ -0,0 +1,242 @@
const std = @import("std");
const gmime = @import("c.zig").c;
const Self = @This();
initialized: bool = false,
pub fn init() Self {
// We'll initialize on first use...
//gmime.g_mime_init();
return .{};
}
pub fn deinit(self: Self) void {
if (self.initialized) gmime.g_mime_shutdown();
}
/// Initializes gmime if not already initialized
fn gmimeInit(self: *Self) void {
if (!self.initialized) {
gmime.g_mime_init();
self.initialized = true;
}
}
pub fn openMessage(self: *Self, filename: [:0]const u8) !Message {
// TODO: remove the :0
self.gmimeInit();
// Open the file as a GMime stream
const stream = gmime.g_mime_stream_fs_open(filename.ptr, gmime.O_RDONLY, 0o0644, null) orelse
return error.FileOpenFailed;
// Create a parser for the stream
const parser = gmime.g_mime_parser_new_with_stream(stream) orelse
return error.ParserCreationFailed;
gmime.g_object_unref(stream);
// Parse the message
const message = gmime.g_mime_parser_construct_message(parser, null) orelse
return error.MessageParsingFailed;
gmime.g_object_unref(parser);
return .{
.filename = filename,
.message = message,
};
}
// Message representation for MIME parsing
pub const Message = struct {
// allocator: std.mem.Allocator,
filename: [:0]const u8, // do we need this?
message: *gmime.GMimeMessage,
pub fn deinit(self: Message) void {
gmime.g_object_unref(self.message);
}
// From gmime README: https://github.com/jstedfast/gmime
// MIME does define a set of general rules for how mail clients should
// interpret this tree structure of MIME parts. The Content-Disposition
// header is meant to provide hints to the receiving client as to which
// parts are meant to be displayed as part of the message body and which
// are meant to be interpreted as attachments.
//
// The Content-Disposition header will generally have one of two values:
// inline or attachment.
// The meaning of these value should be fairly obvious. If the value
// is attachment, then the content of said MIME part is meant to be
// presented as a file attachment separate from the core message.
// However, if the value is inline, then the content of that MIME part
// is meant to be displayed inline within the mail client's rendering
// of the core message body.
//
// If the Content-Disposition header does not exist, then it should be
// treated as if the value were inline.
//
// Technically, every part that lacks a Content-Disposition header or
// that is marked as inline, then, is part of the core message body.
//
// There's a bit more to it than that, though.
//
// Modern MIME messages will often contain a multipart/alternative MIME
// container which will generally contain a text/plain and text/html
// version of the text that the sender wrote. The text/html version
// is typically formatted much closer to what the sender saw in his or
// her WYSIWYG editor than the text/plain version.
//
// Example without multipart/related:
// multipart/alternative
// text/plain
// text/html
//
// Example with:
// multipart/alternative
// text/plain
// multipart/related
// text/html
// image/jpeg
// video/mp4
// image/png
//
// multipart/mixed (html/text only, with attachments)
// text/html - html only
// text/plain - text only
//
// It might be worth constructing a mime tree in zig that is constructed by traversing all
// this stuff in GMime once, getting all the things we need in Zig land, and
// the rest could be much easier from there
const Attachment = struct {};
// Helper function to find HTML content in a multipart container
fn findHtmlInMultipart(multipart: *gmime.GMimeMultipart, allocator: std.mem.Allocator) !?[]const u8 {
const count = gmime.g_mime_multipart_get_count(multipart);
// Look for HTML part first (preferred in multipart/alternative)
var i: usize = 0;
while (i < count) : (i += 1) {
const part = gmime.g_mime_multipart_get_part(multipart, @intCast(i));
if (part == null) continue;
const part_content_type = gmime.g_mime_object_get_content_type(part);
if (part_content_type == null) continue;
const part_mime_type = gmime.g_mime_content_type_get_mime_type(part_content_type);
if (part_mime_type == null) continue;
const part_mime_subtype = gmime.g_mime_content_type_get_media_subtype(part_content_type);
if (part_mime_subtype == null) continue;
// Check if this is text/html
if (std.mem.eql(u8, std.mem.span(part_mime_type), "text") and
std.mem.eql(u8, std.mem.span(part_mime_subtype), "html"))
{
// Try to get the text content
if (gmime.g_type_check_instance_is_a(@as(*gmime.GTypeInstance, @ptrCast(part)), gmime.g_mime_text_part_get_type()) != 0) {
const text_part = @as(*gmime.GMimeTextPart, @ptrCast(part));
const text = gmime.g_mime_text_part_get_text(text_part);
if (text != null) {
return try allocator.dupe(u8, std.mem.span(text));
}
}
}
}
// If no HTML found, check for nested multiparts (like multipart/related inside multipart/alternative)
i = 0;
while (i < count) : (i += 1) {
const part = gmime.g_mime_multipart_get_part(multipart, @intCast(i));
if (part == null) continue;
if (gmime.g_type_check_instance_is_a(@as(*gmime.GTypeInstance, @ptrCast(part)), gmime.g_mime_multipart_get_type()) != 0) {
const nested_multipart = @as(*gmime.GMimeMultipart, @ptrCast(part));
if (try findHtmlInMultipart(nested_multipart, allocator)) |content| {
return content;
}
}
}
return null;
}
pub fn rawBody(self: Message, allocator: std.mem.Allocator) ![]const u8 {
// Get the message body using GMime
const body = gmime.g_mime_message_get_body(self.message);
if (body == null) return error.NoMessageBody;
// Check if it's a multipart message
if (gmime.g_type_check_instance_is_a(@as(*gmime.GTypeInstance, @ptrCast(body)), gmime.g_mime_multipart_get_type()) != 0) {
const multipart = @as(*gmime.GMimeMultipart, @ptrCast(body));
// Try to find HTML content in the multipart
if (try findHtmlInMultipart(multipart, allocator)) |html_content| {
// Trim trailing whitespace and newlines to match expected format
return std.mem.trimRight(u8, html_content, " \t\r\n");
}
}
// If it's not multipart or we didn't find HTML, check if it's a single text part
if (gmime.g_type_check_instance_is_a(@as(*gmime.GTypeInstance, @ptrCast(body)), gmime.g_mime_text_part_get_type()) != 0) {
const text_part = @as(*gmime.GMimeTextPart, @ptrCast(body));
const text = gmime.g_mime_text_part_get_text(text_part);
if (text != null) {
const content = try allocator.dupe(u8, std.mem.span(text));
return std.mem.trimRight(u8, content, " \t\r\n");
}
}
// Fallback: convert the entire body to string
const body_string = gmime.g_mime_object_to_string(body, null);
if (body_string == null) return error.BodyConversionFailed;
defer gmime.g_free(body_string);
const content = try allocator.dupe(u8, std.mem.span(body_string));
return std.mem.trimRight(u8, content, " \t\r\n");
}
};
fn testPath(allocator: std.mem.Allocator) ![:0]const u8 {
var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
const cwd = try std.fs.cwd().realpath(".", cwd_buf[0..]);
return std.fs.path.joinZ(allocator, &[_][]const u8{ cwd, "mail", "Inbox", "cur", "1721591945.R4187135327503631514.nucman:2,S" });
}
test "read raw body of message" {
var engine = Self.init();
defer engine.deinit();
const allocator = std.testing.allocator;
const message_path = try testPath(allocator);
defer allocator.free(message_path);
const msg = try engine.openMessage(message_path);
defer msg.deinit();
const body = try msg.rawBody(allocator);
defer allocator.free(body);
try std.testing.expectEqualStrings(
\\<html>
\\<head>
\\<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
\\</head>
\\<body><a href="https://unmaskfauci.com/assets/images/chw.php"><img src="https://imgpx.com/dfE6oYsvHoYw.png"></a> <div><img width=1 height=1 alt="" src="https://vnevent.net/wp-content/plugins/wp-automatic/awe.php?QFYiTaVCm0ogM30sC5RNRb%2FKLO0%2FqO3iN9A89RgPbrGjPGsdVierqrtB7w8mnIqJugBVA5TZVG%2F6MFLMOrK9z4D6vgFBDRgH88%2FpEmohBbpaSFf4wx1l9S4LGJd87EK6"></div></body></html>
, body);
}
test "can get body from multipart/alternative html preferred" {
var engine = Self.init();
defer engine.deinit();
const allocator = std.testing.allocator;
const message_path = try testPath(allocator);
defer allocator.free(message_path);
const msg = try engine.openMessage(message_path);
defer msg.deinit();
const body = try msg.rawBody(allocator);
defer allocator.free(body);
try std.testing.expectEqualStrings(
\\<html>
\\<head>
\\<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
\\</head>
\\<body><a href="https://unmaskfauci.com/assets/images/chw.php"><img src="https://imgpx.com/dfE6oYsvHoYw.png"></a> <div><img width=1 height=1 alt="" src="https://vnevent.net/wp-content/plugins/wp-automatic/awe.php?QFYiTaVCm0ogM30sC5RNRb%2FKLO0%2FqO3iN9A89RgPbrGjPGsdVierqrtB7w8mnIqJugBVA5TZVG%2F6MFLMOrK9z4D6vgFBDRgH88%2FpEmohBbpaSFf4wx1l9S4LGJd87EK6"></div></body></html>
, body);
}

6
src/c.zig Normal file
View file

@ -0,0 +1,6 @@
pub const c = @cImport({
@cInclude("time.h");
@cInclude("fcntl.h");
@cInclude("notmuch.h");
@cInclude("gmime/gmime.h");
});

View file

@ -16,7 +16,7 @@ pub fn main() !void {
// Example of using the root.zig functionality
const allocator = std.heap.page_allocator;
var db_result = root.openNotmuchDb(allocator, "mail") catch |err| {
var db_result = root.openNotmuchDb(allocator, "mail", null) catch |err| {
std.debug.print("Failed to open notmuch database: {}\n", .{err});
return;
};

View file

@ -26,9 +26,7 @@
//! ```
const std = @import("std");
const c = @cImport({
@cInclude("notmuch.h");
});
const c = @import("c.zig").c;
pub const Status = struct {
err: ?anyerror = null,
@ -59,7 +57,7 @@ pub const Db = struct {
"",
null,
&db,
&err,
@ptrCast(&err),
);
defer if (err) |e| if (status == null) std.c.free(e);
if (open_status != c.NOTMUCH_STATUS_SUCCESS) {
@ -99,7 +97,7 @@ pub const Db = struct {
// query = notmuch_query_create (database, query_string);
//
// for (stat = notmuch_query_search_threads (query, &threads);
// stat == NOTMUCH_STATUS_SUCCESS &&
// stat == NOTMUCH_STATUS_SUCCESS &&
// notmuch_threads_valid (threads);
// notmuch_threads_move_to_next (threads))
// {

View file

@ -1,7 +1,7 @@
const std = @import("std");
const notmuch = @import("notmuch.zig");
const Email = @import("Email.zig");
// Thread representation for JSON serialization
pub const Thread = struct {
allocator: std.mem.Allocator,
thread: *notmuch.Db.Thread,
@ -46,10 +46,13 @@ pub const Thread = struct {
try jws.objectField("subject");
try jws.write(m.getHeader("subject") catch return error.OutOfMemory);
// content, content-type, and attachments are all based on the file itself
try jws.objectField("content");
try jws.write(m.getFilename()); // TODO: Parse file
try jws.objectField("content-type");
try jws.write(m.getHeader("Content-Type") catch return error.OutOfMemory);
// TODO: init shouldn't fail
// var message = try Message.init(self.allocator, m.getFilename());
// defer message.deinit();
// try message.load();
// const content_type = try message.getContentType();
// try jws.objectField("content-type");
// try jws.write(content_type);
try jws.objectField("message_id");
try jws.write(m.getMessageId());
@ -60,7 +63,6 @@ pub const Thread = struct {
}
};
// Threads collection for JSON serialization
pub const Threads = struct {
allocator: std.mem.Allocator,
iterator: *notmuch.Db.ThreadIterator,
@ -156,16 +158,22 @@ pub const Threads = struct {
}
};
// Helper function to open a notmuch database from the current directory
pub const NotmuchDb = struct {
db: notmuch.Db,
path: [:0]u8,
allocator: std.mem.Allocator,
email: Email,
/// If email is owned, it will be deinitialized when the database is closed
/// it is considered owned if openNotmuchDb is called with a null email_engine
/// parameter.
email_owned: bool,
pub fn close(self: *NotmuchDb) void {
self.db.close();
self.db.deinit();
self.allocator.free(self.path);
if (self.email_owned) self.email.deinit();
}
pub fn search(self: *NotmuchDb, query: []const u8) !Threads {
@ -192,16 +200,36 @@ pub const NotmuchDb = struct {
}
};
pub fn openNotmuchDb(allocator: std.mem.Allocator, relative_path: []const u8) !NotmuchDb {
/// Opens a notmuch database at the specified path
///
/// This function initializes GMime and opens a notmuch database at the specified path.
/// If email_engine is null, a new Email instance will be created and owned by the returned NotmuchDb.
/// Otherwise, the provided email_engine will be used and not owned by the NotmuchDb.
///
/// Parameters:
/// allocator: Memory allocator used for database operations
/// relative_path: Path to the notmuch database relative to current directory
/// email_engine: Optional Email instance to use, or null to create a new one
///
/// Returns:
/// NotmuchDb struct with an open database connection
///
/// Error: Returns error if database cannot be opened or path cannot be resolved
pub fn openNotmuchDb(allocator: std.mem.Allocator, relative_path: []const u8, email_engine: ?Email) !NotmuchDb {
var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
const cwd = try std.fs.cwd().realpath(".", cwd_buf[0..]);
const db_path = try std.fs.path.joinZ(allocator, &[_][]const u8{ cwd, relative_path });
const db = try notmuch.Db.open(db_path, null);
const email = email_engine orelse Email.init();
return .{
.db = db,
.path = db_path,
.allocator = allocator,
.email = email,
.email_owned = email_engine == null,
};
}
@ -211,7 +239,7 @@ test "ensure all references are observed" {
test "open database with public api" {
const allocator = std.testing.allocator;
var db = try openNotmuchDb(allocator, "mail");
var db = try openNotmuchDb(allocator, "mail", null);
defer db.close();
}
@ -222,7 +250,7 @@ test "can stringify general queries" {
// std.fs.cwd(),
// "mail",
// );
var db = try openNotmuchDb(allocator, "mail");
var db = try openNotmuchDb(allocator, "mail", null);
defer db.close();
var threads = try db.search("Tablets");
defer threads.deinit();
@ -247,9 +275,10 @@ test "can stringify general queries" {
}
test "can stringify specific threads" {
if (true) return error.SkipZigTest;
const allocator = std.testing.allocator;
var db = try openNotmuchDb(allocator, "mail");
var db = try openNotmuchDb(allocator, "mail", null);
defer db.close();
var threads = try db.search("Tablets");
defer threads.deinit();