basic search functionality

This commit is contained in:
Emil Lerch 2024-07-30 10:41:32 -07:00
parent bfea6e15ef
commit 5d6a777965
Signed by: lobo
GPG key ID: A7B62D657EF764F8
10 changed files with 446 additions and 5 deletions

21
README.md Normal file
View file

@ -0,0 +1,21 @@
Zetviel
-------
As some background, I've had some issues with the very usable [netviel](https://github.com/DavidMStraub/netviel).
I wanted to address those issues, but also simplify the deployment. And, I like zig,
so I decided this was small enough I'd just re-write the thing to make my own.
This is still very work in progress, to the point it is not yet usable. It has
some basic notmuch integration and a usable build system.
Building
--------
If you have notmuch installed (libnotmuch-dev on a debian-based system),
`zig build` is all you need. If you are using nix, you can `nix develop`, which
will install the necessary notmuch header/library, and the build system will
detect and use that. Again, `zig build` will work in that instance, but you must
`nix develop` first.
More to come...

Binary file not shown.

View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,77 @@
Return-Path: <mail@youpharm.co>
Delivered-To: lobo@lerch.org
Received: from mail.eler.ch
by mail.eler.ch with LMTP
id BJ9kLnLDm2aYDgQAyA9pPg
(envelope-from <mail@youpharm.co>)
for <lobo@lerch.org>; Sat, 20 Jul 2024 14:02:26 +0000
Received: from youpharm.co (youpharm.co [5.101.65.218])
by mail.eler.ch (Postfix) with ESMTP id E3C9F467AB
for <emil@lerch.org>; Sat, 20 Jul 2024 14:02:25 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=s1; d=youpharm.co;
h=Message-ID:From:To:Subject:Date:MIME-Version:Content-Type; i=abuse@youpharm.co;
bh=Lnj6s7dL4V4gz92hab9GJ0HxEL8=;
b=MLiLXLaBVkYaJuabi+DsOFUkjmqsYJ0hsfHW5JKX61Fal+1j2iFjFWrggCv+m0zruA+j6W+iJ7CV
nxcDpT5mZe0+e2bOu1f8YEGNj7DPVpkYjeB8esR4qo/LSot0TIOU7YojSk8HP/hQVYEwpC58f21C
sXgqEyMn1bV4+UHE1QnhoyZRP/lyadba4SCCSeyG5VQMZ4cIZtlcBFA+yd6I03lZ2f/Lh7tinFXj
HrOyPjQLpk4VNbVsbbpsI+sKOEGlmgpRVIatV+Hcwk8ZuhFsubdF/cSc1p3jFbUdhBa3TMcqFVS2
L1UO4e13PWJVjHzAUXlGDF66PGIR2pHnBd1pew==
DomainKey-Signature: a=rsa-sha1; c=nofws; q=dns; s=s1; d=youpharm.co;
b=fFAj5RzuUx0++wASk/u6T0GlBurb2y6h/1WEls22gWKfoEKHzChyXYJNyknfho2r/3Cw0DNrWXFI
nRIivnoNX4rOvc4hsCieljl9lt0fOaYzLgHKS083D8JIYLLySX0Qwj7xydC3nB3WmHhOlrz6eM7d
lPIOT14K1e5LxQTLox8PaUqknSUNrsBZ8tREcVLqb7Ud9SVvdHjyccjampV70XPOeKMd9NLt4a/H
sEeS184PGBo7/uAuHojS2y2LDkY6nRdZPmjPvA9ghNU8udr+biG3NEX8V2v2ZJy7w9H6FfJfCb2/
MrxZmGPWgcYJ7cQ/pNMKcHM1QoAKYKMEG76V8g==;
Message-ID: <8afeb74dca321817e44e07ac4a2e040962e86e@youpharm.co>
From: Top Medications <mail@youpharm.co>
To: emil@lerch.org
Subject: ***SPAM*** Tablets without a prescription
Date: Sat, 20 Jul 2024 16:02:18 +0200
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="08044db730c8e2ed1eb4fd56b0d4fef3d9e86e"
X-Rspamd-Queue-Id: E3C9F467AB
X-Rspamd-Server: mail.eler.ch
X-Spamd-Result: default: False [10.00 / 11.00];
ABUSE_SURBL(5.50)[youpharm.co:helo,youpharm.co:dkim,youpharm.co:rdns];
RSPAMD_URIBL(4.50)[unmaskfauci.com:url];
BAD_REP_POLICIES(0.10)[];
MIME_GOOD(-0.10)[multipart/alternative,text/plain];
DKIM_TRACE(0.00)[youpharm.co:+];
ARC_NA(0.00)[];
DMARC_POLICY_ALLOW(0.00)[youpharm.co,none];
RCVD_COUNT_ZERO(0.00)[0];
MIME_TRACE(0.00)[0:+,1:+,2:~];
FROM_EQ_ENVFROM(0.00)[];
RCPT_COUNT_ONE(0.00)[1];
TO_DN_NONE(0.00)[];
R_SPF_ALLOW(0.00)[+a];
R_DKIM_ALLOW(0.00)[youpharm.co:s=s1];
ASN(0.00)[asn:34665, ipnet:5.101.65.0/24, country:RU];
FROM_HAS_DN(0.00)[];
TO_MATCH_ENVRCPT_ALL(0.00)[];
DWL_DNSWL_BLOCKED(0.00)[youpharm.co:dkim];
MID_RHS_MATCH_FROM(0.00)[]
X-Rspamd-Action: rewrite subject
Content-Length: 789
--08044db730c8e2ed1eb4fd56b0d4fef3d9e86e
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
--08044db730c8e2ed1eb4fd56b0d4fef3d9e86e
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
<html>
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
</head>
<body><a href=3D"https://unmaskfauci.com/assets/images/chw.php"><img src=3D=
"https://imgpx.com/dfE6oYsvHoYw.png"></a> <div><img width=3D1 height=3D1 =
alt=3D"" src=3D"https://vnevent.net/wp-content/plugins/wp-automatic/awe.p=
hp?QFYiTaVCm0ogM30sC5RNRb%2FKLO0%2FqO3iN9A89RgPbrGjPGsdVierqrtB7w8mnIqJug=
BVA5TZVG%2F6MFLMOrK9z4D6vgFBDRgH88%2FpEmohBbpaSFf4wx1l9S4LGJd87EK6"></div=
></body></html>
--08044db730c8e2ed1eb4fd56b0d4fef3d9e86e--

