package main // The invented mail the local run shows (issue 1859.5): two accounts, work and personal, and their mails — // the mockup's own invented people and firms (design 63's mockup: Sara Claes, Tim Jacobs, Lena Janssens, the // Harbor demo, Hostco, Telco), at addresses under .example, which no mail server answers. Nothing here is a // real person, firm or provider. The mail-store stand-ins answer in mesh-mail's own shapes (cmd/mesh-mail // tools.go accountsTool, searchTool, readTool; imap.go metadata), copied from that code. import ( "context" "encoding/base64" "encoding/json" "fmt" "sort" "strings" "sync" "time" stdio "git.novox.be/novox/mesh-sdk/go" ) type person struct { Name string `json:"name,omitempty"` Address string `json:"address"` } // devMail is one invented mail. type devMail struct { Account, Folder string UID int MessageID string Date time.Time From person Subject, Text string Unread bool Attachment string // a file name, or none } // devAccounts are the two invented accounts. var devAccounts = []struct{ Name, Kind, Address string }{ {"work", "imap", "op@work.example"}, {"personal", "imap", "op@mail.example"}, } var ( sara = person{"Sara Claes", "sara.claes@harbor.example"} tim = person{"Tim Jacobs", "tim.jacobs@harbor.example"} lena = person{"Lena Janssens", "lena.janssens@hostco.example"} telco = person{"Telco", "noreply@telco.example"} digest = person{"", "digest@lists.example"} // no display name: issue 1850's case ) // world is the invented mailbox and everything the local run keeps, in memory only. type world struct { mu sync.Mutex now func() time.Time mails []*devMail sends map[string]int // a send's id, and how often its state was asked read *reading } // mailAt is a time some hours before the run's start. func hoursBefore(start time.Time, h int) time.Time { return start.Add(-time.Duration(h) * time.Hour) } func newWorld(start time.Time, now func() time.Time) *world { w := &world{now: now, sends: map[string]int{}} uid := 100 add := func(account, folder string, hours int, from person, subject, text string, attachment string) *devMail { uid++ m := &devMail{Account: account, Folder: folder, UID: uid, MessageID: fmt.Sprintf("<%d@%s.example>", uid, account), Date: hoursBefore(start, hours), From: from, Subject: subject, Text: text, Unread: hours < 30, Attachment: attachment} w.mails = append(w.mails, m) return m } m := map[string]*devMail{} m["offer"] = add("work", "INBOX", 2, sara, "The Harbor demo: the signed offer", "Hi,\n\nAttached is the offer for the Harbor demo. Could you sign it and send it back to me today, so legal can file it?\n\nThanks,\nSara", "harbor-offer.pdf") m["quote"] = add("work", "INBOX", 5, tim, "Re: the revised quote", "Hi,\n\nI am working on the revised quote for the Harbor demo and will send it before Friday.\n\nTim", "") m["hostco"] = add("work", "INBOX", 9, lena, "Hostco renewal for next year", "Hello,\n\nYour Hostco plan renews on 1 November. Please approve the renewal by Saturday; the invoice is attached for your finance team.\nThe new contract is on its way by post.\n\nKind regards,\nLena Janssens\nHostco", "hostco-invoice.pdf") m["room"] = add("work", "INBOX", 20, sara, "A room for the demo", "Could you book a room for the Harbor demo, two weeks from now? Twelve people.\n\nSara", "") m["slides"] = add("work", "INBOX", 26, tim, "The demo slides", "Thanks for the call. You said you would send me the demo slides tomorrow — looking forward to them.\n\nTim", "") m["newsletter"] = add("work", "INBOX", 30, person{"Harbor news", "news@harbor.example"}, "Harbor news: the autumn issue", "The autumn issue: how the Harbor team moved its demo lab, and three talks worth watching.", "") m["router"] = add("personal", "INBOX", 4, telco, "Your new router ships today", "Your Telco router left our warehouse today and arrives tomorrow.\n\nTelco", "") m["digest"] = add("work", "INBOX", 12, digest, "Weekly digest", "This week on the list: four threads about release notes, one about a new mirror.", "") m["parcel"] = add("personal", "INBOX", 3, person{"Parcel service", "parcel@deliver-fast.example"}, "Your parcel is held: pay 2 EUR", "Your parcel is held at our depot. Pay the fee of 2 EUR at the link below within 24 hours or it is sent back.", "") m["thanks"] = add("work", "INBOX", 40, lena, "Thanks for Tuesday", "Thanks for the meeting on Tuesday. Nothing more is needed from you for now.\n\nLena", "") m["agenda"] = add("work", "INBOX", 6, sara, "Kick-off agenda", "Here is the agenda for the kick-off. Could you check the time slots and tell me which suits you?\n\nSara", "") m["bill"] = add("personal", "INBOX", 8, person{"Telco billing", "billing@telco.example"}, "Your Telco bill for October", "Your bill for October is 34.50 EUR, due on the 28th.", "telco-october.pdf") m["photos"] = add("personal", "INBOX", 28, lena, "Photos from the weekend", "A few photos from the weekend at the coast, no hurry.\n\nLena", "") m["club"] = add("personal", "INBOX", 50, person{"The book club", "club@readers.example"}, "Book club: next month's book", "Next month we read a short novel; the meeting stays on the first Thursday.", "") m["dentist"] = add("personal", "INBOX", 7, person{"Smile practice", "reception@smile.example"}, "Please confirm your appointment", "Please confirm your appointment of next Tuesday at 9:30 by replying to this mail.", "") // The rest of the inboxes and an archive: mail reading's list does not name them, All mail does. add("work", "INBOX", 60, tim, "Lunch on Friday?", "Lunch on Friday with the Harbor team?", "") add("work", "INBOX", 72, person{"Facilities", "facilities@work.example"}, "The lift is serviced on Monday", "The lift is out of use on Monday morning.", "") add("work", "Archive", 200, sara, "The Harbor demo: first ideas", "First ideas for the demo, for later.", "") add("work", "Archive", 260, lena, "Hostco: your plan", "Your current Hostco plan, for your records.", "") add("work", "Archive", 300, tim, "Old quote", "The first quote, kept for the record.", "") add("personal", "INBOX", 80, person{"Telco", "noreply@telco.example"}, "Your order is confirmed", "Your router order is confirmed.", "") add("personal", "Archive", 400, person{"The book club", "club@readers.example"}, "Book club: last month's notes", "The notes of last month's meeting.", "") add("personal", "Archive", 500, lena, "Old photos", "Some old photos.", "") w.read = newReading(start, now, m) return w } // ---- mail-store's verbs (cmd/mesh-mail/tools.go) -------------------------------------------------------------- func metadataOf(m *devMail) map[string]any { flags := []string{} if !m.Unread { flags = append(flags, `\Seen`) } atts := []map[string]any{} if m.Attachment != "" { atts = append(atts, map[string]any{"bucket": "mail-attachments", "key": "att-" + fmt.Sprint(m.UID), "size": 18432, "type": "application/pdf", "name": m.Attachment}) } return map[string]any{ "account": m.Account, "folder": m.Folder, "uid": m.UID, "uidvalidity": 1, "message-id": m.MessageID, "in-reply-to": "", "date": m.Date.UTC().Format(time.RFC3339), "from": []person{m.From}, "to": []person{{Address: accountAddress(m.Account)}}, "cc": []person{}, "subject": m.Subject, "size": len(m.Text) + 600, "flags": flags, "attachments": atts, } } func accountAddress(name string) string { for _, a := range devAccounts { if a.Name == name { return a.Address } } return "" } func answer(v any) (json.RawMessage, error) { return json.Marshal(v) } func (w *world) accounts(context.Context) (json.RawMessage, error) { var out []map[string]any for _, a := range devAccounts { out = append(out, map[string]any{"account": a.Name, "kind": a.Kind, "address": a.Address, "state": "current", "why": "the local run's own invented mail", "folders": []string{"INBOX", "Archive"}}) } return answer(map[string]any{"accounts": out, "caller": "mesh-mail-web"}) } // bucket answers mesh-mail's account bucket (mirror.go AccountValue): each folder's mail and unread counts. func (w *world) bucket(account string) (*stdio.StateEntry, error) { w.mu.Lock() defer w.mu.Unlock() counts := map[string][2]int{} for _, m := range w.mails { if m.Account == account { c := counts[m.Folder] c[0]++ if m.Unread { c[1]++ } counts[m.Folder] = c } } var folders []map[string]any for _, f := range []string{"INBOX", "Archive"} { folders = append(folders, map[string]any{"name": f, "mails": counts[f][0], "unread": counts[f][1]}) } raw, _ := json.Marshal(map[string]any{"state": "current", "folders": folders}) return &stdio.StateEntry{Key: account, Value: raw}, nil } func text(args map[string]any, key string) string { s, _ := args[key].(string); return s } func (w *world) search(_ context.Context, args map[string]any) (json.RawMessage, error) { w.mu.Lock() defer w.mu.Unlock() limit := 20 if l, ok := args["limit"].(float64); ok { limit = int(l) } query := strings.ToLower(text(args, "query")) var before time.Time if s := text(args, "before"); s != "" { before, _ = time.Parse(time.RFC3339, s) } var found []*devMail for _, m := range w.mails { if a := text(args, "account"); a != "" && m.Account != a { continue } if f := text(args, "folder"); f != "" && m.Folder != f { continue } if !before.IsZero() && !m.Date.Before(before) { continue } hay := strings.ToLower(m.Subject + " " + m.Text + " " + m.From.Name + " " + m.From.Address) if query != "" && !strings.Contains(hay, query) { continue } found = append(found, m) } sort.SliceStable(found, func(i, j int) bool { return found[i].Date.After(found[j].Date) }) if len(found) > limit { found = found[:limit] } out := make([]map[string]any, len(found)) for i, m := range found { out[i] = metadataOf(m) } return answer(map[string]any{"searched": []string{"work", "personal"}, "found": out}) } func (w *world) mailAt(account, folder string, uid int) *devMail { for _, m := range w.mails { if m.Account == account && m.Folder == folder && m.UID == uid { return m } } return nil } func number(args map[string]any, key string) int { switch v := args[key].(type) { case float64: return int(v) case int: return v case int64: return int(v) } return 0 } func (w *world) readMail(_ context.Context, args map[string]any) (json.RawMessage, error) { w.mu.Lock() defer w.mu.Unlock() m := w.mailAt(text(args, "account"), text(args, "folder"), number(args, "uid")) if m == nil { return nil, fmt.Errorf("the account %s holds no message %d in %s", text(args, "account"), number(args, "uid"), text(args, "folder")) } if key := text(args, "attachment"); key != "" { if m.Attachment == "" || key != "att-"+fmt.Sprint(m.UID) { return nil, fmt.Errorf("the message holds no attachment %s", key) } body := "%PDF-1.4\n% an invented attachment of the local run: " + m.Attachment + "\n" return answer(map[string]any{"attachment": map[string]any{"key": key, "name": m.Attachment, "size": len(body), "type": "application/pdf"}, "base64": base64.StdEncoding.EncodeToString([]byte(body))}) } m.Unread = false out := metadataOf(m) out["text"] = m.Text return answer(out) } // send answers a send's id at once: the operator's Approve is the real mesh's, never asked here. func (w *world) send(_ context.Context, args map[string]any) (json.RawMessage, error) { w.mu.Lock() defer w.mu.Unlock() id := fmt.Sprintf("local-send-%d", len(w.sends)+1) w.sends[id] = 0 return answer(map[string]any{"ask": id}) } // sendStatus walks a send to sent: waiting when first asked, sent after that. Nothing is ever sent. func (w *world) sendStatus(_ context.Context, args map[string]any) (json.RawMessage, error) { w.mu.Lock() defer w.mu.Unlock() id := text(args, "id") n, ok := w.sends[id] if !ok { return answer(map[string]any{"status": "unknown", "words": "no send of that id"}) } w.sends[id] = n + 1 if n == 0 { return answer(map[string]any{"status": "waiting", "words": "the operator's Approve (the local run approves it by itself)"}) } return answer(map[string]any{"status": "sent", "words": "the local run sends nothing: no mail left this machine"}) }