ci: add go lint and race test workflow (#187)

This commit is contained in:
BBQ
2026-03-05 11:25:33 +08:00
committed by GitHub
parent 387ac50030
commit 3feb03aca7
192 changed files with 2245 additions and 2028 deletions
+21 -18
View File
@@ -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 {
+10 -9
View File
@@ -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
View File
@@ -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))
}
}
+1 -1
View File
@@ -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
-1
View File
@@ -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"`