Add mesh-mail-notifier, the default holder of mail-notifier (issue 800 part B)

ADR 0378 §9 as amended by ADR 0379: a module of its own, built from mesh-mail's
event shape, runs as its own account to hear mesh-mail's restricted MAIL stream
(ADR 0376 §2), shows each new inbox mail through node-notifier.send at most
once, folds more than three from one account into one notice, and emits its
own shown event.
This commit is contained in:
jochen
2026-10-15 17:33:15 +02:00
parent 6c10288f0b
commit dc89995d96
9 changed files with 808 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
# mesh-mail-notifier — the default holder of mail-notifier
Built from mesh-mail's source (novox/hq ADR 0378 §9, as amended by ADR 0379, issue 800 part B): it carries a
byte-identical copy of mesh-mail's event shape (`mailevent.go`, pinned by `copies_test.go`, the way
`modules/networkmanager`'s `uplink-tools` carries `ifwatchd`'s own file, ADR 0241). Nothing here names
Betterbird (ADR 0379 §3), and nothing here reaches a mailbox: it only hears what mesh-mail already said.
## What it does
It claims `mail-notifier` (held once per machine, needing `node-notifier` there) and shows each new inbox mail
on this machine's notifier, through `node-notifier.send`, at most once:
- quiet during a first sync or a resync (mesh-mail already says nothing then, `imap.go` ~387)
- quiet for a mail from the account's own address (its address, from `mail-store.accounts`; not known, the
notice is shown)
- quiet for a mail already marked read (`\Seen` in its flags)
- quiet for anything outside the inbox
- more than three new mails from one account arriving close together become one notice naming the count and
the latest sender, rather than one notice each
Each notice shown emits this module's own event `shown` (account and message id, never sender or subject), and
is kept in its own state (`shown`) so a redelivered event is not shown twice.
## Where it runs
**Its own account** (`runs-as`), because it hears mesh-mail's restricted stream `MAIL` (novox/hq ADR 0376 §2):
the machine's runtime runs as the operator's account, which every agent shares and which never hears a
restricted stream. Held once per machine (wherever `mail-notifier` is assigned); the operator grants it the
accounts it announces, in mesh-mail's trusted setting `grants` (`mail-store`'s stream).
## Tools
| tool | does |
|---|---|
| `mail-notifier.status` | the accounts it announces on this machine, and when it last showed a notice; its rules. Read-only |
@@ -0,0 +1,31 @@
package main
// mesh-mail-notifier carries a byte-identical copy of mesh-mail's event shape (novox/hq ADR 0378 §9, issue 800
// part B), the way modules/networkmanager's uplink-tools carries ifwatchd's own file (ADR 0241, copies_test.go):
// each builds alone, so each holds its own copy, and this test holds them to one text.
import (
"bytes"
"os"
"path/filepath"
"testing"
)
// source is where mesh-mail's own copy of the file lives.
var source = filepath.Join("..", "..", "..", "mesh-mail", "cmd", "mesh-mail")
func TestCarriesTheSameMailEventAsMeshMail(t *testing.T) {
for _, f := range []string{"mailevent.go"} {
mine, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
theirs, err := os.ReadFile(filepath.Join(source, f))
if err != nil {
t.Fatalf("mesh-mail's copy of %s could not be read: %v", f, err)
}
if !bytes.Equal(mine, theirs) {
t.Errorf("this copy of %s differs from mesh-mail's: change both together", f)
}
}
}
@@ -0,0 +1,13 @@
package main
// The shape of a mail-store message event (novox/hq ADR 0378 §2, issue 800 part A), moved here on its own so
// mesh-mail-notifier (issue 800 part B) carries a byte-identical copy, the way modules/networkmanager's
// uplink-tools carries ifwatchd's own file (novox/hq ADR 0241, copies_test.go). Nothing here reaches a mailbox or
// a server; it only says where an account's change is told on the bus and what the three changes are called.
// EventKey is where an account's change is said: mail.<account>.message.<added|changed|removed> (research 049
// document 02 §5), the account by its name in the setting, never its address.
func EventKey(account, change string) string { return "mail." + account + ".message." + change }
// Changes are the three things said of a message.
var Changes = []string{"added", "changed", "removed"}
@@ -0,0 +1,167 @@
// mesh-mail-notifier: the default claimant of mail-notifier, built from mesh-mail's own event shape (novox/hq
// ADR 0378 §9, issue 800 part B). A Go bundle the node's runtime launches, its own account (runs-as, ADR 0376
// §2): the base invocation serves mail-notifier.status; a separate process (`notify`) hears mesh-mail's added
// events and shows them. What this module says, it says on stderr; stdout is the MCP channel.
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
// AddedPattern is the mesh-mail stream's event this module consumes (its own `consumes` entry, novox/hq ADR
// 0363, ADR 0378 §9): a new, changed or removed message's `added` only.
const AddedPattern = "mesh-mail.mail.*.message.added"
// StatusTool is the mail-notifier seat's one verb this module serves.
const StatusTool = "mail-notifier.status"
// ShownEvent is this module's own event (its `emits`), one per notice shown.
const ShownEvent = "shown"
func main() {
n := &Notifier{
Send: func(args map[string]any) error {
_, err := stdio.Ask("seat:node-notifier.send", args)
return err
},
Accounts: readAccounts,
Store: busStore{},
EmitShown: func(account, messageID string) error {
return stdio.Emit(ShownEvent, map[string]any{"account": account, "message-id": messageID})
},
Logf: logf,
}
n.init()
if len(os.Args) > 1 && os.Args[1] == "notify" {
go watch(n)
if err := stdio.Serve("", nil); err != nil {
logf("%v", err)
os.Exit(1)
}
return
}
tools := []stdio.Tool{{Name: StatusTool,
Description: "The accounts mesh-mail-notifier announces on this machine, and when it last showed a " +
"notice of new mail (novox/hq ADR 0378 §7). Read-only.",
Run: func(map[string]any) (any, error) { return n.Status() }}}
if err := stdio.Serve("", tools); err != nil {
logf("%v", err)
os.Exit(1)
}
}
// watch hears mesh-mail's added events and flushes what it buffered once each account has been quiet a
// moment (ADR 0378 §9: folding a burst into one notice).
func watch(n *Notifier) {
time.Sleep(500 * time.Millisecond) // Serve first: the bus is reached through it
err := stdio.Subscribe(AddedPattern, func(e stdio.Envelope) error {
var body EventBody
if err := json.Unmarshal(e.Body, &body); err != nil {
logf("[mesh-mail-notifier] an added event could not be read; taken and dropped: %v", err)
return nil
}
return n.Add(body)
})
if err != nil {
logf("[mesh-mail-notifier] could not hear %s: %v", AddedPattern, err)
os.Exit(1)
}
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
if err := n.Flush(); err != nil {
logf("[mesh-mail-notifier] a buffered notice could not be shown: %v", err)
}
}
}
// readAccounts asks mail-store.accounts for the accounts this module is granted, by name to own address.
func readAccounts() (map[string]string, error) {
raw, err := stdio.Ask("seat:mail-store.accounts", map[string]any{})
if err != nil {
return nil, err
}
var answer struct {
Accounts []struct {
Account string `json:"account"`
Address string `json:"address"`
} `json:"accounts"`
}
if err := json.Unmarshal(raw, &answer); err != nil {
return nil, err
}
out := map[string]string{}
for _, a := range answer.Accounts {
out[a.Account] = a.Address
}
return out, nil
}
// busStore keeps the notifier's own state (the `shown` bucket the manifest declares): which messages were
// shown already, and each account's last notice.
type busStore struct{}
func stateKey(parts ...string) string {
words := make([]string, len(parts))
for i, p := range parts {
var b strings.Builder
for _, r := range p {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-':
b.WriteRune(r)
default:
b.WriteRune('_')
}
}
words[i] = b.String()
}
return strings.Join(words, "_")
}
func (busStore) HandleSeen(handle string) (bool, error) {
e, err := stdio.State("shown").Get("handle_" + stateKey(strings.Split(handle, "\x1f")...))
return e != nil, err
}
func (busStore) MarkHandle(handle string) error {
_, err := stdio.State("shown").Put("handle_"+stateKey(strings.Split(handle, "\x1f")...), true)
return err
}
func (busStore) PutStatus(account string, s AccountStatus) error {
_, err := stdio.State("shown").Put("status_"+stateKey(account), s)
return err
}
func (busStore) Statuses() (map[string]AccountStatus, error) {
keys, err := stdio.State("shown").Keys()
if err != nil {
return nil, err
}
out := map[string]AccountStatus{}
for _, k := range keys {
account, ok := strings.CutPrefix(k, "status_")
if !ok {
continue
}
e, err := stdio.State("shown").Get(k)
if err != nil || e == nil {
continue
}
var s AccountStatus
if json.Unmarshal(e.Value, &s) == nil {
out[account] = s
}
}
return out, nil
}
@@ -0,0 +1,307 @@
package main
// mesh-mail-notifier: the default holder of mail-notifier (novox/hq ADR 0378 §7, as amended by ADR 0379 §4,
// issue 800 part B). It hears mesh-mail's added events on its own account (runs-as, ADR 0376 §2) and shows each
// new inbox mail at most once, through node-notifier.send: quiet during a first sync (mesh-mail already says
// nothing then, imap.go ~387), for mail from the account's own address, for mail already marked read (\Seen),
// and for anything outside the inbox. More than three new mails from one account arriving close together are
// folded into one notice naming the count and the latest sender, rather than shown one by one.
//
// Nothing here names Betterbird (ADR 0379 §3).
import (
"strconv"
"strings"
"sync"
"time"
)
// Inbox is the one folder a new mail is shown for.
const Inbox = "INBOX"
// Seen is the flag that means a mail was already read: shown for nothing it marks.
const SeenFlag = "\\Seen"
// FoldOver is how many new mails from one account, arriving close together, are still shown one by one; one
// more than that and they become a single notice (novox/hq ADR 0378 §9: "more than three").
const FoldOver = 3
// BurstWindow is how long the notifier waits, after the last qualifying mail of an account, before it decides
// whether to show what it buffered one by one or as a single notice. Well under the minute the decision is
// judged over (ADR 0378 §9), so a burst within that minute is still buffered together.
const BurstWindow = 5 * time.Second
// Person is a sender or a recipient, as mesh-mail's event says it.
type Person struct {
Name string `json:"name,omitempty"`
Address string `json:"address"`
}
// EventBody is what mesh-mail's added event says of a message (metadata only; mailevent.go names where it is
// said). Unknown fields are ignored: the notifier reads only what it needs.
type EventBody struct {
Account string `json:"account"`
Folder string `json:"folder"`
UID uint32 `json:"uid"`
MessageID string `json:"message-id"`
Subject string `json:"subject"`
From []Person `json:"from"`
Flags []string `json:"flags"`
}
// Handle names one message, stably, for the dedupe state (novox/hq ADR 0378 §9: "a redelivered event is not
// shown twice").
func (e EventBody) Handle() string {
return e.Account + "\x1f" + e.Folder + "\x1f" + strconv.FormatUint(uint64(e.UID), 10)
}
func (e EventBody) hasFlag(flag string) bool {
for _, f := range e.Flags {
if strings.EqualFold(f, flag) {
return true
}
}
return false
}
// sender is how a message's first sender reads in a notice: its name and address, or its address alone.
func (e EventBody) sender() string {
if len(e.From) == 0 {
return "(unknown sender)"
}
p := e.From[0]
if p.Name == "" {
return p.Address
}
return p.Name + " <" + p.Address + ">"
}
func (e EventBody) fromAddress() string {
if len(e.From) == 0 {
return ""
}
return e.From[0].Address
}
// Notice is one notification the notifier shows: its args for node-notifier.send, and the message its `shown`
// event names (the latest one, for a folded notice).
type Notice struct {
Account string
Args map[string]any
Handles []string // every message this notice accounts for, marked seen once shown
Latest EventBody
}
// Store is what the notifier keeps of its own (the `shown` state the manifest declares): whether a message
// was shown already, and each account's last notice, for its status tool.
type Store interface {
HandleSeen(handle string) (bool, error)
MarkHandle(handle string) error
PutStatus(account string, s AccountStatus) error
Statuses() (map[string]AccountStatus, error)
}
// AccountStatus is what the notifier last showed of one account.
type AccountStatus struct {
At time.Time `json:"at"`
Summary string `json:"summary"`
}
// Notifier buffers qualifying mail per account and decides, once no more of that account's mail arrives for a
// moment, how to show what it buffered.
type Notifier struct {
// Send calls node-notifier.send on this machine.
Send func(args map[string]any) error
// Accounts reads the accounts the notifier is granted, by mail-store.accounts: each one's own address, for
// the quiet rule (novox/hq ADR 0378 §9's LEAD note). An account not known there is shown anyway.
Accounts func() (map[string]string, error)
Store Store
// EmitShown says mesh-mail-notifier's own event `shown`: account and message id, never sender or subject.
EmitShown func(account, messageID string) error
Now func() time.Time
Logf func(string, ...any)
Debounce time.Duration
mu sync.Mutex
pending map[string][]EventBody
last map[string]time.Time
}
func (n *Notifier) init() {
n.mu.Lock()
defer n.mu.Unlock()
if n.pending == nil {
n.pending = map[string][]EventBody{}
n.last = map[string]time.Time{}
}
if n.Now == nil {
n.Now = time.Now
}
if n.Logf == nil {
n.Logf = func(string, ...any) {}
}
if n.Debounce == 0 {
n.Debounce = BurstWindow
}
}
// Add takes one added event. It returns an error only to have the event offered again (the handler
// Subscribe wraps this in follows that rule).
func (n *Notifier) Add(e EventBody) error {
n.init()
seen, err := n.Store.HandleSeen(e.Handle())
if err != nil {
return err
}
if seen {
return nil // a redelivery of a message already shown (or already judged not to be)
}
own, known, err := n.ownAddress(e.Account)
if err != nil {
return err
}
if !n.qualifies(e, own, known) {
return nil
}
n.mu.Lock()
n.pending[e.Account] = append(n.pending[e.Account], e)
n.last[e.Account] = n.Now()
n.mu.Unlock()
return nil
}
// qualifies says whether a mail is shown at all: a new inbox mail, not the account's own, not already read.
func (n *Notifier) qualifies(e EventBody, ownAddress string, ownKnown bool) bool {
if !strings.EqualFold(e.Folder, Inbox) {
return false
}
if e.hasFlag(SeenFlag) {
return false
}
if ownKnown && ownAddress != "" && strings.EqualFold(e.fromAddress(), ownAddress) {
return false
}
return true
}
func (n *Notifier) ownAddress(account string) (string, bool, error) {
accounts, err := n.Accounts()
if err != nil {
// Not known: ADR 0378 §9's LEAD note says the notice is shown when the address cannot be read. Logged,
// not retried: mail-store being unreachable is not this event's fault.
n.Logf("[mesh-mail-notifier] the account's own address could not be read (%v); the notice is shown", err)
return "", false, nil
}
addr, ok := accounts[account]
return addr, ok, nil
}
// Flush decides every account whose buffer has been quiet for Debounce: one notice each mail it holds, or
// one notice folding them all when there are more than FoldOver.
func (n *Notifier) Flush() error {
n.init()
now := n.Now()
n.mu.Lock()
var ready []string
for account, at := range n.last {
if now.Sub(at) >= n.Debounce {
ready = append(ready, account)
}
}
batches := map[string][]EventBody{}
for _, account := range ready {
batches[account] = n.pending[account]
delete(n.pending, account)
delete(n.last, account)
}
n.mu.Unlock()
for account, events := range batches {
if err := n.decide(account, events); err != nil {
return err
}
}
return nil
}
// decide shows what one account buffered, and emits `shown` for the notice.
func (n *Notifier) decide(account string, events []EventBody) error {
if len(events) == 0 {
return nil
}
latest := events[len(events)-1]
var notice Notice
if len(events) > FoldOver {
notice = Notice{
Account: account,
Args: map[string]any{
"summary": strconv.Itoa(len(events)) + " new mails",
"body": "latest from " + latest.sender() + " — " + account,
"app_name": "mesh-mail",
},
Latest: latest,
}
for _, e := range events {
notice.Handles = append(notice.Handles, e.Handle())
}
if err := n.show(notice); err != nil {
return err
}
return nil
}
for _, e := range events {
notice := Notice{
Account: account,
Args: map[string]any{
"summary": e.Subject,
"body": e.sender() + " — " + account,
"app_name": "mesh-mail",
},
Handles: []string{e.Handle()},
Latest: e,
}
if err := n.show(notice); err != nil {
return err
}
}
return nil
}
// show sends one notice, marks every message it accounts for seen, keeps the account's status, and emits
// `shown`.
func (n *Notifier) show(notice Notice) error {
if err := n.Send(notice.Args); err != nil {
n.Logf("[mesh-mail-notifier] %s's notice could not be shown: %v", notice.Account, err)
return err
}
for _, h := range notice.Handles {
if err := n.Store.MarkHandle(h); err != nil {
n.Logf("[mesh-mail-notifier] shown, but not kept as shown (%v): a redelivery may show it again", err)
}
}
summary, _ := notice.Args["summary"].(string)
if err := n.Store.PutStatus(notice.Account, AccountStatus{At: n.Now(), Summary: summary}); err != nil {
n.Logf("[mesh-mail-notifier] shown, but the status could not be kept: %v", err)
}
return n.EmitShown(notice.Account, notice.Latest.MessageID)
}
// Status is the notifier's account of itself, for mail-notifier.status (novox/hq ADR 0378 §7: "accounts it
// announces there, when it last showed a notice").
func (n *Notifier) Status() (map[string]any, error) {
statuses, err := n.Store.Statuses()
if err != nil {
return nil, err
}
accounts := map[string]any{}
for account, s := range statuses {
accounts[account] = map[string]any{"at": s.At.UTC().Format(time.RFC3339), "shown": s.Summary}
}
return map[string]any{
"accounts": accounts,
"rules": []string{
"it shows each new inbox mail at most once, through node-notifier.send on this machine (novox/hq ADR 0378 §7)",
"quiet during a first sync, for mail from the account's own address, for mail already marked read, and for anything outside the inbox",
"more than three new mails from one account arriving close together become one notice naming the count and the latest sender",
},
}, nil
}
@@ -0,0 +1,169 @@
package main
import (
"strings"
"sync"
"testing"
"time"
)
// fakeStore is Store kept in memory, for the test alone.
type fakeStore struct {
mu sync.Mutex
handles map[string]bool
statuses map[string]AccountStatus
}
func newFakeStore() *fakeStore {
return &fakeStore{handles: map[string]bool{}, statuses: map[string]AccountStatus{}}
}
func (s *fakeStore) HandleSeen(handle string) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.handles[handle], nil
}
func (s *fakeStore) MarkHandle(handle string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.handles[handle] = true
return nil
}
func (s *fakeStore) PutStatus(account string, st AccountStatus) error {
s.mu.Lock()
defer s.mu.Unlock()
s.statuses[account] = st
return nil
}
func (s *fakeStore) Statuses() (map[string]AccountStatus, error) {
s.mu.Lock()
defer s.mu.Unlock()
out := map[string]AccountStatus{}
for k, v := range s.statuses {
out[k] = v
}
return out, nil
}
// fakeClock is a clock the test moves by hand, so Flush's debounce needs no real sleep.
type fakeClock struct {
mu sync.Mutex
now time.Time
}
func (c *fakeClock) Now() time.Time {
c.mu.Lock()
defer c.mu.Unlock()
return c.now
}
func (c *fakeClock) Advance(d time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.now = c.now.Add(d)
}
// TestFirstMailOfAnAccountIsShownOnceEachAndABurstIsFolded is the issue's first test (novox/hq ADR 0378 §9,
// issue 800 part B): given a new inbox mail from another sender, one already read, one from the account's own
// address, one in another folder, and five within a minute from one account, it sends exactly one notice,
// nothing, nothing, nothing, and one notice folding the five and naming the latest sender; it fails today
// because the module does not exist.
func TestFirstMailOfAnAccountIsShownOnceEachAndABurstIsFolded(t *testing.T) {
clock := &fakeClock{now: time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC)}
store := newFakeStore()
var mu sync.Mutex
var sent []map[string]any
var shown []struct{ account, id string }
n := &Notifier{
Now: clock.Now,
Send: func(args map[string]any) error {
mu.Lock()
defer mu.Unlock()
sent = append(sent, args)
return nil
},
Accounts: func() (map[string]string, error) {
return map[string]string{"work": "me@work.example", "home": "me@home.example"}, nil
},
Store: store,
EmitShown: func(account, messageID string) error {
mu.Lock()
defer mu.Unlock()
shown = append(shown, struct{ account, id string }{account, messageID})
return nil
},
Logf: t.Logf,
}
// 1. A new inbox mail from another sender: shown.
one := EventBody{Account: "work", Folder: "INBOX", UID: 1, MessageID: "m1", Subject: "Hi",
From: []Person{{Name: "Alice", Address: "alice@example.com"}}}
// 2. Already read.
two := EventBody{Account: "work", Folder: "INBOX", UID: 2, MessageID: "m2", Subject: "Old",
From: []Person{{Address: "bob@example.com"}}, Flags: []string{"\\Seen"}}
// 3. From the account's own address.
three := EventBody{Account: "work", Folder: "INBOX", UID: 3, MessageID: "m3", Subject: "Sent",
From: []Person{{Address: "me@work.example"}}}
// 4. In another folder.
four := EventBody{Account: "work", Folder: "Archive", UID: 4, MessageID: "m4", Subject: "Filed",
From: []Person{{Address: "carol@example.com"}}}
for _, e := range []EventBody{one, two, three, four} {
if err := n.Add(e); err != nil {
t.Fatalf("Add(%+v): %v", e, err)
}
}
// 5. Five new inbox mails from another account within a minute.
var burst []EventBody
for i := 1; i <= 5; i++ {
e := EventBody{Account: "home", Folder: "INBOX", UID: uint32(i), MessageID: "burst" + string(rune('0'+i)),
Subject: "Mail", From: []Person{{Name: "Sender " + string(rune('0'+i)), Address: "s@example.com"}}}
burst = append(burst, e)
clock.Advance(time.Second)
if err := n.Add(e); err != nil {
t.Fatalf("Add(%+v): %v", e, err)
}
}
latest := burst[len(burst)-1]
clock.Advance(BurstWindow + time.Second)
if err := n.Flush(); err != nil {
t.Fatalf("Flush: %v", err)
}
if len(sent) != 2 {
t.Fatalf("sent %d notice(s): %+v", len(sent), sent)
}
if sent[0]["summary"] != "Hi" || !strings.Contains(sent[0]["body"].(string), "Alice") {
t.Errorf("the first notice is %+v, want Alice's mail Hi", sent[0])
}
if sent[1]["summary"] != "5 new mails" || !strings.Contains(sent[1]["body"].(string), latest.sender()) {
t.Errorf("the folded notice is %+v, want 5 new mails naming %s", sent[1], latest.sender())
}
if len(shown) != 2 {
t.Fatalf("emitted shown %d time(s): %+v", len(shown), shown)
}
if shown[0].account != "work" || shown[0].id != "m1" {
t.Errorf("the first shown is %+v, want work/m1", shown[0])
}
if shown[1].account != "home" || shown[1].id != latest.MessageID {
t.Errorf("the folded shown is %+v, want home/%s (the latest)", shown[1], latest.MessageID)
}
// Redelivering the first event again shows nothing more: it was marked seen.
if err := n.Add(one); err != nil {
t.Fatalf("Add(one) again: %v", err)
}
clock.Advance(BurstWindow + time.Second)
if err := n.Flush(); err != nil {
t.Fatalf("Flush: %v", err)
}
if len(sent) != 2 {
t.Fatalf("a redelivered event was shown again: sent now %+v", sent)
}
}
+5
View File
@@ -0,0 +1,5 @@
module mesh-mail-notifier
go 1.22
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013182924-5933eb757d44
+2
View File
@@ -0,0 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013182924-5933eb757d44 h1:r5xIo5qWOXQdCSMxvqJy8kV4U+P/QWXFK6971Ha6wU8=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013182924-5933eb757d44/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
+79
View File
@@ -0,0 +1,79 @@
{
"module": "mesh-mail-notifier",
"version": "1",
"runs-as": "mesh-mail-notifier",
"claims": [
{
"name": "mail-notifier",
"scope": "node",
"serves": ["status"]
}
],
"consumes": [
"mesh-mail.mail.*.message.added"
],
"invokes": [
"seat:node-notifier.send",
"seat:mail-store.accounts"
],
"emits": [
"shown"
],
"state": [
{
"name": "shown",
"ttl-seconds": 2592000
}
],
"tools": [
"mail-notifier.status"
],
"own-secrets": {
"broker": "${dir:state}/broker"
},
"secrets-owner": "mesh-mail-notifier",
"resources": [
{
"id": "account",
"type": "user",
"name": "mesh-mail-notifier",
"shell": "/usr/bin/nologin",
"home": "/var/lib/mesh-mail-notifier"
},
{
"id": "state",
"type": "directory",
"mode": "0700",
"owner": "mesh-mail-notifier",
"place": "."
},
{
"id": "notify",
"type": "process",
"name": "mesh-mail-notifier-notify",
"artifact": "tools",
"run": [
"./mesh-mail-notifier",
"notify"
],
"health": {
"kind": "unit"
}
}
],
"build": {
"artifacts": [
{
"name": "tools",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/mesh-mail-notifier",
"binary": "mesh-mail-notifier",
"loads": [
"mesh-mail-notifier"
]
}
]
}
}