//! 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, ), };