wk2: account management tooling

This commit is contained in:
ari melody 2026-02-05 14:02:32 +00:00
parent 37eeeb2467
commit f864d9c84e
Signed by: ari
GPG key ID: CF99829C92678188
34 changed files with 379 additions and 350 deletions

View file

@ -0,0 +1,3 @@
module jupiter-mail.org/account-manager
go 1.23

136
src/account-manager/main.go Normal file
View file

@ -0,0 +1,136 @@
package main
import (
_ "embed"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"path"
"strings"
"jupiter-mail.org/account-manager/model"
)
//go:embed res/help.txt
var helpText string
var USERDB_PATH = path.Clean("/etc/dovecot/userdb")
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, helpText)
os.Exit(1)
}
switch os.Args[1] {
case "create":
account, err := createAccount(os.Args[2], os.Args[3])
if err != nil {
log.Fatalf("Failed to create account: %v", err)
os.Exit(1)
}
log.Printf("Account \"%s\" created successfully.", account.FullUsername())
case "list":
log.Fatalf("list: not implemented")
os.Exit(1)
case "update":
log.Fatalf("update: not implemented")
os.Exit(1)
case "delete":
log.Fatalf("delete: not implemented")
os.Exit(1)
default:
log.Fatalf("Unrecognised action \"%s\"", os.Args[1])
os.Exit(1)
}
}
func openUserDB() (*os.File, error) {
err := os.MkdirAll(path.Dir(USERDB_PATH), 0755)
if err != nil { return nil, err }
file, err := os.OpenFile(USERDB_PATH, os.O_CREATE | os.O_RDWR, 0640)
if err != nil && errors.Is(err, os.ErrExist) { return nil, err }
return file, nil
}
func getMailHome(user string, domain string) string {
return fmt.Sprintf("/var/mail/%s/%s/home", domain, user)
}
func checkAccountExists(username string) (bool, error) {
file, err := openUserDB()
if err != nil { return false, fmt.Errorf("open userdb: %v", err) }
data, err := io.ReadAll(file)
if err != nil { return false, fmt.Errorf("read userdb: %v", err) }
file.Close()
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(line, username) { return true, nil }
}
return false, nil
}
func createAccount(fullUsername string, password string) (*model.MailAccount, error) {
exists, err := checkAccountExists(fullUsername)
if err != nil { return nil, fmt.Errorf("failed to check if account exists: %v", err) }
if exists { return nil, fmt.Errorf("account already exists: %s", fullUsername) }
usernameSplits := strings.Split(fullUsername, "@")
if len(usernameSplits) != 2 {
return nil, fmt.Errorf("invalid email format: %s", fullUsername)
}
username := usernameSplits[0]
domain := usernameSplits[1]
passwordHash, err := hashPassword(username, password)
if err != nil { return nil, fmt.Errorf("hash password: %v", err) }
account := model.NewMailAccount(username, domain, []byte(passwordHash))
uid := 5000; gid := 5000
// <username>:<password>:5000:5000::/var/mail/<domain>/<user>/home::
userDbLine := fmt.Sprintf(
"%s:%s:%d:%d::%s::\n",
account.FullUsername(),
account.PasswordHash,
uid, gid,
getMailHome(account.Username, account.Domain),
)
file, err := openUserDB()
if err != nil { return nil, fmt.Errorf("open userdb: %v", err) }
buf := make([]byte, 1024)
for n, err := file.Read(buf); n > 0; {
if err != nil {
return nil, fmt.Errorf("read userdb: %v", err)
}
}
_, err = file.Write([]byte(userDbLine))
if err != nil { return nil, err }
err = file.Close()
if err != nil { return nil, err }
return &account, nil
}
func hashPassword(username string, password string) (string, error) {
// doveadm pw -s SHA512-CRYPT -u "$USERNAME" -p "$PASSWORD"
hashCmd := exec.Command(
"doveadm", "pw",
"-s", "SHA512-CRYPT",
"-u", username,
"-p", password,
)
out, err := hashCmd.Output()
if err != nil { return "", err }
return string(out), nil
}

View file

@ -0,0 +1,23 @@
package model
import "fmt"
type (
MailAccount struct {
Username string `json:"username"`
Domain string `json:"domain"`
PasswordHash []byte `json:"password"`
}
)
func NewMailAccount(username string, domain string, passwordHash []byte) MailAccount {
return MailAccount{
Username: username,
Domain: domain,
PasswordHash: passwordHash,
}
}
func (account *MailAccount) FullUsername() string {
return fmt.Sprintf("%s@%s", account.Username, account.Domain)
}

View file

@ -0,0 +1,7 @@
usage: accounts <action> [...opts]
actions:
create <username> <password>
list
update <username> <password>
delete <username>