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}