//! Demo mail: the labels and threads parcel shows until it syncs with
//! Gmail, ages counted back from when they load.
const std = @import("std");
const Contacts = @import("Contacts.zig");
const Mailbox = @import("Mailbox.zig");
const cards = @import("cards.zig");
const ascii = std.ascii;
const mem = std.mem;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const Io = std.Io;
const Rgb = Mailbox.Rgb;
const Writer = Io.Writer;
/// The labels the threads use: Gmail's system ones, named as Mailbox
/// names them, then the user's.
const L = enum {
inbox,
starred,
sent,
drafts,
spam,
trash,
work,
hiring,
family,
receipts,
travel,
newsletters,
waiting,
someday,
};
/// The user's labels: which, Gmail id, name, and colour as 0xRRGGBB.
const user_labels = [_]struct { L, []const u8, []const u8, u24 }{
.{ .work, "Label_1", "Work", 0x2563eb },
.{ .hiring, "Label_2", "Work/Hiring", 0x7c3aed },
.{ .family, "Label_3", "Family", 0x16a34a },
.{ .receipts, "Label_4", "Receipts", 0xea580c },
.{ .travel, "Label_5", "Travel", 0x0891b2 },
.{ .newsletters, "Label_6", "Newsletters", 0xdb2777 },
.{ .waiting, "Label_7", "Waiting", 0xca8a04 },
.{ .someday, "Label_8", "Someday", 0x64748b },
};
const Sample = struct {
from: []const u8,
subject: []const u8,
snippet: []const u8,
minutes_ago: u32,
messages: u8,
labels: []const L,
unread: bool,
};
const hour = 60;
const day = 24 * hour;
fn t(
from: []const u8,
subject: []const u8,
snippet: []const u8,
minutes_ago: u32,
messages: u8,
labels: []const L,
unread: bool,
) Sample {
return .{
.from = from,
.subject = subject,
.snippet = snippet,
.minutes_ago = minutes_ago,
.messages = messages,
.labels = labels,
.unread = unread,
};
}
/// Adds the labels and threads to `mail`, dated back from `now`, in
/// seconds since the epoch.
/// Made-up messages for a sample thread, so the reader has something to
/// show without Gmail: its people in turn, three hours apart, the newest
/// saying the snippet over a quote of the one before.
pub fn conversation(arena: Allocator, thread: *const Mailbox.Thread) ![]const Mailbox.Message {
var names: std.ArrayList([]const u8) = .empty;
var it = mem.splitSequence(u8, thread.participants, ", ");
while (it.next()) |name| try names.append(arena, name);
const out = try arena.alloc(Mailbox.Message, thread.messages);
for (out, 0..) |*m, i| {
const newest = i + 1 == out.len;
const from = names.items[i % names.items.len];
const said = if (newest)
thread.snippet
else
try arena.print("Message {d} of “{s}”.", .{ i + 1, thread.subject });
var html: Writer.Allocating = .init(arena);
try html.writer.writeAll("
");
try escape(&html.writer, said);
try html.writer.writeAll("
Thanks,
");
try escape(&html.writer, from);
try html.writer.writeAll("
");
if (newest and i > 0) {
try html.writer.writeAll("");
try escape(&html.writer, out[i - 1].snippet);
try html.writer.writeAll("
");
}
const back: i64 = @intCast(out.len - 1 - i);
// An agenda comes with one, to open.
const files: []const Mailbox.Attachment = if (newest and mem.find(u8, thread.subject, "agenda") != null)
try arena.dupe(Mailbox.Attachment, &.{.{
.name = "agenda.txt",
.mime = "text/plain",
.content_id = "",
.id = "",
.data = try arena.print("{s}\n\n{s}\n", .{ thread.subject, thread.snippet }),
.inline_shown = false,
}})
else
&.{};
m.* = .{
.id = try arena.print("{s}.{d}", .{ thread.id, i }),
.from = from,
.address = if (mem.eql(u8, from, "me")) "me@example.com" else try arena.print("{s}@example.com", .{from}),
.date = thread.date - back * 3 * 3600,
.html = html.written(),
.text = try arena.print("{s}\n\nThanks,\n{s}", .{ said, from }),
.snippet = said,
.attachments = files,
.unread = newest and thread.unread,
.message_id = try arena.print("<{s}.{d}@sample>", .{ thread.id, i }),
.references = "",
.reply_to = if (mem.eql(u8, from, "me"))
"me@example.com"
else
try arena.print("{s} <{s}@example.com>", .{ from, from }),
.to = "me@example.com",
.cc = "",
.sent = mem.eql(u8, from, "me"),
// A newsletter can be left by mail.
.unsubscribe = if (mem.eql(u8, from, "Zig Weekly"))
", "
else
"",
.one_click = false,
// GitHub's mail links its page, and lunch comes with an invitation.
.action = if (mem.eql(u8, from, "GitHub"))
.{ .name = "View workflow run", .url = "https://github.com/rockorager/siv/actions" }
else
null,
.invite = if (i == 0 and mem.eql(u8, thread.subject, "Lunch tomorrow"))
try cards.invite(arena, try lunch(arena, thread.date))
else
null,
};
}
return out;
}
/// An invitation to lunch at noon the day after `date`.
fn lunch(arena: Allocator, date: i64) ![]const u8 {
const epoch = std.time.epoch;
const seconds: epoch.EpochSeconds = .{ .secs = @intCast(@max(date + 24 * 3600, 0)) };
const year_day = seconds.getEpochDay().calculateYearDay();
const month_day = year_day.calculateMonthDay();
const ymd = try arena.print("{d:0>4}{d:0>2}{d:0>2}", .{
year_day.year,
@backingInt(month_day.month),
month_day.day_index + 1,
});
return arena.print(
"BEGIN:VCALENDAR\r\nMETHOD:REQUEST\r\nBEGIN:VEVENT\r\nSUMMARY:Lunch\r\n" ++
"DTSTART;TZID=America/Chicago:{s}T120000\r\nDTEND;TZID=America/Chicago:{s}T130000\r\n" ++
"LOCATION:Taco place on Lake St\r\nORGANIZER;CN=Sam Torres:mailto:sam@example.com\r\n" ++
"END:VEVENT\r\nEND:VCALENDAR\r\n",
.{ ymd, ymd },
);
}
fn escape(w: *Writer, text: []const u8) !void {
for (text) |b| try switch (b) {
'&' => w.writeAll("&"),
'<' => w.writeAll("<"),
'>' => w.writeAll(">"),
else => w.writeByte(b),
};
}
/// The user, on the threads' messages.
const me: Mailbox.Person = .{ .name = "", .address = "me@example.com" };
pub fn load(mail: *Mailbox, now: i64) !void {
var map: std.EnumArray(L, Mailbox.Label) = .initUndefined();
inline for (.{ .inbox, .starred, .sent, .drafts, .spam, .trash }) |system| map.set(system, system);
for (user_labels) |u| {
const rgb: Rgb = .{ .r = @truncate(u[3] >> 16), .g = @truncate(u[3] >> 8), .b = @truncate(u[3]) };
map.set(u[0], try mail.addLabel(.{ .id = u[1], .name = u[2], .kind = .user, .color = rgb }));
}
for (threads, 0..) |s, i| {
var labels: [user_labels.len + 6]Mailbox.Label = undefined;
for (s.labels, 0..) |l, k| labels[k] = map.get(l);
var id_buf: [16]u8 = undefined;
var people_buf: [64]u8 = undefined;
// A thread of more than one message is the sender and the replies.
const first_name = s.from[0 .. mem.findScalar(u8, s.from, ' ') orelse s.from.len];
const participants = if (s.messages == 1) s.from else try mem.print(&people_buf, "{s}, me", .{first_name});
// The sender wrote to the user, who answered three hours before
// where a thread has more than one message.
const date = now - @as(i64, s.minutes_ago) * 60;
var address_buf: [96]u8 = undefined;
const sender: Mailbox.Person = .{ .name = s.from, .address = try address(&address_buf, s.from) };
const notes = [_]Mailbox.NewNote{
.{ .date = date - 3 * 3600, .from = me, .to = &.{sender}, .cc = &.{} },
.{ .date = date, .from = sender, .to = &.{me}, .cc = &.{} },
};
_ = try mail.addThread(.{
.id = try mem.print(&id_buf, "sample-{d}", .{i}),
.participants = participants,
.subject = s.subject,
.snippet = s.snippet,
.date = date,
.messages = s.messages,
.labels = labels[0..s.labels.len],
.unread = s.unread,
.notes = if (s.messages > 1) ¬es else notes[1..],
});
}
}
/// Tells `contacts` who is on the threads, as `load` notes them.
pub fn people(contacts: *Contacts, now: i64) !void {
for (threads) |s| {
var address_buf: [96]u8 = undefined;
var buf: [192]u8 = undefined;
const entry = try mem.print(&buf, "{s} <{s}>", .{ s.from, try address(&address_buf, s.from) });
const ms = (now - @as(i64, s.minutes_ago) * 60) * 1000;
try contacts.note(.heard, entry, ms);
if (s.messages > 1) try contacts.note(.wrote, entry, ms - 3 * 3600 * 1000);
}
}
/// An address made from a name: Maya Okafor is maya.okafor@example.com.
fn address(buf: *[96]u8, name: []const u8) ![]const u8 {
var w: Writer = .fixed(buf);
var dot = false;
for (name) |c| {
if (c == ' ') dot = true;
if (!ascii.isAlphanumeric(c)) continue;
if (dot) try w.writeByte('.');
dot = false;
try w.writeByte(ascii.toLower(c));
}
try w.writeAll("@example.com");
return w.buffered();
}
const threads = [_]Sample{
t(
"Maya Okafor",
"Offsite agenda, final",
"Moved the roadmap session to Thursday morning so Ravi can join from Lisbon.",
7,
4,
&.{ .inbox, .work, .starred },
true,
),
t(
"GitHub",
"[siv] CI failed on main",
"The test-render-vulkan job failed after 14m 02s: validation error in vkQueueSubmit2.",
22,
1,
&.{ .inbox, .work },
true,
),
t(
"Mom",
"Sunday dinner?",
"Your dad is making the lamb again. Bring the kids, and the good bread if you pass the bakery.",
41,
3,
&.{ .inbox, .family },
true,
),
t(
"Priya Raman",
"Candidate packet: Jonas Weber",
"Attached his take-home and the panel notes. Strong on systems, quiet in the design round.",
1 * hour + 10,
2,
&.{ .inbox, .work, .hiring },
true,
),
t(
"Delta",
"Your trip to Lisbon is confirmed",
"Confirmation HX7K2P. Departs Tue 6:05 PM from MSP, arrives Wed 10:40 AM.",
2 * hour,
1,
&.{ .inbox, .travel, .receipts },
false,
),
t(
"Ravi Menon",
"Re: render thread priorities",
"I tried SCHED_FIFO for the compositor thread and frame pacing got worse, not better.",
3 * hour,
6,
&.{ .inbox, .work },
false,
),
t(
"Zig Weekly",
"Issue 212: the 0.17 build system",
"This week: addPassthruArgs, cache poisoning, and why @cImport had to go.",
5 * hour,
1,
&.{ .inbox, .newsletters },
true,
),
t(
"Sam Torres",
"Lunch tomorrow",
"Taco place on Lake St at noon? I'll grab a table if I'm early.",
6 * hour,
2,
&.{.inbox},
false,
),
t(
"Stripe",
"Receipt from Fastmail",
"Amount paid $60.00. Standard plan, 12 months. Card ending 4242.",
9 * hour,
1,
&.{.receipts},
false,
),
t(
"Lena Fischer",
"Re: offer letter",
"Thank you! I've signed and sent it back. Looking forward to starting on the 1st.",
1 * day,
5,
&.{ .inbox, .work, .hiring, .starred },
false,
),
t(
"Dad",
"Photos from the lake",
"Finally got these off the camera. The one of Ada with the fish is my favorite.",
1 * day + 2 * hour,
1,
&.{ .family, .starred },
false,
),
t(
"Hotel Avenida",
"Your reservation request",
"We are pleased to confirm 3 nights, arriving 12 Nov. Breakfast is included.",
1 * day + 5 * hour,
2,
&.{ .inbox, .travel },
false,
),
t(
"Jonas Weber",
"Question about the take-home",
"Should the parser stream, or is it fine to read the whole file first?",
1 * day + 8 * hour,
3,
&.{ .work, .hiring, .waiting },
false,
),
t(
"Linear",
"3 issues assigned to you",
"PAR-12 Message list virtualization, PAR-14 Label picker, PAR-15 Undo archive.",
2 * day,
1,
&.{ .inbox, .work },
true,
),
t(
"Maya Okafor",
"Budget numbers for Q1",
"Can you sanity check the infra line? It looks high but I might be double counting.",
2 * day + 3 * hour,
4,
&.{ .work, .waiting },
false,
),
t(
"Amazon",
"Your order has shipped",
"Keychron Q1 Pro will arrive Friday. Track your package.",
2 * day + 6 * hour,
1,
&.{.receipts},
false,
),
t(
"Ada's school",
"Field trip permission slip",
"Please sign and return by Friday. The trip to the science museum is on the 14th.",
3 * day,
1,
&.{ .inbox, .family, .waiting },
true,
),
t(
"This Week in Rust",
"Issue 571",
"Rust 1.92 ships async closures in traits, plus a look at the new trait solver.",
3 * day + 4 * hour,
1,
&.{.newsletters},
false,
),
t(
"Ravi Menon",
"Lisbon dinner spots",
"Cervejaria Ramiro is a must. Book ahead or come at 5 and wait with everyone else.",
4 * day,
2,
&.{ .travel, .starred },
false,
),
t(
"Ana Lima",
"Re: siv accessibility review",
"Orca reads the sidebar fine, but the table headers announce twice. Notes inline.",
4 * day + 2 * hour,
7,
&.{ .work, .starred },
false,
),
t(
"Fastmail",
"Your storage is 80% full",
"You're using 24 GB of 30 GB. Upgrade or clear out old messages.",
5 * day,
1,
&.{.inbox},
false,
),
t(
"Grandma",
"Recipe you asked for",
"Two cups flour, one of sugar, a pinch of salt, and the secret is the browned butter.",
6 * day,
2,
&.{ .family, .someday },
false,
),
t(
"Priya Raman",
"Panel schedule next week",
"Can you take the systems round on Tuesday at 2? Two candidates back to back.",
6 * day + 5 * hour,
3,
&.{ .work, .hiring },
false,
),
t(
"Kickstarter",
"A project you backed has an update",
"Production is two weeks behind, but the first units ship in March.",
8 * day,
1,
&.{ .newsletters, .someday },
false,
),
t(
"Sam Torres",
"Climbing Saturday?",
"The new gym in Northeast has a bouldering wall. Want to try it?",
9 * day,
4,
&.{.someday},
false,
),
t(
"City of Minneapolis",
"Water bill available",
"Your bill of $84.12 is due on the 28th. Pay online or by phone.",
10 * day,
1,
&.{ .receipts, .waiting },
false,
),
t(
"Mom",
"Thanksgiving plans",
"We're doing it at your aunt's this year. Can you bring pie? Two, maybe three.",
11 * day,
9,
&.{.family},
false,
),
t(
"Lobste.rs Digest",
"Top stories this week",
"A tour of io_uring, Zig's new std.Io, and why your terminal is slow.",
12 * day,
1,
&.{.newsletters},
false,
),
t(
"Maya Okafor",
"Re: performance reviews",
"Drafts are due on the 30th. I'll send the template this week.",
14 * day,
3,
&.{.work},
false,
),
t(
"Unknown sender",
"You have won a gift card",
"Claim your $500 reward now. Click the link below before it expires.",
15 * day,
1,
&.{.spam},
true,
),
t(
"Me",
"Notes to self: parcel",
"Keyboard first. Every action has a key and a click. Labels are first class.",
2 * hour,
1,
&.{ .sent, .starred, .someday },
false,
),
t("Me", "Re: Lunch tomorrow", "Sounds good, see you at noon.", 5 * hour, 2, &.{.sent}, false),
t(
"Me",
"Draft: reply to Ana",
"Thanks for the review. On the double announcement, I think the header cell",
30,
1,
&.{.drafts},
false,
),
};