hooks.go

  1package plugin
  2
  3import (
  4	"log"
  5	"time"
  6
  7	lua "github.com/yuin/gopher-lua"
  8)
  9
 10// Hook event names.
 11const (
 12	HookStartup         = "startup"
 13	HookShutdown        = "shutdown"
 14	HookEmailReceived   = "email_received"
 15	HookEmailSendBefore = "email_send_before"
 16	HookEmailSendAfter  = "email_send_after"
 17	HookEmailViewed     = "email_viewed"
 18	HookFolderChanged   = "folder_changed"
 19	HookComposerUpdated = "composer_updated"
 20	HookEmailBodyRender = "email_body_render"
 21)
 22
 23// Status area names.
 24const (
 25	StatusInbox     = "inbox"
 26	StatusComposer  = "composer"
 27	StatusEmailView = "email_view"
 28)
 29
 30type registeredHook struct {
 31	fn     *lua.LFunction
 32	plugin string
 33}
 34
 35// registerHook adds a callback for the given event.
 36func (m *Manager) registerHook(event string, fn *lua.LFunction) {
 37	m.hooks[event] = append(m.hooks[event], registeredHook{fn: fn, plugin: m.currentPlugin})
 38}
 39
 40// CallHook invokes all callbacks registered for the given event.
 41func (m *Manager) CallHook(event string, args ...lua.LValue) {
 42	callbacks, ok := m.hooks[event]
 43	if !ok {
 44		return
 45	}
 46
 47	previousPlugin := m.currentPlugin
 48	defer func() {
 49		m.currentPlugin = previousPlugin
 50	}()
 51
 52	for _, hook := range callbacks {
 53		m.currentPlugin = hook.plugin
 54		if err := m.state.CallByParam(lua.P{
 55			Fn:      hook.fn,
 56			NRet:    0,
 57			Protect: true,
 58		}, args...); err != nil {
 59			log.Printf("plugin hook %q error: %v", event, err)
 60		}
 61	}
 62}
 63
 64// CallSendHook calls a hook with email send metadata.
 65func (m *Manager) CallSendHook(event string, to, cc, subject, accountID string) {
 66	callbacks, ok := m.hooks[event]
 67	if !ok || len(callbacks) == 0 {
 68		return
 69	}
 70
 71	L := m.state
 72	t := L.NewTable()
 73	t.RawSetString("to", lua.LString(to))
 74	t.RawSetString("cc", lua.LString(cc))
 75	t.RawSetString("subject", lua.LString(subject))
 76	t.RawSetString("account_id", lua.LString(accountID))
 77
 78	previousPlugin := m.currentPlugin
 79	defer func() {
 80		m.currentPlugin = previousPlugin
 81	}()
 82	for _, hook := range callbacks {
 83		m.currentPlugin = hook.plugin
 84		if err := L.CallByParam(lua.P{
 85			Fn:      hook.fn,
 86			NRet:    0,
 87			Protect: true,
 88		}, t); err != nil {
 89			log.Printf("plugin hook %q error: %v", event, err)
 90		}
 91	}
 92}
 93
 94// CallFolderHook calls a hook with a folder name.
 95func (m *Manager) CallFolderHook(event string, folderName string) {
 96	callbacks, ok := m.hooks[event]
 97	if !ok {
 98		return
 99	}
100
101	previousPlugin := m.currentPlugin
102	defer func() {
103		m.currentPlugin = previousPlugin
104	}()
105
106	for _, hook := range callbacks {
107		m.currentPlugin = hook.plugin
108		if err := m.state.CallByParam(lua.P{
109			Fn:      hook.fn,
110			NRet:    0,
111			Protect: true,
112		}, lua.LString(folderName)); err != nil {
113			log.Printf("plugin hook %q error: %v", event, err)
114		}
115	}
116}
117
118// CallComposerHook calls a hook with composer state info.
119func (m *Manager) CallComposerHook(event string, body, subject, to, cc, bcc string) {
120	callbacks, ok := m.hooks[event]
121	if !ok || len(callbacks) == 0 {
122		return
123	}
124
125	L := m.state
126	t := L.NewTable()
127	t.RawSetString("body_len", lua.LNumber(len(body)))
128	t.RawSetString("body", lua.LString(body))
129	t.RawSetString("subject", lua.LString(subject))
130	t.RawSetString("to", lua.LString(to))
131	t.RawSetString("cc", lua.LString(cc))
132	t.RawSetString("bcc", lua.LString(bcc))
133
134	previousPlugin := m.currentPlugin
135	defer func() {
136		m.currentPlugin = previousPlugin
137	}()
138	for _, hook := range callbacks {
139		m.currentPlugin = hook.plugin
140		if err := L.CallByParam(lua.P{
141			Fn:      hook.fn,
142			NRet:    0,
143			Protect: true,
144		}, t); err != nil {
145			log.Printf("plugin hook %q error: %v", event, err)
146		}
147	}
148}
149
150// CallBodyRenderHook runs all email_body_render callbacks, threading the body
151// string through each. Callbacks receive (email_table, rendered, raw):
152//   - rendered: the current display string (ANSI-styled, post-HTML→terminal)
153//   - raw: the original message body (HTML or plain text, same string fed to
154//     the renderer) — useful for parsing the source instead of the rendered
155//     output
156//
157// A callback may return a string to replace the rendered body, or nil to leave
158// it unchanged. Non-string returns are ignored. Multiple callbacks chain in
159// registration order; each subsequent callback sees the previous callback's
160// rendered output, but always the same raw source.
161func (m *Manager) CallBodyRenderHook(email *lua.LTable, rendered, raw string) string {
162	callbacks, ok := m.hooks[HookEmailBodyRender]
163	if !ok {
164		return rendered
165	}
166
167	L := m.state
168	previousPlugin := m.currentPlugin
169	defer func() {
170		m.currentPlugin = previousPlugin
171	}()
172
173	for _, hook := range callbacks {
174		m.currentPlugin = hook.plugin
175		if err := L.CallByParam(lua.P{
176			Fn:      hook.fn,
177			NRet:    1,
178			Protect: true,
179		}, email, lua.LString(rendered), lua.LString(raw)); err != nil {
180			log.Printf("plugin hook %q error: %v", HookEmailBodyRender, err)
181			continue
182		}
183		ret := L.Get(-1)
184		L.Pop(1)
185		if s, ok := ret.(lua.LString); ok {
186			rendered = string(s)
187		}
188	}
189	return rendered
190}
191
192// CallKeyBinding invokes a plugin key binding callback with the given arguments.
193func (m *Manager) CallKeyBinding(binding KeyBinding, args ...lua.LValue) {
194	previousPlugin := m.currentPlugin
195	m.currentPlugin = binding.Plugin
196	defer func() {
197		m.currentPlugin = previousPlugin
198	}()
199
200	if err := m.state.CallByParam(lua.P{
201		Fn:      binding.Fn,
202		NRet:    0,
203		Protect: true,
204	}, args...); err != nil {
205		log.Printf("plugin keybinding %q error: %v", binding.Key, err)
206	}
207}
208
209// EmailToTable converts email fields into a Lua table.
210func (m *Manager) EmailToTable(uid uint32, from string, to []string, subject string, date time.Time, isRead bool, accountID string, folder string) *lua.LTable {
211	L := m.state
212
213	t := L.NewTable()
214	t.RawSetString("uid", lua.LNumber(uid))
215	t.RawSetString("from", lua.LString(from))
216	t.RawSetString("subject", lua.LString(subject))
217	t.RawSetString("date", lua.LString(date.Format(time.RFC3339)))
218	t.RawSetString("is_read", lua.LBool(isRead))
219	t.RawSetString("account_id", lua.LString(accountID))
220	t.RawSetString("folder", lua.LString(folder))
221
222	toTable := L.NewTable()
223	for i, addr := range to {
224		toTable.RawSetInt(i+1, lua.LString(addr))
225	}
226	t.RawSetString("to", toTable)
227
228	return t
229}