Maddy account management UI
diff --git a/apps/maddy/Dockerfile b/apps/maddy/Dockerfile
new file mode 100644
index 0000000..7218ec3
--- /dev/null
+++ b/apps/maddy/Dockerfile
@@ -0,0 +1,7 @@
+FROM giolekva/maddy:v0.4.4 AS maddy
+
+# FROM alpine:latest
+
+# COPY --from=maddy /usr/bin/maddyctl /usr/bin
+COPY maddy-web /usr/bin
+RUN chmod +x /usr/bin/maddy-web
diff --git a/apps/maddy/Makefile b/apps/maddy/Makefile
new file mode 100644
index 0000000..38a2eaf
--- /dev/null
+++ b/apps/maddy/Makefile
@@ -0,0 +1,18 @@
+build:
+	go1.16 build -o maddy-web *.go
+
+clean:
+	rm -f maddy-web
+
+image: clean build
+	docker build --tag=giolekva/maddy-web .
+
+push: image
+	docker push giolekva/maddy-web:latest
+
+
+push_arm64: export GOOS=linux
+push_arm64: export GOARCH=arm64
+push_arm64: export CGO_ENABLED=0
+push_arm64: export GO111MODULE=on
+push_arm64: push
diff --git a/apps/maddy/install.yaml b/apps/maddy/install.yaml
index 22a46a7..f8a0926 100644
--- a/apps/maddy/install.yaml
+++ b/apps/maddy/install.yaml
@@ -30,6 +30,47 @@
       protocol: TCP
       name: e
 ---
+apiVersion: v1
+kind: Service
+metadata:
+  name: web
+  namespace: app-maddy
+spec:
+  type: ClusterIP
+  selector:
+    app: maddy
+  ports:
+  - name: http
+    port: 80
+    targetPort: http
+    protocol: TCP
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: web-ingress
+  namespace: app-maddy
+  annotations:
+    cert-manager.io/cluster-issuer: "selfsigned-ca"
+    acme.cert-manager.io/http01-edit-in-place: "true"
+spec:
+  ingressClassName: nginx-private
+  tls:
+  - hosts:
+    - maddy.pcloud
+    secretName: cert-maddy-web.pcloud
+  rules:
+  - host: maddy.pcloud
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: web
+            port:
+              name: http
+---
 apiVersion: cert-manager.io/v1
 kind: Certificate
 metadata:
@@ -99,6 +140,22 @@
           mountPath: /etc/maddy/certs
         - name: data
           mountPath: /var/lib/maddy
+      - name: web
+        image: giolekva/maddy-web:latest
+        imagePullPolicy: Always
+        ports:
+        - name: http
+          containerPort: 80
+          protocol: TCP
+        command: ["maddy-web"]
+        args: ["-port", "80", "-maddy-config", "/etc/maddy/config/maddy.conf"]
+        volumeMounts:
+        - name: config
+          mountPath: /etc/maddy/config
+        - name: certs
+          mountPath: /etc/maddy/certs
+        - name: data
+          mountPath: /var/lib/maddy
 ---
 apiVersion: v1
 kind: ConfigMap
diff --git a/apps/maddy/main.go b/apps/maddy/main.go
new file mode 100644
index 0000000..d4bced0
--- /dev/null
+++ b/apps/maddy/main.go
@@ -0,0 +1,135 @@
+package main
+
+import (
+	"bufio"
+	"embed"
+	"flag"
+	"fmt"
+	"html/template"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os/exec"
+)
+
+var port = flag.Int("port", 8080, "Port to listen on.")
+var maddyConfig = flag.String("maddy-config", "", "Path to the Maddy configuration file.")
+
+//go:embed templates/*
+var tmpls embed.FS
+
+type Templates struct {
+	Index *template.Template
+}
+
+func ParseTemplates(fs embed.FS) (*Templates, error) {
+	index, err := template.ParseFS(fs, "templates/index.html")
+	if err != nil {
+		return nil, err
+	}
+	return &Templates{index}, nil
+}
+
+type MaddyManager struct {
+	configPath string
+}
+
+func (m MaddyManager) ListAccounts() ([]string, error) {
+	cmd := exec.Command("maddyctl", "-config", m.configPath, "creds", "list")
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, err
+	}
+	if err := cmd.Start(); err != nil {
+		return nil, err
+	}
+	scanner := bufio.NewScanner(stdout)
+	accts := make([]string, 0)
+	for scanner.Scan() {
+		acct := scanner.Text()
+		if len(acct) == 0 {
+			continue
+		}
+		accts = append(accts, acct)
+	}
+	return accts, nil
+
+}
+
+func (m MaddyManager) CreateAccount(username, password string) error {
+	cmd := exec.Command("maddyctl", "-config", m.configPath, "creds", "create", username)
+	stdin, err := cmd.StdinPipe()
+	if err != nil {
+		return err
+	}
+	if err := cmd.Start(); err != nil {
+		return err
+	}
+	go func() {
+		defer stdin.Close()
+		io.WriteString(stdin, password)
+	}()
+	if err := cmd.Wait(); err != nil {
+		return err
+	}
+	// Create IMAP
+	cmd = exec.Command("maddyctl", "-config", m.configPath, "imap-acct", "create", username)
+	return cmd.Run()
+}
+
+type MaddyHandler struct {
+	mgr   MaddyManager
+	tmpls *Templates
+}
+
+func (h *MaddyHandler) handleListAccounts(w http.ResponseWriter, r *http.Request) {
+	accts, err := h.mgr.ListAccounts()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := h.tmpls.Index.Execute(w, accts); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	}
+}
+
+func (h *MaddyHandler) handleCreateAccount(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	err := h.mgr.CreateAccount(r.FormValue("username"), r.FormValue("password"))
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	http.Redirect(w, r, "/", http.StatusSeeOther)
+}
+
+func main() {
+	flag.Parse()
+	t, err := ParseTemplates(tmpls)
+	if err != nil {
+		log.Fatal(err)
+	}
+	mgr := MaddyManager{
+		configPath: *maddyConfig,
+	}
+	handler := MaddyHandler{
+		mgr:   mgr,
+		tmpls: t,
+	}
+	http.HandleFunc("/", handler.handleListAccounts)
+	http.HandleFunc("/create", handler.handleCreateAccount)
+	fmt.Printf("Starting HTTP server on port: %d\n", *port)
+	fmt.Printf("Maddy config: %s\n", *maddyConfig)
+	if cfg, err := ioutil.ReadFile(*maddyConfig); err != nil {
+		log.Fatal(err)
+	} else {
+		log.Print(string(cfg))
+	}
+	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
+
+}
diff --git a/apps/maddy/templates/index.html b/apps/maddy/templates/index.html
new file mode 100644
index 0000000..a5fcb97
--- /dev/null
+++ b/apps/maddy/templates/index.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8" />
+    <title>Manage email accounts</title>
+</head>
+<body>
+    <form action="/create" method="POST">
+	<label for="username">Username:</label><br />
+	<input type="text" name="username" /><br />
+	<label for="password">Last name:</label><br />
+	<input type="password" name="password" /><br />
+	<input type="submit" value="Create New Account" />
+    </form>
+    <table>
+	<tr>
+	    <th>Account</th>
+	    <th>Delete</th>
+	</tr>
+	{{range .}}
+	<tr>
+	    <td>
+		{{.}}
+	    </td>
+	    <td>
+		Delete
+	    </td>
+	</tr>
+	{{end}}
+    </table>
+</body>
+</html>
+