mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
ci: add go lint and race test workflow (#187)
This commit is contained in:
@@ -5,14 +5,14 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
mail "github.com/wneessen/go-mail"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapclient"
|
||||
mail "github.com/wneessen/go-mail"
|
||||
|
||||
"github.com/memohai/memoh/internal/email"
|
||||
)
|
||||
@@ -27,9 +27,9 @@ func New(log *slog.Logger) *Adapter {
|
||||
return &Adapter{logger: log.With(slog.String("adapter", "generic"))}
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() email.ProviderName { return ProviderName }
|
||||
func (*Adapter) Type() email.ProviderName { return ProviderName }
|
||||
|
||||
func (a *Adapter) Meta() email.ProviderMeta {
|
||||
func (*Adapter) Meta() email.ProviderMeta {
|
||||
return email.ProviderMeta{
|
||||
Provider: string(ProviderName),
|
||||
DisplayName: "Generic (SMTP/IMAP)",
|
||||
@@ -49,7 +49,7 @@ func (a *Adapter) Meta() email.ProviderMeta {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) NormalizeConfig(raw map[string]any) (map[string]any, error) {
|
||||
func (*Adapter) NormalizeConfig(raw map[string]any) (map[string]any, error) {
|
||||
for _, key := range []string{"smtp_host", "imap_host", "username", "password"} {
|
||||
if v, _ := raw[key].(string); strings.TrimSpace(v) == "" {
|
||||
return nil, fmt.Errorf("%s is required", key)
|
||||
@@ -75,7 +75,7 @@ func (a *Adapter) NormalizeConfig(raw map[string]any) (map[string]any, error) {
|
||||
|
||||
// ---- Sender ----
|
||||
|
||||
func (a *Adapter) Send(ctx context.Context, config map[string]any, msg email.OutboundEmail) (string, error) {
|
||||
func (*Adapter) Send(ctx context.Context, config map[string]any, msg email.OutboundEmail) (string, error) {
|
||||
host, _ := config["smtp_host"].(string)
|
||||
port := intVal(config["smtp_port"], 587)
|
||||
username, _ := config["username"].(string)
|
||||
@@ -223,7 +223,7 @@ func (c *imapConn) connectAndReceive(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial imap (%s): %w", c.security, err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
if err := client.Login(c.username, c.password).Wait(); err != nil {
|
||||
return fmt.Errorf("imap login: %w", err)
|
||||
@@ -302,7 +302,7 @@ func (c *imapConn) fetchNewMessages(ctx context.Context, client *imapclient.Clie
|
||||
BodySection: []*imap.FetchItemBodySection{{}},
|
||||
}
|
||||
fetchCmd := client.Fetch(uidSet, fetchOpts)
|
||||
defer fetchCmd.Close()
|
||||
defer func() { _ = fetchCmd.Close() }()
|
||||
|
||||
isFirstRun := c.lastUID == 0
|
||||
processed := 0
|
||||
@@ -341,7 +341,7 @@ func (c *imapConn) fetchNewMessages(ctx context.Context, client *imapclient.Clie
|
||||
c.logger.Info("imap fetch completed", slog.Int("processed", processed), slog.Uint64("last_uid", uint64(c.lastUID)))
|
||||
}
|
||||
|
||||
func (c *imapConn) bufToInbound(buf *imapclient.FetchMessageBuffer) *email.InboundEmail {
|
||||
func (*imapConn) bufToInbound(buf *imapclient.FetchMessageBuffer) *email.InboundEmail {
|
||||
env := buf.Envelope
|
||||
if env == nil {
|
||||
return nil
|
||||
@@ -373,7 +373,7 @@ func (c *imapConn) bufToInbound(buf *imapclient.FetchMessageBuffer) *email.Inbou
|
||||
|
||||
// ---- MailboxReader (on-demand IMAP queries) ----
|
||||
|
||||
func (a *Adapter) dialIMAP(config map[string]any) (*imapclient.Client, error) {
|
||||
func (*Adapter) dialIMAP(config map[string]any) (*imapclient.Client, error) {
|
||||
host, _ := config["imap_host"].(string)
|
||||
port := intVal(config["imap_port"], 993)
|
||||
username, _ := config["username"].(string)
|
||||
@@ -397,22 +397,22 @@ func (a *Adapter) dialIMAP(config map[string]any) (*imapclient.Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.Login(username, password).Wait(); err != nil {
|
||||
client.Close()
|
||||
_ = client.Close()
|
||||
return nil, err
|
||||
}
|
||||
if _, err := client.Select("INBOX", nil).Wait(); err != nil {
|
||||
client.Close()
|
||||
_ = client.Close()
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) ListMailbox(ctx context.Context, config map[string]any, page, pageSize int) ([]email.InboundEmail, int, error) {
|
||||
func (a *Adapter) ListMailbox(_ context.Context, config map[string]any, page, pageSize int) ([]email.InboundEmail, int, error) {
|
||||
client, err := a.dialIMAP(config)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("imap connect: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
// Get total message count via STATUS
|
||||
statusData, err := client.Status("INBOX", &imap.StatusOptions{NumMessages: true}).Wait()
|
||||
@@ -438,6 +438,9 @@ func (a *Adapter) ListMailbox(ctx context.Context, config map[string]any, page,
|
||||
}
|
||||
|
||||
seqSet := imap.SeqSet{}
|
||||
if start > math.MaxUint32 || end > math.MaxUint32 {
|
||||
return nil, 0, fmt.Errorf("mail sequence range out of bounds: start=%d end=%d", start, end)
|
||||
}
|
||||
seqSet.AddRange(uint32(start), uint32(end))
|
||||
|
||||
fetchOpts := &imap.FetchOptions{
|
||||
@@ -445,7 +448,7 @@ func (a *Adapter) ListMailbox(ctx context.Context, config map[string]any, page,
|
||||
UID: true,
|
||||
}
|
||||
fetchCmd := client.Fetch(seqSet, fetchOpts)
|
||||
defer fetchCmd.Close()
|
||||
defer func() { _ = fetchCmd.Close() }()
|
||||
|
||||
var results []email.InboundEmail
|
||||
for {
|
||||
@@ -478,12 +481,12 @@ func (a *Adapter) ListMailbox(ctx context.Context, config map[string]any, page,
|
||||
return results, total, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) ReadMailbox(ctx context.Context, config map[string]any, uid uint32) (*email.InboundEmail, error) {
|
||||
func (a *Adapter) ReadMailbox(_ context.Context, config map[string]any, uid uint32) (*email.InboundEmail, error) {
|
||||
client, err := a.dialIMAP(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("imap connect: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
uidSet := imap.UIDSet{}
|
||||
uidSet.AddNum(imap.UID(uid))
|
||||
@@ -494,7 +497,7 @@ func (a *Adapter) ReadMailbox(ctx context.Context, config map[string]any, uid ui
|
||||
BodySection: []*imap.FetchItemBodySection{{}},
|
||||
}
|
||||
fetchCmd := client.Fetch(uidSet, fetchOpts)
|
||||
defer fetchCmd.Close()
|
||||
defer func() { _ = fetchCmd.Close() }()
|
||||
|
||||
msgData := fetchCmd.Next()
|
||||
if msgData == nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -33,9 +34,9 @@ func New(log *slog.Logger) *Adapter {
|
||||
return &Adapter{logger: log.With(slog.String("adapter", "mailgun"))}
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() email.ProviderName { return ProviderName }
|
||||
func (*Adapter) Type() email.ProviderName { return ProviderName }
|
||||
|
||||
func (a *Adapter) Meta() email.ProviderMeta {
|
||||
func (*Adapter) Meta() email.ProviderMeta {
|
||||
return email.ProviderMeta{
|
||||
Provider: string(ProviderName),
|
||||
DisplayName: "Mailgun",
|
||||
@@ -52,7 +53,7 @@ func (a *Adapter) Meta() email.ProviderMeta {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) NormalizeConfig(raw map[string]any) (map[string]any, error) {
|
||||
func (*Adapter) NormalizeConfig(raw map[string]any) (map[string]any, error) {
|
||||
for _, key := range []string{"domain", "api_key"} {
|
||||
if v, _ := raw[key].(string); strings.TrimSpace(v) == "" {
|
||||
return nil, fmt.Errorf("%s is required", key)
|
||||
@@ -64,7 +65,7 @@ func (a *Adapter) NormalizeConfig(raw map[string]any) (map[string]any, error) {
|
||||
}
|
||||
if mode == InboundModeWebhook {
|
||||
if v, _ := raw["webhook_signing_key"].(string); strings.TrimSpace(v) == "" {
|
||||
return nil, fmt.Errorf("webhook_signing_key is required for webhook mode")
|
||||
return nil, errors.New("webhook_signing_key is required for webhook mode")
|
||||
}
|
||||
}
|
||||
if _, ok := raw["region"]; !ok {
|
||||
@@ -81,14 +82,14 @@ func newClient(config map[string]any) *mg.Client {
|
||||
client := mg.NewMailgun(apiKey)
|
||||
region, _ := config["region"].(string)
|
||||
if region == "eu" {
|
||||
client.SetAPIBase(mg.APIBaseEU)
|
||||
_ = client.SetAPIBase(mg.APIBaseEU)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// ---- Sender ----
|
||||
|
||||
func (a *Adapter) Send(ctx context.Context, config map[string]any, msg email.OutboundEmail) (string, error) {
|
||||
func (*Adapter) Send(ctx context.Context, config map[string]any, msg email.OutboundEmail) (string, error) {
|
||||
client := newClient(config)
|
||||
domain, _ := config["domain"].(string)
|
||||
|
||||
@@ -137,7 +138,7 @@ func (a *Adapter) StartReceiving(ctx context.Context, config map[string]any, han
|
||||
|
||||
// ---- WebhookReceiver ----
|
||||
|
||||
func (a *Adapter) HandleWebhook(_ context.Context, config map[string]any, r *http.Request) (*email.InboundEmail, error) {
|
||||
func (*Adapter) HandleWebhook(_ context.Context, config map[string]any, r *http.Request) (*email.InboundEmail, error) {
|
||||
signingKey, _ := config["webhook_signing_key"].(string)
|
||||
|
||||
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||
@@ -154,7 +155,7 @@ func (a *Adapter) HandleWebhook(_ context.Context, config map[string]any, r *htt
|
||||
mac.Write([]byte(timestamp + token))
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
if !hmac.Equal([]byte(expected), []byte(signature)) {
|
||||
return nil, fmt.Errorf("webhook signature verification failed")
|
||||
return nil, errors.New("webhook signature verification failed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +250,7 @@ func (c *pollConn) pollEvents(ctx context.Context) {
|
||||
|
||||
type noopStopper struct{}
|
||||
|
||||
func (n *noopStopper) Stop(_ context.Context) error { return nil }
|
||||
func (*noopStopper) Stop(_ context.Context) error { return nil }
|
||||
|
||||
func intVal(v any, fallback int) int {
|
||||
switch n := v.(type) {
|
||||
|
||||
+11
-10
@@ -2,6 +2,7 @@ package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
@@ -9,10 +10,10 @@ import (
|
||||
|
||||
// Manager manages the lifecycle of all email receiving connections.
|
||||
type Manager struct {
|
||||
logger *slog.Logger
|
||||
service *Service
|
||||
trigger *Trigger
|
||||
outbox *OutboxService
|
||||
logger *slog.Logger
|
||||
service *Service
|
||||
trigger *Trigger
|
||||
outbox *OutboxService
|
||||
|
||||
mu sync.Mutex
|
||||
conns map[string]Stopper // provider_id -> stopper
|
||||
@@ -57,7 +58,7 @@ func (m *Manager) startProvider(ctx context.Context, p ProviderResponse) error {
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.stopped {
|
||||
return fmt.Errorf("manager is stopped")
|
||||
return errors.New("manager is stopped")
|
||||
}
|
||||
if _, exists := m.conns[p.ID]; exists {
|
||||
return nil
|
||||
@@ -86,7 +87,7 @@ func (m *Manager) startProvider(ctx context.Context, p ProviderResponse) error {
|
||||
|
||||
// RefreshProvider restarts receiving for a specific provider.
|
||||
func (m *Manager) RefreshProvider(ctx context.Context, providerID string) error {
|
||||
m.stopProvider(providerID)
|
||||
m.stopProvider(ctx, providerID)
|
||||
|
||||
p, err := m.service.GetProvider(ctx, providerID)
|
||||
if err != nil {
|
||||
@@ -104,7 +105,7 @@ func (m *Manager) RefreshProvider(ctx context.Context, providerID string) error
|
||||
return m.startProvider(ctx, p)
|
||||
}
|
||||
|
||||
func (m *Manager) stopProvider(providerID string) {
|
||||
func (m *Manager) stopProvider(ctx context.Context, providerID string) {
|
||||
m.mu.Lock()
|
||||
stopper, exists := m.conns[providerID]
|
||||
if exists {
|
||||
@@ -113,14 +114,14 @@ func (m *Manager) stopProvider(providerID string) {
|
||||
m.mu.Unlock()
|
||||
|
||||
if exists && stopper != nil {
|
||||
if err := stopper.Stop(context.Background()); err != nil {
|
||||
if err := stopper.Stop(ctx); err != nil {
|
||||
m.logger.Error("failed to stop provider", slog.String("provider_id", providerID), slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down all receiving connections.
|
||||
func (m *Manager) Stop() {
|
||||
func (m *Manager) Stop(ctx context.Context) {
|
||||
m.mu.Lock()
|
||||
m.stopped = true
|
||||
conns := make(map[string]Stopper, len(m.conns))
|
||||
@@ -131,7 +132,7 @@ func (m *Manager) Stop() {
|
||||
m.mu.Unlock()
|
||||
|
||||
for id, stopper := range conns {
|
||||
if err := stopper.Stop(context.Background()); err != nil {
|
||||
if err := stopper.Stop(ctx); err != nil {
|
||||
m.logger.Error("failed to stop provider", slog.String("provider_id", id), slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func (s *OutboxService) ListByBot(ctx context.Context, botID string, limit, offs
|
||||
return items, count, nil
|
||||
}
|
||||
|
||||
func (s *OutboxService) toOutboxResponse(row sqlc.EmailOutbox) OutboxItemResponse {
|
||||
func (*OutboxService) toOutboxResponse(row sqlc.EmailOutbox) OutboxItemResponse {
|
||||
var to []string
|
||||
_ = json.Unmarshal(row.ToAddresses, &to)
|
||||
var attachments []any
|
||||
|
||||
@@ -4,7 +4,6 @@ import "time"
|
||||
|
||||
type ProviderName string
|
||||
|
||||
|
||||
// FieldSchema describes a single configuration field for dynamic form generation.
|
||||
type FieldSchema struct {
|
||||
Key string `json:"key"`
|
||||
|
||||
Reference in New Issue
Block a user