View file

@ -1,11 +1,9 @@
const std = @import("std");
const c = @cImport({
@cInclude("notmuch.h");
});
const notmuch = @import("notmuch.zig");
pub fn main() !void {
// Prints to stderr (it's a shortcut based on `std.io.getStdErr()`)
std.debug.print("All your {s} are belong to us. Status: {s}\n", .{ "codebase", c.notmuch_status_to_string(0) });
std.debug.print("All your {s} are belong to us. \n", .{"codebase"});
// stdout is for the actual output of your application, for example if you
// are implementing gzip, then only the compressed bytes should be sent to
@ -20,5 +18,151 @@ pub fn main() !void {
}
test "can get status" {
try std.testing.expectEqualStrings("No error occurred", std.mem.span(c.notmuch_status_to_string(0)));
// const allocator = std.testing.allocator;
// const db_path = try std.fs.path.join(
// allocator,
// std.fs.cwd(),
// "mail",
// );
// Current directory under test is root of project
var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
const cwd = try std.fs.cwd().realpath(".", cwd_buf[0..]);
var path_buf: [std.fs.max_path_bytes:0]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(path_buf[0..]);
const db_path = try std.fs.path.joinZ(fba.allocator(), &[_][]const u8{ cwd, "mail" });
{
var status: notmuch.Status = undefined;
var db = try notmuch.Db.open(db_path, &status);
defer db.deinit();
defer db.close();
defer status.deinit();
try std.testing.expectEqualStrings("No error occurred", status.statusString());
}
{
var db = try notmuch.Db.open(db_path, null);
defer db.deinit();
defer db.close();
}
{
var status: notmuch.Status = undefined;
try std.testing.expectError(error.CouldNotOpenDatabase, notmuch.Db.open(
"NON-EXISTANT",
&status,
));
defer status.deinit();
try std.testing.expectEqualStrings(
"Path supplied is illegal for this function",
status.statusString(),
);
}
//
// // This is the python that's executing
// // def get(self, thread_id):
// // threads = notmuch.Query(
// // get_db(), "thread:{}".format(thread_id)
// // ).search_threads()
// // thread = next(threads) # there can be only 1
// // messages = thread.get_messages()
// // return messages_to_json(messages)
// try std.testing.expectEqualStrings("No error occurred", std.mem.span(c.notmuch_status_to_string(open_status)));
}
test "can search threads" {
// const allocator = std.testing.allocator;
// const db_path = try std.fs.path.join(
// allocator,
// std.fs.cwd(),
// "mail",
// );
// Current directory under test is root of project
var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
const cwd = try std.fs.cwd().realpath(".", cwd_buf[0..]);
var path_buf: [std.fs.max_path_bytes:0]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(path_buf[0..]);
const db_path = try std.fs.path.joinZ(fba.allocator(), &[_][]const u8{ cwd, "mail" });
{
var status: notmuch.Status = undefined;
var db = try notmuch.Db.open(db_path, &status);
defer db.deinit();
defer db.close();
defer status.deinit();
try std.testing.expectEqualStrings("No error occurred", status.statusString());
var t_iter = try db.searchThreads("Tablets");
defer t_iter.deinit();
var inx: usize = 0;
while (t_iter.next()) |t| : (inx += 1) {
defer t.deinit();
try std.testing.expectEqual(@as(c_int, 1), t.getTotalMessages());
try std.testing.expectEqualStrings("0000000000000001", t.getThreadId());
var message_iter = try t.getMessages();
var jnx: usize = 0;
while (message_iter.next()) |m| : (jnx += 1) {
defer m.deinit();
try std.testing.expectStringEndsWith(m.getFilename(), "/1721591945.R4187135327503631514.nucman:2,S");
}
try std.testing.expectEqual(@as(usize, 1), jnx);
}
try std.testing.expectEqual(@as(usize, 1), inx);
}
// This is the json we're looking to match on api/query/<term>
// [
// {
// "authors": "The Washington Post",
// "matched_messages": 1,
// "newest_date": 1721664948,
// "oldest_date": 1721664948,
// "subject": "Biden is out. What now?",
// "tags": [
// "inbox",
// "unread"
// ],
// "thread_id": "0000000000031723",
// "total_messages": 1
// },
// {
// "authors": "The Washington Post",
// "matched_messages": 1,
// "newest_date": 1721603115,
// "oldest_date": 1721603115,
// "subject": "Upcoming Virtual Programs",
// "tags": [
// "inbox",
// "unread"
// ],
// "thread_id": "0000000000031712",
// "total_messages": 1
// },
// {
// "authors": "The Washington Post",
// "matched_messages": 1,
// "newest_date": 1721590157,
// "oldest_date": 1721590157,
// "subject": "Biden Steps Aside",
// "tags": [
// "inbox"
// ],
// "thread_id": "000000000003170d",
// "total_messages": 1
// }
// ]
//
// And on api/thread/<threadid>
//
// [
// {
// "from": "The Washington Post <email@washingtonpost.com>",
// "to": "elerch@lerch.org",
// "cc": null,
// "bcc": null,
// "date": "Sun, 21 Jul 2024 19:23:38 +0000",
// "subject": "Biden steps aside",
// "content": "...content...",
// "content_type": "text/html",
// "attachments": [],
// "message_id": "01010190d6bfe4e1-185e2720-e415-4086-8865-9604cde886c2-000000@us-west-2.amazonses.com"
// }
// ]
}

199
src/notmuch.zig Normal file
View file

@ -0,0 +1,199 @@
const std = @import("std");
const c = @cImport({
@cInclude("notmuch.h");
});
pub const Status = struct {
err: ?anyerror = null,
status: c.notmuch_status_t = c.NOTMUCH_STATUS_SUCCESS,
msg: ?[*:0]u8 = null,
pub fn deinit(status: *Status) void {
if (status.msg) |m| std.c.free(m);
status.err = undefined;
status.status = c.NOTMUCH_STATUS_SUCCESS;
status.msg = null;
}
pub fn statusString(status: Status) []const u8 {
return std.mem.span(c.notmuch_status_to_string(status.status));
}
};
pub const Db = struct {
handle: *c.notmuch_database_t,
pub fn open(path: [:0]const u8, status: ?*Status) !Db {
var db: ?*c.notmuch_database_t = null;
var err: ?[*:0]u8 = null;
const open_status = c.notmuch_database_open_with_config(
path,
c.NOTMUCH_DATABASE_MODE_READ_ONLY,
"",
null,
&db,
&err,
);
defer if (err) |e| if (status == null) std.c.free(e);
if (open_status != c.NOTMUCH_STATUS_SUCCESS) {
if (status) |s| s.* = .{
.msg = err,
.status = open_status,
.err = error.CouldNotOpenDatabase,
};
return error.CouldNotOpenDatabase;
}
if (db == null) unreachable; // If we opened the database successfully, this should never be null
if (status) |s| s.* = .{};
return .{ .handle = db.? };
}
pub fn close(db: *Db) void {
_ = c.notmuch_database_close(db.handle);
}
pub fn deinit(db: *Db) void {
_ = c.notmuch_database_destroy(db.handle);
db.handle = undefined;
}
//
// Execute a query for threads, returning a notmuch_threads_t object
// which can be used to iterate over the results. The returned threads
// object is owned by the query and as such, will only be valid until
// notmuch_query_destroy.
//
// Typical usage might be:
//
// notmuch_query_t *query;
// notmuch_threads_t *threads;
// notmuch_thread_t *thread;
// notmuch_status_t stat;
//
// query = notmuch_query_create (database, query_string);
//
// for (stat = notmuch_query_search_threads (query, &threads);
// stat == NOTMUCH_STATUS_SUCCESS &&
// notmuch_threads_valid (threads);
// notmuch_threads_move_to_next (threads))
// {
// thread = notmuch_threads_get (threads);
// ....
// notmuch_thread_destroy (thread);
// }
//
// notmuch_query_destroy (query);
//
// Note: If you are finished with a thread before its containing
// query, you can call notmuch_thread_destroy to clean up some memory
// sooner (as in the above example). Otherwise, if your thread objects
// are long-lived, then you don't need to call notmuch_thread_destroy
// and all the memory will still be reclaimed when the query is
// destroyed.
//
// Note that there's no explicit destructor needed for the
// notmuch_threads_t object. (For consistency, we do provide a
// notmuch_threads_destroy function, but there's no good reason
// to call it if the query is about to be destroyed).
pub fn searchThreads(db: Db, query: [:0]const u8) !ThreadIterator {
const nm_query = c.notmuch_query_create(db.handle, query);
if (nm_query == null) return error.CouldNotCreateQuery;
const handle = nm_query.?;
errdefer c.notmuch_query_destroy(handle);
var threads: ?*c.notmuch_threads_t = undefined;
const status = c.notmuch_query_search_threads(handle, &threads);
if (status != c.NOTMUCH_STATUS_SUCCESS) return error.CouldNotSearchThreads;
return .{
.query = handle,
.thread_state = threads orelse return error.CouldNotSearchThreads,
};
}
pub const Message = struct {
message_handle: *c.notmuch_message_t,
pub fn getFilename(self: Message) []const u8 {
return std.mem.span(c.notmuch_message_get_filename(self.message_handle));
}
pub fn deinit(self: Message) void {
c.notmuch_message_destroy(self.message_handle);
}
};
pub const MessageIterator = struct {
messages_state: *c.notmuch_messages_t,
first: bool = true,
pub fn next(self: *MessageIterator) ?Message {
if (!self.first) c.notmuch_messages_move_to_next(self.messages_state);
self.first = false;
if (c.notmuch_messages_valid(self.messages_state) == 0) return null;
const message = c.notmuch_messages_get(self.messages_state) orelse return null;
return .{
.message_handle = message,
};
}
// Docs imply strongly not to bother with deinitialization here
};
pub const Thread = struct {
thread_handle: *c.notmuch_thread_t,
// Get the thread ID of 'thread'.
//
// The returned string belongs to 'thread' and as such, should not be
// modified by the caller and will only be valid for as long as the
// thread is valid, (which is until deinit() or the query from which
// it derived is destroyed).
pub fn getThreadId(self: Thread) []const u8 {
return std.mem.span(c.notmuch_thread_get_thread_id(self.thread_handle));
}
// Get the total number of messages in 'thread'.
//
// This count consists of all messages in the database belonging to
// this thread. Contrast with notmuch_thread_get_matched_messages() .
pub fn getTotalMessages(self: Thread) c_int {
return c.notmuch_thread_get_total_messages(self.thread_handle);
}
// Get the total number of files in 'thread'.
//
// This sums notmuch_message_count_files over all messages in the
// thread
pub fn getTotalFiles(self: Thread) c_int {
return c.notmuch_thread_get_total_files(self.thread_handle);
}
pub fn getMessages(self: Thread) !MessageIterator {
return .{
.messages_state = c.notmuch_thread_get_messages(self.thread_handle) orelse return error.CouldNotGetIterator,
};
}
pub fn deinit(self: Thread) void {
c.notmuch_thread_destroy(self.thread_handle);
// self.thread_handle = undefined;
}
};
pub const ThreadIterator = struct {
query: *c.notmuch_query_t,
thread_state: *c.notmuch_threads_t,
first: bool = true,
pub fn next(self: *ThreadIterator) ?Thread {
if (!self.first) c.notmuch_threads_move_to_next(self.thread_state);
self.first = false;
if (c.notmuch_threads_valid(self.thread_state) == 0) return null;
const thread = c.notmuch_threads_get(self.thread_state) orelse return null;
return .{
.thread_handle = thread,
};
}
pub fn deinit(self: *ThreadIterator) void {
c.notmuch_query_destroy(self.query);
self.query = undefined;
}
};
};