initial project setup
This commit is contained in:
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Binaries
|
||||||
|
/arcline-email
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of `go env GOPATH`
|
||||||
|
/go/
|
||||||
|
|
||||||
|
# Dependency vendor directory
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# Go workspace
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# Environment / secrets
|
||||||
|
*.env
|
||||||
|
.env*
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
|
||||||
|
# Config (use arcline-email.example.toml as template)
|
||||||
|
arcline-email.toml
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
25
arcline-email.example.toml
Normal file
25
arcline-email.example.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# arcline-email configuration
|
||||||
|
# Copy to arcline-email.toml and adjust for your environment.
|
||||||
|
|
||||||
|
[server]
|
||||||
|
hostname = "mail.example.com"
|
||||||
|
|
||||||
|
[smtp]
|
||||||
|
inbound_addr = ":25"
|
||||||
|
submission_addr = ":587"
|
||||||
|
submission_tls_addr = ":465"
|
||||||
|
|
||||||
|
[imap]
|
||||||
|
addr = ":143"
|
||||||
|
tls_addr = ":993"
|
||||||
|
|
||||||
|
[tls]
|
||||||
|
cert_file = "/etc/arcline-email/cert.pem"
|
||||||
|
key_file = "/etc/arcline-email/key.pem"
|
||||||
|
|
||||||
|
[storage]
|
||||||
|
maildir_root = "/var/mail"
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
level = "info" # debug | info | warn | error
|
||||||
|
format = "json" # json | text
|
||||||
60
cmd/arcline-email/main.go
Normal file
60
cmd/arcline-email/main.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"arcline-email/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfgPath := flag.String("config", "arcline-email.toml", "path to config file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
cfg, err := config.Load(*cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to load config", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := newLogger(cfg.Logging)
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
|
slog.Info("arcline-email starting", "hostname", cfg.Server.Hostname)
|
||||||
|
|
||||||
|
// TODO: initialize and start servers (SMTP, IMAP)
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
slog.Info("shutdown signal received")
|
||||||
|
stop() // release signal resources before shutdown
|
||||||
|
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// TODO: call server.Shutdown(shutdownCtx) for each listener
|
||||||
|
_ = shutdownCtx
|
||||||
|
|
||||||
|
slog.Info("shutdown complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogger(cfg config.LoggingConfig) *slog.Logger {
|
||||||
|
var level slog.Level
|
||||||
|
if err := level.UnmarshalText([]byte(cfg.Level)); err != nil {
|
||||||
|
level = slog.LevelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &slog.HandlerOptions{Level: level}
|
||||||
|
|
||||||
|
if cfg.Format == "text" {
|
||||||
|
return slog.New(slog.NewTextHandler(os.Stdout, opts))
|
||||||
|
}
|
||||||
|
return slog.New(slog.NewJSONHandler(os.Stdout, opts))
|
||||||
|
}
|
||||||
78
config/config.go
Normal file
78
config/config.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server ServerConfig `toml:"server"`
|
||||||
|
SMTP SMTPConfig `toml:"smtp"`
|
||||||
|
IMAP IMAPConfig `toml:"imap"`
|
||||||
|
TLS TLSConfig `toml:"tls"`
|
||||||
|
Storage StorageConfig `toml:"storage"`
|
||||||
|
Logging LoggingConfig `toml:"logging"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
Hostname string `toml:"hostname"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SMTPConfig struct {
|
||||||
|
InboundAddr string `toml:"inbound_addr"`
|
||||||
|
SubmissionAddr string `toml:"submission_addr"`
|
||||||
|
SubmissionTLSAddr string `toml:"submission_tls_addr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IMAPConfig struct {
|
||||||
|
Addr string `toml:"addr"`
|
||||||
|
TLSAddr string `toml:"tls_addr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TLSConfig struct {
|
||||||
|
CertFile string `toml:"cert_file"`
|
||||||
|
KeyFile string `toml:"key_file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorageConfig struct {
|
||||||
|
MaildirRoot string `toml:"maildir_root"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoggingConfig struct {
|
||||||
|
Level string `toml:"level"` // debug, info, warn, error
|
||||||
|
Format string `toml:"format"` // json, text
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads the TOML config file at path, applying defaults for any missing fields.
|
||||||
|
func Load(path string) (*Config, error) {
|
||||||
|
cfg := defaults()
|
||||||
|
if _, err := toml.DecodeFile(path, cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaults() *Config {
|
||||||
|
return &Config{
|
||||||
|
Server: ServerConfig{
|
||||||
|
Hostname: "localhost",
|
||||||
|
},
|
||||||
|
SMTP: SMTPConfig{
|
||||||
|
InboundAddr: ":25",
|
||||||
|
SubmissionAddr: ":587",
|
||||||
|
SubmissionTLSAddr: ":465",
|
||||||
|
},
|
||||||
|
IMAP: IMAPConfig{
|
||||||
|
Addr: ":143",
|
||||||
|
TLSAddr: ":993",
|
||||||
|
},
|
||||||
|
Storage: StorageConfig{
|
||||||
|
MaildirRoot: "/var/mail",
|
||||||
|
},
|
||||||
|
Logging: LoggingConfig{
|
||||||
|
Level: "info",
|
||||||
|
Format: "json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module arcline-email
|
||||||
|
|
||||||
|
go 1.25.8
|
||||||
|
|
||||||
|
require github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
12
todo.md
12
todo.md
@@ -3,12 +3,12 @@
|
|||||||
## Phase 1: MVP (Core Mail Flow)
|
## Phase 1: MVP (Core Mail Flow)
|
||||||
|
|
||||||
### Project Setup
|
### Project Setup
|
||||||
- [ ] Initialize Go module (`go mod init arcline-email`)
|
- [x] Initialize Go module (`go mod init arcline-email`)
|
||||||
- [ ] Set up directory structure (`cmd/`, `internal/`, `config/`)
|
- [x] Set up directory structure (`cmd/`, `internal/`, `config/`)
|
||||||
- [ ] Add `.gitignore`
|
- [x] Add `.gitignore`
|
||||||
- [ ] Wire up config parsing (TOML)
|
- [x] Wire up config parsing (TOML)
|
||||||
- [ ] Structured logging (`log/slog`)
|
- [x] Structured logging (`log/slog`)
|
||||||
- [ ] Graceful shutdown (signal handling)
|
- [x] Graceful shutdown (signal handling)
|
||||||
|
|
||||||
### SMTP — Inbound (Port 25)
|
### SMTP — Inbound (Port 25)
|
||||||
- [ ] Basic SMTP listener using `emersion/go-smtp`
|
- [ ] Basic SMTP listener using `emersion/go-smtp`
|
||||||
|
|||||||
Reference in New Issue
Block a user