initial project setup

This commit is contained in:
Blake Ridgway
2026-03-25 02:36:12 -05:00
parent 725bd460a5
commit f1f8ae610b
7 changed files with 216 additions and 6 deletions

40
.gitignore vendored Normal file
View 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

View 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
View 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
View 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
View 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
View 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
View File

@@ -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`