Repository navigation
Expand file tree
/
Copy pathplugin.go
More file actions
125 lines (109 loc) · 3.62 KB
/
Copy pathplugin.go
File metadata and controls
125 lines (109 loc) · 3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package mailbox
import (
"context"
"errors"
"log/slog"
"github.com/rbaliyan/mailbox/store"
)
// Plugin defines the interface for mailbox extensions.
// Plugins can hook into message sending to add custom behavior
// such as spam filtering, rate limiting, or content validation.
//
// For observing other operations (read, delete, archive, etc.),
// use the event system instead (EventMessageSent, EventMessageRead, etc.).
type Plugin interface {
// Name returns the plugin identifier.
Name() string
// Init initializes the plugin. Called when service connects.
Init(ctx context.Context) error
// Close cleans up plugin resources. Called when service closes.
Close(ctx context.Context) error
}
// SendHook is called before/after sending messages.
// This is the primary extension point for message validation and filtering.
type SendHook interface {
Plugin
// BeforeSend is called before a draft is sent. Return an error to abort.
// Use this for spam filtering, rate limiting, or content validation.
BeforeSend(ctx context.Context, userID string, draft store.DraftMessage) error
// AfterSend is called after a message is successfully sent.
// Return an error to signal post-send failures (e.g., notification errors).
// Note: The message is already sent and cannot be rolled back.
AfterSend(ctx context.Context, userID string, msg store.Message) error
}
// pluginRegistry holds registered plugins.
type pluginRegistry struct {
all []Plugin
send []SendHook
logger *slog.Logger
}
// newPluginRegistry creates a new plugin registry.
func newPluginRegistry(logger *slog.Logger) *pluginRegistry {
if logger == nil {
logger = slog.Default()
}
return &pluginRegistry{logger: logger}
}
// register adds a plugin to the registry.
func (r *pluginRegistry) register(p Plugin) {
r.all = append(r.all, p)
if h, ok := p.(SendHook); ok {
r.send = append(r.send, h)
}
}
// initAll initializes all plugins.
// On failure, already-initialized plugins are closed in reverse order.
func (r *pluginRegistry) initAll(ctx context.Context) error {
for i, p := range r.all {
if err := p.Init(ctx); err != nil {
// Close already-initialized plugins in reverse order
for j := i - 1; j >= 0; j-- {
if closeErr := r.all[j].Close(ctx); closeErr != nil {
r.logger.Error("failed to close plugin during init rollback",
"plugin", r.all[j].Name(), "error", closeErr)
}
}
return &PluginError{Plugin: p.Name(), Op: "init", Err: err}
}
}
return nil
}
// closeAll closes all plugins in reverse order.
func (r *pluginRegistry) closeAll(ctx context.Context) error {
var errs []error
for i := len(r.all) - 1; i >= 0; i-- {
if err := r.all[i].Close(ctx); err != nil {
errs = append(errs, &PluginError{Plugin: r.all[i].Name(), Op: "close", Err: err})
}
}
return errors.Join(errs...)
}
// PluginError represents an error from a plugin.
type PluginError struct {
Plugin string
Op string
Err error
}
func (e *PluginError) Error() string {
return "plugin " + e.Plugin + " " + e.Op + ": " + e.Err.Error()
}
func (e *PluginError) Unwrap() error {
return e.Err
}
// Hook execution helpers
func (r *pluginRegistry) beforeSend(ctx context.Context, userID string, draft store.DraftMessage) error {
for _, h := range r.send {
if err := h.BeforeSend(ctx, userID, draft); err != nil {
return &PluginError{Plugin: h.Name(), Op: "BeforeSend", Err: err}
}
}
return nil
}
func (r *pluginRegistry) afterSend(ctx context.Context, userID string, msg store.Message) error {
for _, h := range r.send {
if err := h.AfterSend(ctx, userID, msg); err != nil {
return &PluginError{Plugin: h.Name(), Op: "AfterSend", Err: err}
}
}
return nil
}