mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
cc5f00355f
* feat: add email service with multi-adapter support Implement a full-stack email service with global provider management, per-bot bindings with granular read/write permissions, outbox audit storage, and MCP tool integration for direct mailbox access. Backend: - Email providers: CRUD with dynamic config schema (generic SMTP/IMAP, Mailgun) - Generic adapter: go-mail (SMTP) + go-imap/v2 (IMAP IDLE real-time push via UnilateralDataHandler + UID-based tracking + periodic check fallback) - Mailgun adapter: mailgun-go/v5 with dual inbound mode (webhook + poll) - Bot email bindings: per-bot provider binding with independent r/w permissions - Outbox: outbound email audit log with status tracking - Trigger: inbound emails push notification to bot_inbox (from/subject only, LLM reads full content on demand via MCP tools) - MailboxReader interface: on-demand IMAP queries for listing/reading emails - MCP tools: email_accounts, email_send, email_list (paginated mailbox), email_read (by UID) — all with multi-binding and provider_id selection - Webhook: /email/mailgun/webhook/:config_id (JWT-skipped, signature-verified) - DB migration: 0019_add_email (email_providers, bot_email_bindings, email_outbox) Frontend: - Email Providers page: /email-providers with MasterDetailSidebarLayout - Dynamic config form rendered from ordered provider meta schema with i18n keys - Bot detail: Email tab with bindings management + outbox audit table - Sidebar navigation entry - Full i18n support (en + zh) - Auto-generated SDK from Swagger Closes #17 * feat(email): trigger bot conversation immediately on inbound email Instead of only storing an inbox item and waiting for the next chat, the email trigger now proactively invokes the conversation resolver so the bot processes new emails right away — aligned with the schedule/heartbeat trigger pattern. * fix: lint --------- Co-authored-by: Acbox <acbox0328@gmail.com>
122 lines
3.1 KiB
Go
122 lines
3.1 KiB
Go
package email
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
)
|
|
|
|
// Adapter is the base interface every email adapter must implement.
|
|
type Adapter interface {
|
|
Type() ProviderName
|
|
Meta() ProviderMeta
|
|
NormalizeConfig(raw map[string]any) (map[string]any, error)
|
|
}
|
|
|
|
// Sender sends outbound emails.
|
|
type Sender interface {
|
|
Send(ctx context.Context, config map[string]any, msg OutboundEmail) (messageID string, err error)
|
|
}
|
|
|
|
// Receiver establishes a long-lived connection (IMAP IDLE / polling) to receive emails.
|
|
type Receiver interface {
|
|
StartReceiving(ctx context.Context, config map[string]any, handler InboundHandler) (Stopper, error)
|
|
}
|
|
|
|
// WebhookReceiver handles inbound emails via HTTP webhook callbacks.
|
|
type WebhookReceiver interface {
|
|
HandleWebhook(ctx context.Context, config map[string]any, r *http.Request) (*InboundEmail, error)
|
|
}
|
|
|
|
// MailboxReader lists and reads emails directly from the remote mailbox.
|
|
type MailboxReader interface {
|
|
ListMailbox(ctx context.Context, config map[string]any, page, pageSize int) ([]InboundEmail, int, error)
|
|
ReadMailbox(ctx context.Context, config map[string]any, uid uint32) (*InboundEmail, error)
|
|
}
|
|
|
|
// Deleter removes an email from the remote mailbox.
|
|
type Deleter interface {
|
|
DeleteRemote(ctx context.Context, config map[string]any, messageID string) error
|
|
}
|
|
|
|
// InboundHandler is invoked when a new email arrives.
|
|
type InboundHandler func(ctx context.Context, providerID string, email InboundEmail) error
|
|
|
|
// Stopper represents a stoppable background process.
|
|
type Stopper interface {
|
|
Stop(ctx context.Context) error
|
|
}
|
|
|
|
// Registry holds all registered email adapters.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
adapters map[ProviderName]Adapter
|
|
}
|
|
|
|
func NewRegistry() *Registry {
|
|
return &Registry{adapters: make(map[ProviderName]Adapter)}
|
|
}
|
|
|
|
func (r *Registry) Register(a Adapter) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.adapters[a.Type()] = a
|
|
}
|
|
|
|
func (r *Registry) Get(name ProviderName) (Adapter, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
a, ok := r.adapters[name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("email adapter not found: %s", name)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
func (r *Registry) GetSender(name ProviderName) (Sender, error) {
|
|
a, err := r.Get(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s, ok := a.(Sender)
|
|
if !ok {
|
|
return nil, fmt.Errorf("email adapter %s does not support sending", name)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (r *Registry) GetReceiver(name ProviderName) (Receiver, error) {
|
|
a, err := r.Get(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
recv, ok := a.(Receiver)
|
|
if !ok {
|
|
return nil, fmt.Errorf("email adapter %s does not support receiving", name)
|
|
}
|
|
return recv, nil
|
|
}
|
|
|
|
func (r *Registry) GetMailboxReader(name ProviderName) (MailboxReader, error) {
|
|
a, err := r.Get(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reader, ok := a.(MailboxReader)
|
|
if !ok {
|
|
return nil, fmt.Errorf("email adapter %s does not support mailbox reading", name)
|
|
}
|
|
return reader, nil
|
|
}
|
|
|
|
func (r *Registry) ListMeta() []ProviderMeta {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
metas := make([]ProviderMeta, 0, len(r.adapters))
|
|
for _, a := range r.adapters {
|
|
metas = append(metas, a.Meta())
|
|
}
|
|
return metas
|
|
}
|