list

server

This is the main dovel repository, it has the Go code to run dovel SMTP server.

curl https://dovel.email/server.tar tar

1db678f

Author: b (git@mail.blmayer.dev)

Date: Tue Jul 18 20:03:41 2023 -0300

Parent: 66c46aa

Moved web part out

- Added raw handler

Diff

TODO.txt

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/TODO.txt b/TODO.txt
index eb715a1..0c35df6 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -10,6 +10,6 @@
 ☐ Improve web users
 ✓ Add insert attachments button
 ✓ Use XDG desktop for config path
-☐ Support SMTP for sending email?
+✓ Support SMTP for sending email?
 ☐ Add config menu
 ☐ Add denylist filtering

cmd/dovel/backend.go

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/cmd/dovel/backend.go b/cmd/dovel/backend.go
index f06c52f..998937a 100644
--- a/cmd/dovel/backend.go
+++ b/cmd/dovel/backend.go
@@ -1,16 +1,17 @@
 package main
 
 import (
+	"bytes"
 	"crypto"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"net/mail"
 	"strings"
 
 	"blmayer.dev/x/vault"
 
 	"git.derelict.garden/dovel/email/interfaces"
-	"github.com/OfimaticSRL/parsemail"
 	"github.com/emersion/go-smtp"
 )
 
@@ -23,7 +24,7 @@ type Session struct {
 	vault vault.Vault[interfaces.WebUser]
 }
 
-func (s Session) AuthPlain(username, password string) error {
+func (s *Session) AuthPlain(username, password string) error {
 	println("connection sent", username, password)
 	if s.vault.Validate(username, password) {
 		s.user = username
@@ -31,19 +32,21 @@ func (s Session) AuthPlain(username, password string) error {
 	return nil
 }
 
-func (s Session) Mail(from string, opts *smtp.MailOptions) error {
+func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
 	println("Mail from:", from)
 	s.from = from
 	return nil
 }
 
-func (s Session) Rcpt(to string) error {
+func (s *Session) Rcpt(to string) error {
 	println("Rcpt to:", to)
 	s.tos = append(s.tos, to)
 	return nil
 }
 
-func (s Session) Data(r io.Reader) error {
+func (s *Session) Data(r io.Reader) error {
+	content, _ := ioutil.ReadAll(r)
+
 	from, err := mail.ParseAddress(s.from)
 	if err != nil {
 		println("parse address", err.Error())
@@ -56,16 +59,10 @@ func (s Session) Data(r io.Reader) error {
 		return err
 	}
 
-	email, err := parsemail.Parse(r)
-	if err != nil {
-		println("parse email", err)
-		return err
-	}
-	m := interfaces.ToEmail(email)
 	for _, to := range tos {
 		localdom := strings.Split(to.Address, "@")
 		if h, ok := s.handlers[localdom[1]]; ok {
-			err = h.Save(m)
+			err = h.Save(s.from, to.Address, bytes.NewReader(content))
 		} else {
 			if s.user == "" {
 				return fmt.Errorf("needs auth")
@@ -74,7 +71,7 @@ func (s Session) Data(r io.Reader) error {
 			if !ok {
 				return fmt.Errorf("from is wrong")
 			}
-			err = h.Send(m, interfaces.Opt{})
+			err = h.Send(s.from, to.Address, bytes.NewReader(content))
 		}
 		if err != nil {
 			println("handler error", err.Error())
@@ -85,9 +82,9 @@ func (s Session) Data(r io.Reader) error {
 	return nil
 }
 
-func (s Session) Reset() { }
+func (s *Session) Reset() { }
 
-func (s Session) Logout() error {
+func (s *Session) Logout() error {
 	println("logged out")
 	return nil
 }
@@ -98,6 +95,6 @@ type backend struct {
 }
 
 func (b backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
-	return Session{handlers: b.Handlers, tos:[]string{}}, nil
+	return &Session{handlers: b.Handlers, tos:[]string{}}, nil
 }
 

cmd/dovel/main.go

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go
index 9f2b4d9..d046520 100644
--- a/cmd/dovel/main.go
+++ b/cmd/dovel/main.go
@@ -6,13 +6,13 @@ import (
 	"net/http"
 	"os"
 	"path"
-	"html/template"
 	"time"
 
 	"git.derelict.garden/dovel/email/config"
 	"git.derelict.garden/dovel/email/interfaces"
 	"git.derelict.garden/dovel/email/interfaces/file"
 	"git.derelict.garden/dovel/email/interfaces/gwi"
+	"git.derelict.garden/dovel/email/interfaces/raw"
 
 	"blmayer.dev/x/vault"
 
@@ -80,21 +80,11 @@ func main() {
 				panic(err)
 			}
 
-			web := webHandler{
-				assetsPath: hand.Templates,
-				vault:      v,
-				mailer:     mail,
-			}
-			if hand.Templates != "" {
-				web.templates, err = template.ParseGlob(hand.Templates + "/*.html")
-				if err != nil {
-					panic(err)
-				}
-				http.HandleFunc(hand.Domain+"/", web.indexHandler())
-				http.HandleFunc(hand.Domain+"/move", web.moveHandler)
-				http.HandleFunc(hand.Domain+"/delete", web.deleteHandler)
-				http.HandleFunc(hand.Domain+"/assets/", web.assetsHandler())
-				http.HandleFunc(hand.Domain+"/out", web.sendHandler())
+			b.Handlers[hand.Domain] = mail
+		case "raw":
+			mail, err := raw.NewRawHandler(hand)
+			if err != nil {
+				panic(err)
 			}
 
 			b.Handlers[hand.Domain] = mail

cmd/dovel/web.go

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/cmd/dovel/web.go b/cmd/dovel/web.go
deleted file mode 100644
index 9ffb278..0000000
--- a/cmd/dovel/web.go
+++ /dev/null
@@ -1,169 +0,0 @@
-package main
-
-import (
-	"html/template"
-	"io"
-	"net/http"
-	"net/url"
-	"path"
-	"time"
-
-	"git.derelict.garden/dovel/email/interfaces"
-
-	"blmayer.dev/x/vault"
-)
-
-type webHandler struct {
-	assetsPath string
-	templates  *template.Template
-	vault      vault.Vault[interfaces.WebUser]
-	mailer     interfaces.Mailer
-}
-
-func (h webHandler) indexHandler() http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		user, pass, _ := r.BasicAuth()
-		if user == "" || !h.vault.Validate(user, pass) {
-			w.Header().Add("WWW-Authenticate", "Basic")
-			http.Error(w, "wrong auth", http.StatusUnauthorized)
-			return
-		}
-
-		if r.URL.Path == "/" {
-			r.URL.Path = "/index.html"
-		}
-
-		u := h.vault.GetUser(user)
-		err := h.templates.ExecuteTemplate(
-			w, r.URL.Path[1:],
-			struct {
-				Query  url.Values
-				Mailer interfaces.Mailer
-				User   interfaces.WebUser
-				TimeZone *time.Location
-			}{
-				r.URL.Query(),
-				h.mailer,
-				u,
-				time.FixedZone(u.TimeZone.Name, u.TimeZone.Offset),
-			},
-		)
-		if err != nil {
-			println("execute", err.Error())
-		}
-	}
-}
-
-func (h webHandler) moveHandler(w http.ResponseWriter, r *http.Request) {
-	user, pass, _ := r.BasicAuth()
-	if user == "" || !h.vault.Validate(user, pass) {
-		w.Header().Add("WWW-Authenticate", "Basic")
-		http.Error(w, "wrong auth", http.StatusUnauthorized)
-		return
-	}
-
-	id := r.URL.Query().Get("id")
-	to := r.URL.Query().Get("to")
-	if id == "" || to == "" {
-		http.Error(w, "empty parameters", http.StatusBadRequest)
-		return
-	}
-
-	if err := h.mailer.Move(id, to); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-
-	http.Redirect(w, r, "/", http.StatusFound)
-}
-
-func (h webHandler) deleteHandler(w http.ResponseWriter, r *http.Request) {
-	user, pass, _ := r.BasicAuth()
-	if user == "" || !h.vault.Validate(user, pass) {
-		w.Header().Add("WWW-Authenticate", "Basic")
-		http.Error(w, "wrong auth", http.StatusUnauthorized)
-		return
-	}
-
-	id := r.URL.Query().Get("id")
-	if id == "" {
-		http.Error(w, "empty id", http.StatusBadRequest)
-		return
-	}
-
-	if err := h.mailer.Delete(id); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-
-	http.Redirect(w, r, "/", http.StatusFound)
-}
-
-func (h webHandler) assetsHandler() http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		http.ServeFile(w, r, path.Join(h.assetsPath, r.URL.Path[1:]))
-	}
-}
-
-func (h webHandler) sendHandler() http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		println("file handling send email")
-		user, pass, _ := r.BasicAuth()
-		if user == "" || !h.vault.Validate(user, pass) {
-			w.Header().Add("WWW-Authenticate", "Basic")
-			http.Error(w, "wrong auth", http.StatusUnauthorized)
-			return
-		}
-
-		if err := r.ParseMultipartForm(20 * 1024 * 1024); err != nil {
-			println("form error: " + err.Error())
-			http.Error(w, "form error"+err.Error(), http.StatusNotAcceptable)
-			return
-		}
-		fo := r.MultipartForm
-
-		email := interfaces.Email{
-			From:        fo.Value["from"][0],
-			To:          fo.Value["to"],
-			Cc:          fo.Value["cc"],
-			Subject:     fo.Value["subject"][0],
-			Date:        time.Now(),
-			Body:        fo.Value["body"][0],
-			Attachments: map[string]interfaces.Attachment{},
-		}
-
-		for _, fi := range fo.File {
-			attach, err := fi[0].Open()
-			if err != nil {
-				println("error getting attachment: " + err.Error())
-				continue
-			}
-			defer attach.Close()
-
-			content, err := io.ReadAll(attach)
-			if err != nil {
-				println("error getting attachment: " + err.Error())
-				continue
-			}
-			email.Attachments[fi[0].Filename] = interfaces.Attachment{
-				Name:        fi[0].Filename,
-				Data:        content,
-				ContentType: fi[0].Header.Get("Content-Type"),
-			}
-		}
-
-		opts := interfaces.Opt{
-			Encrypt: len(fo.Value["encrypt"]) > 0,
-		}
-		if len(fo.Value["key"]) > 0 {
-			opts.Key = fo.Value["key"][0]
-		}
-		err := h.mailer.Send(email, opts)
-		if err != nil {
-			println("send error: " + err.Error())
-			http.Error(w, "send error"+err.Error(), http.StatusInternalServerError)
-			return
-		}
-		http.Redirect(w, r, "/", http.StatusFound)
-	}
-}

dovel

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/dovel b/dovel
new file mode 100755
index 0000000..c4466aa
Binary files /dev/null and b/dovel differ

index.html

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/index.html b/index.html
deleted file mode 100644
index f4e7e72..0000000
--- a/index.html
+++ /dev/null
@@ -1,198 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-	<title>dovel email</title>
-	<meta charset="UTF-8">
-	<meta name="author" content="Brian Lee Mayer">
-	<meta name="description" content="Dovel is a simple and extensible SMTP server that let's you send and receive email using a self hosted instance with minimum configuration. Dovel is free and open source software written in Go.">
-	<meta name="language" content="english">
-	<meta name="keywords" content="email, dovel, blmayer, self host, software, smtp, web, interface">
-	<meta name="viewport" content="width=device-width, initial-scale=1">
-	<meta name="color-scheme" content="light dark">
-	<style>
-body {
-	max-width: 600px;
-	margin: 60px auto;
-	padding: 20px;
-	font-family: sans-serif;
-}
-
-pre {
-	font-weight: bold;
-	background: lightgray;
-	padding: 8px;
-	border-radius: 8px;
-}
-
-ol li {
-	padding-bottom: 10px;
-}
-
-tt {
-	font-weight: bold;
-}
-
-@media (prefers-color-scheme: dark) {
-	pre {
-		background: darkslategray;
-}
-	</style>
-</head>
-<body>
-	<center>
-<pre style="background:none">
-_________                 ______         
-______  /________   _________  /         
-_  __  /_  __ \_ | / /  _ \_  /          
-/ /_/ / / /_/ /_ |/ //  __/  /           
-\__,_/  \____/_____/ \___//_/   ________ 
-                               |   ~~ # |
-                               |  ---   |
-                               '--------'
-
-
-
-</pre>
-		<i>Self host your email in 4 easy steps.</i>
-		<p>
-		<a href=https://goreportcard.com/report/blmayer.dev/x/dovel><img src=https://goreportcard.com/badge/blmayer.dev/x/dovel></a>
-		&emsp;
-		<a href=https://pkg.go.dev/blmayer.dev/x/dovel><img src=https://pkg.go.dev/badge/blmayer.dev/x/dovel.svg></a>
-		</p>
-	</center>
-
-	<h2>About</h2>
-	Dovel is a SMTP server that sends and receives emails acording to a
-	simple configuration file. It also comes with an optional web interface
-	that you can use to browse your emails. It is designed to be simple and
-	easy to use, and will remain so. Dovel is great because:
-
-	<ul>
-		<li>
-			Is lightweight: Uses only a few MBs of RAM and loads
-			pages super fast.
-		</li>
-		<li>
-			Is easy to configure: Needs just one JSON file.
-		</li>
-		<li>
-			Is easy to use: The design is simple and extensible.
-		</li>
-		<li>
-			Is customizable: Pages use <i>templates</i>, so
-			everything can be changed.
-		</li>
-		<li>
-			Has a GWI interface: Colaborate on git repositories
-			using <a href=//blmayer.dev/x/gwi>gwi</a>.
-		</li>
-		<li>Supports DKIM</li>
-	</ul>
-	This project is under the BSD-3-Clause license, its code can be
-	found in our <a href=//git.derelict.garden/dovel/email>git repository</a>. 
-
-	<h3>Roadmap</h3>
-	Dovel is in early stages and in active development, this means there
-	is a lot to be done. The major features planned are as follows:
-	<ul>
-		<li><s>Support DKIM.</s></li>
-		<li><s>Multiple inboxes.</s></li>
-		<li><s>Support moving and deleting email on web.</s></li>
-		<li><s>Dark theme</s></li>
-		<li><s>PGP support.</s></li>
-		<li>Multiple users.</li>
-		<li>Implement mailing lists behaviour.</li>
-	</ul>
-
-	The detailed development features and progress can be found on our
-	<a href=//blmayer.dev/x/dovel/files/TODO.txt>TODO</a> file.
-
-	<h2>Installation</h2>
-	To install <tt>dovel</tt> you need <tt>golang</tt> propperly installed,
-	and a port, normally 25, this guide works for MacOS, Linux and BSD
-	systems.
-	<ol>
-		<li>
-			Install dovel by running:
-			<tt>go install git.derelict.garden/dovel/email/cmd/dovel</tt>.
-		</li>
-		<li>
-			Configure dovel by creating the configuration file, an
-			example is provided bellow. Running <tt>dovel</tt>
-			is enough to start.
-		</li>
-		<li>
-			Configure your MX record in your domain registrar to
-			point to your server. This is needed to receive email.
-		</li>
-		<li>
-			Configure other DNS records as you wish in your domain
-			registrar such as PTR, SPF, DKIM or DMARC to improve
-			sending emails. Dovel supports DKIM by passing the key
-			path in the configuration file.
-		</li>
-	</ol>
-
-	<h3>Example configuration</h3>
-	Dovel needs the file <tt>~/.config/dovel/config.json</tt> is order to
-	work correctly, otherwise it will use default settings which may not
-	suit you.
-	<pre>
-{
-    "webport": "8000",
-    "server": {
-	"address": ":25",
-	"domain": "dovel.email",
-	"inboxes": [
-	    {
-		"domain": "dovel.email",
-		"handler": "file",
-		"root": "mail/user",
-		"dKIMKeyPath": "dkim.priv",
-		"templates": "www"
-	    }
-	]
-    }
-}</pre>
-
-	Dovel can handle email for different domains, so you can have multiple
-	inboxes. This configuration uses the <i>file</i> handler, which saves
-	emails separated by the receiver and subject. For example, emails
-	addressed to <i>joe@user.dovel.email</i> with subject <i>test</i> will
-	be saved in the <tt>mail/user/joe/test/</tt> folder.
-
-	<p>
-		To see all options please refer to the 
-		<a href=//pkg.go.dev/git.derelict.garden/dovel/email> go docs</a>.
-	</p>
-
-	<h2>Using</h2>
-	Using <tt>dovel</tt> email server and the web interface is meant to be
-	super easy: the server uses the configuration above, you can add more
-	inboxes. The web interface is much more interesting: It uses the
-	Golang <tt>template</tt> package to build pages. 
-
-	<p>
-		As a starting point, look at the <tt>www/</tt> folder in our
-		git repo. They are templates that you can customize to your
-		needs. Once you're done simply restart the <tt>dovel</tt>
-		application to apply changes.
-	</p>
-
-	<h3>Functions</h3>
-	In addition to the template features provided by golang, this project
-	defines some functions to improve its capabilities, the go
-	documentation explains this throughtly.
-
-	<h2>Contributing</h2>
-	Sending bug reports, starting discussions and sending patches can be
-	done easily by email, this project follows the convention defined
-	on <a href=//git.derelict.garden>derelict garden's git</a>.
-
-	<p>
-		Financial support is also greatly appreciated, you can donate
-		any amount by using this <a href=//ko-fi.com/blmayer>ko-fi</a>
-		link. Thank you!
-	</p>
-</body>
-</html>

interfaces/file/file.go

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/interfaces/file/file.go b/interfaces/file/file.go
index 579c275..5db9a65 100644
--- a/interfaces/file/file.go
+++ b/interfaces/file/file.go
@@ -4,20 +4,22 @@
 package file
 
 import (
+	"bufio"
 	"bytes"
 	"crypto"
 	"crypto/x509"
 	"encoding/pem"
 	"fmt"
+	"io"
 	"io/ioutil"
-	"mime/multipart"
 	"net"
+	"net/mail"
 	"net/smtp"
+	"net/textproto"
 	"os"
 	"path"
 	"sort"
 	"strings"
-	"time"
 
 	"git.derelict.garden/dovel/email/config"
 	"git.derelict.garden/dovel/email/interfaces"
@@ -28,8 +30,8 @@ import (
 	"github.com/OfimaticSRL/parsemail"
 	"github.com/emersion/go-msgauth/dkim"
 
-	"github.com/ProtonMail/gopenpgp/v2/helper"
 	pgp "github.com/ProtonMail/gopenpgp/v2/crypto"
+	"github.com/ProtonMail/gopenpgp/v2/helper"
 )
 
 // FileHandler is used to configure the file email handler.
@@ -77,7 +79,14 @@ func NewFileHandler(c config.InboxConfig, v vault.Vault[interfaces.WebUser], fs
 	return f, err
 }
 
-func (f FileHandler) Save(email interfaces.Email) error {
+func (f FileHandler) Save(from, to string, r io.Reader) error {
+	m, err := parsemail.Parse(r)
+	if err != nil {
+		println("mail parse", err.Error())
+		return err
+	}
+
+	email := interfaces.ToEmail(m)
 	mailDir := path.Join(f.root, email.To[0], email.Subject)
 	os.MkdirAll(mailDir, os.ModeDir|0o700)
 
@@ -117,39 +126,29 @@ func (f FileHandler) SaveSent(email interfaces.Email) error {
 	return nil
 }
 
-func (f FileHandler) Send(mail interfaces.Email, opts interfaces.Opt) error {
-	mail.ID = fmt.Sprintf("%d@%s", mail.Date.Unix(), f.domain)
-
-	body := bytes.Buffer{}
-	form := multipart.NewWriter(&body)
-
-	// headers
-	body.WriteString("MIME-Version: 1.0\r\n")
-	body.WriteString("From: " + mail.From + "\r\n")
-	body.WriteString("To: " + strings.Join(mail.To, ", ") + "\r\n")
-	if len(mail.Cc) > 0 {
-		body.WriteString("Cc: " + strings.Join(mail.Cc, ", ") + "\r\n")
+func (h FileHandler) Send(from, to string, raw io.Reader) error {
+	email, err := mail.ReadMessage(raw)
+	if err != nil {
+		return err
+	}
+	addrs, err := mail.ParseAddressList(to)
+	if err != nil {
+		return err
+	}
+	tos := []string{}
+	for _, addr := range addrs {
+		tos = append(tos, addr.Address)
 	}
 
-	body.WriteString(fmt.Sprintf("Message-ID: <%s>\r\n", mail.ID))
-
-	body.WriteString("Date: " + mail.Date.Format(time.RFC1123Z) + "\r\n")
-	body.WriteString("Subject: " + mail.Subject + "\r\n")
-	body.WriteString("Content-Type: multipart/mixed; boundary=" + form.Boundary() + "\r\n\r\n")
-
-	for name, fi := range mail.Attachments {
-		part, err := form.CreateFormFile(name, name)
-		if err != nil {
-			println("error creating form file: " + err.Error())
-			continue
-		}
-
-		part.Write(fi.Data)
+	body, err := ioutil.ReadAll(email.Body)
+	if err != nil {
+		return err
 	}
+	for _, to := range addrs {
+		msg := string(body)
 
-	for _, to := range mail.To {
 		// dns mx for email
-		addr := strings.Split(to, "@")
+		addr := strings.Split(to.Address, "@")
 		mxs, err := net.LookupMX(addr[1])
 		if err != nil {
 			return err
@@ -158,67 +157,52 @@ func (f FileHandler) Send(mail interfaces.Email, opts interfaces.Opt) error {
 			return err
 		}
 
-		if opts.Encrypt {
-			text, err := form.CreatePart(
-				map[string][]string{
-					"Content-Type": {"application/pgp-encrypted"},
-				},
-			)
+		key, err := wkd.FetchPGPKey(addr[0], addr[1])
+		if err != nil {
+			return err
+		}
+		if key != "" {
+			email.Header["Content-Type"] = []string{"application/pgp-encrypted"}
+			msg, err = helper.EncryptMessageArmored(key, msg)
 			if err != nil {
 				return err
 			}
+		}
 
-			// grab public key if wanted
-			if opts.Key == "" {
-				opts.Key, err = wkd.FetchPGPKey(addr[0], addr[1])
-				if err != nil {
-					return err
-				}
-			}
-			cypher, err := helper.EncryptMessageArmored(opts.Key, mail.Body)
-			if err != nil {
-				return err
-			}
-			text.Write([]byte(cypher))
-		} else {
-			text, err := form.CreatePart(
-				map[string][]string{
-					"Content-Type": {"text/plain; charset=\"UTF-8\""},
-				},
-			)
-			if err != nil {
-				return err
-			}
-			text.Write([]byte(mail.Body))
+		payload := bytes.Buffer{}
+		writer := textproto.NewWriter(bufio.NewWriter(&payload))
+		for k, v := range email.Header {
+			writer.PrintfLine("%s: %s", k, strings.Join(v, ", "))
 		}
-		form.Close()
+		writer.PrintfLine("")
+		payload.Write([]byte(msg))
 
 		// dkim
-		payload := bytes.Buffer{}
+		res := bytes.Buffer{}
 		options := &dkim.SignOptions{
-			Domain:   f.domain,
+			Domain:   h.domain,
 			Selector: "dkim",
-			Signer:   f.privateKey,
+			Signer:   h.privateKey,
 		}
-		if err := dkim.Sign(&payload, &body, options); err != nil {
+		err = dkim.Sign(&res, &payload, options)
+		if err != nil {
 			println("failed to sign body:", err.Error())
 		}
-		mail.Raw = payload.Bytes()
 
 		server := mxs[0].Host + ":smtp"
 		err = smtp.SendMail(
 			server,
 			nil,
-			mail.From,
-			[]string{to},
-			mail.Raw,
+			from,
+			tos,
+			res.Bytes(),
 		)
 		if err != nil {
 			return err
 		}
 
 	}
-	return f.SaveSent(mail)
+	return h.Save(from, to, raw)
 }
 
 func (f FileHandler) Mailboxes(folder string) ([]interfaces.Mailbox, error) {

interfaces/gwi/gwi.go

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/interfaces/gwi/gwi.go b/interfaces/gwi/gwi.go
index b1dc3e8..f8356f5 100644
--- a/interfaces/gwi/gwi.go
+++ b/interfaces/gwi/gwi.go
@@ -2,6 +2,7 @@
 package gwi
 
 import (
+	"bufio"
 	"bytes"
 	"crypto"
 	"crypto/x509"
@@ -10,21 +11,23 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
-	"mime/multipart"
 	"net"
+	"net/mail"
 	"net/smtp"
+	"net/textproto"
 	"os"
 	"path"
 	"sort"
 	"strings"
-	"time"
 
 	"git.derelict.garden/dovel/email/config"
 	"git.derelict.garden/dovel/email/interfaces"
+	"git.derelict.garden/dovel/email/util/wkd"
 
 	"blmayer.dev/x/vault"
-	
+
 	"github.com/OfimaticSRL/parsemail"
+	"github.com/ProtonMail/gopenpgp/v2/helper"
 	"github.com/emersion/go-msgauth/dkim"
 )
 
@@ -65,7 +68,14 @@ func NewGWIHandler(c config.InboxConfig, vault vault.Vault[interfaces.WebUser])
 
 // Save saves emails to the correct repo using the subject to
 // separate them. Subject field must be of form "[%s] %s".
-func (g GWIHandler) Save(email interfaces.Email) error {
+func (g GWIHandler) Save(from, to string, r io.Reader) error {
+	m, err := parsemail.Parse(r)
+	if err != nil {
+		println("mail parse", err.Error())
+		return err
+	}
+
+	email := interfaces.ToEmail(m)
 	userRepoDomain := strings.Split(email.To[0], "@")
 	userRepo := strings.Split(userRepoDomain[0], "/")
 	if len(userRepo) != 2 {
@@ -140,65 +150,32 @@ The GWI team.`,
 	)
 	email.Subject = "New mail on project " + repo
 	email.From = fmt.Sprintf("%s/%s@%s", user, repo, g.domain)
-	return g.Send(email, interfaces.Opt{})
+	return g.Send(email.From, strings.Join(email.To, ", "), strings.NewReader(email.Body))
 }
 
-func (g GWIHandler) Send(mail interfaces.Email, opts interfaces.Opt) error {
-	mail.ID = fmt.Sprintf("%d@%s", mail.Date.Unix(), g.domain)
-
-	body := bytes.Buffer{}
-	form := multipart.NewWriter(&body)
-
-	// headers
-	body.WriteString("MIME-Version: 1.0\r\n")
-	body.WriteString("From: " + mail.From + "\r\n")
-	body.WriteString("To: " + strings.Join(mail.To, ", ") + "\r\n")
-	if len(mail.Cc) > 0 {
-		body.WriteString("Cc: " + strings.Join(mail.Cc, ", ") + "\r\n")
-	}
-
-	body.WriteString(fmt.Sprintf("Message-ID: <%s>\r\n", mail.ID))
-
-	body.WriteString("Date: " + mail.Date.Format(time.RFC1123Z) + "\r\n")
-	body.WriteString("Subject: " + mail.Subject + "\r\n")
-	body.WriteString("Content-Type: multipart/mixed; boundary=" + form.Boundary() + "\r\n\r\n")
-
-	text, err := form.CreatePart(
-		map[string][]string{
-			"Content-Type": {"text/plain; charset=\"UTF-8\""},
-		},
-	)
+func (h GWIHandler) Send(from, to string, raw io.Reader) error {
+	email, err := mail.ReadMessage(raw)
 	if err != nil {
 		return err
 	}
-	text.Write([]byte(mail.Body))
-
-	for name, fi := range mail.Attachments {
-		part, err := form.CreateFormFile(name, name)
-		if err != nil {
-			println("error creating form file: " + err.Error())
-			continue
-		}
-
-		part.Write(fi.Data)
+	addrs, err := mail.ParseAddressList(to)
+	if err != nil {
+		return err
 	}
-	form.Close()
-
-	// dkim
-	payload := bytes.Buffer{}
-	options := &dkim.SignOptions{
-		Domain:   g.domain,
-		Selector: "dkim",
-		Signer:   g.privateKey,
+	tos := []string{}
+	for _, addr := range addrs {
+		tos = append(tos, addr.Address)
 	}
-	if err := dkim.Sign(&payload, &body, options); err != nil {
-		println("failed to sign body:", err.Error())
+
+	body, err := ioutil.ReadAll(email.Body)
+	if err != nil {
+		return err
 	}
-	mail.Body = payload.String()
+	for _, to := range addrs {
+		msg := string(body)
 
-	// dns mx for email
-	for _, to := range mail.To {
-		addr := strings.Split(to, "@")
+		// dns mx for email
+		addr := strings.Split(to.Address, "@")
 		mxs, err := net.LookupMX(addr[1])
 		if err != nil {
 			return err
@@ -207,19 +184,52 @@ func (g GWIHandler) Send(mail interfaces.Email, opts interfaces.Opt) error {
 			return err
 		}
 
+		key, err := wkd.FetchPGPKey(addr[0], addr[1])
+		if err != nil {
+			return err
+		}
+		if key != "" {
+			email.Header["Content-Type"] = []string{"application/pgp-encrypted"}
+			msg, err = helper.EncryptMessageArmored(key, msg)
+			if err != nil {
+				return err
+			}
+		}
+
+		payload := bytes.Buffer{}
+		writer := textproto.NewWriter(bufio.NewWriter(&payload))
+		for k, v := range email.Header {
+			writer.PrintfLine("%s: %s", k, strings.Join(v, ", "))
+		}
+		writer.PrintfLine("")
+		payload.Write([]byte(msg))
+
+		// dkim
+		res := bytes.Buffer{}
+		options := &dkim.SignOptions{
+			Domain:   h.domain,
+			Selector: "dkim",
+			Signer:   h.privateKey,
+		}
+		err = dkim.Sign(&res, &payload, options)
+		if err != nil {
+			println("failed to sign body:", err.Error())
+		}
+
 		server := mxs[0].Host + ":smtp"
 		err = smtp.SendMail(
 			server,
 			nil,
-			mail.From,
-			[]string{to},
-			[]byte(mail.Body),
+			from,
+			tos,
+			res.Bytes(),
 		)
 		if err != nil {
 			return err
 		}
+
 	}
-	return nil
+	return h.Save(from, to, raw)
 }
 
 func (f GWIHandler) Mailboxes(folder string) ([]interfaces.Mailbox, error) {

interfaces/main.go

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/interfaces/main.go b/interfaces/main.go
index ad6713b..a09c95b 100644
--- a/interfaces/main.go
+++ b/interfaces/main.go
@@ -115,8 +115,8 @@ func ToEmail(mail parsemail.Email) Email {
 }
 
 type Mailer interface {
-	Send(mail Email, opts Opt) error
-	Save(mail Email) error
+	Save(from, to string, raw io.Reader) error
+	Send(from, to string, raw io.Reader) error
 	Mailboxes(folder string) ([]Mailbox, error)
 	Mails(folder string) ([]Email, error)
 	Mail(file string) (Email, error)

interfaces/raw/raw.go

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/interfaces/raw/raw.go b/interfaces/raw/raw.go
new file mode 100644
index 0000000..175a7e4
--- /dev/null
+++ b/interfaces/raw/raw.go
@@ -0,0 +1,335 @@
+// web adds the common email file handling, but with a twist: here we save
+// emails first by inbox and then subject. This package also delivers some
+// functions to be used in the templates, for the web interface.
+package raw
+
+import (
+	"bufio"
+	"bytes"
+	"crypto"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net"
+	"net/mail"
+	"net/smtp"
+	"net/textproto"
+	"os"
+	"path"
+	"sort"
+	"strings"
+	"text/template"
+	"time"
+
+	"git.derelict.garden/dovel/email/config"
+	"git.derelict.garden/dovel/email/interfaces"
+	"git.derelict.garden/dovel/email/util/wkd"
+
+	"github.com/OfimaticSRL/parsemail"
+	"github.com/emersion/go-msgauth/dkim"
+
+	pgp "github.com/ProtonMail/gopenpgp/v2/crypto"
+	"github.com/ProtonMail/gopenpgp/v2/helper"
+)
+
+// RawHandler is used to configure the file email handler.
+// Root is where mail should be saved, Templates is an optional field that
+// is the path to templates, to be used in the web server; Password is the
+// password for the x user, for now this is very simple; Domain is used to
+// filter and separate emails, only emails sent to one of your domains are
+// saved, each according to its configuration.
+type RawHandler struct {
+	root       string
+	domain     string
+	privateKey crypto.Signer
+	keyText string
+	templ *template.Template
+}
+
+func NewRawHandler(c config.InboxConfig) (RawHandler, error) {
+	h := RawHandler{root: c.Root, domain: c.Domain}
+
+	var err error
+	h.templ, err = template.ParseFiles(c.Templates)
+	if err != nil {
+		return h, err
+	}
+
+	if c.DKIMKeyPath != "" {
+		key, err := ioutil.ReadFile(c.DKIMKeyPath)
+		if err != nil {
+			return h, err
+		}
+		h.keyText = string(key)
+
+		block, _ := pem.Decode(key)
+		if block == nil {
+			return h, err
+		}
+		privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+		if err != nil {
+			return h, err
+		}
+		h.privateKey = privKey.(crypto.Signer)
+	}
+
+	return h, err
+}
+
+func (h RawHandler) Save(from, to string, raw io.Reader) error {
+	mail, err := mail.ReadMessage(raw)
+	if err != nil {
+		return err
+	}
+
+	content, err := ioutil.ReadAll(mail.Body)
+	if err != nil {
+		println("file read", err.Error())
+		return err
+	}
+
+	// try to decrypt
+	if h.privateKey != nil && pgp.IsPGPMessage(string(content)) {
+		println("decrypting")
+		txt, err := helper.DecryptMessageArmored(
+			h.keyText, 
+			nil, 
+			string(content),
+		)
+		if err != nil {
+			println(err)
+		}
+		content = []byte(txt)
+	}
+
+	now := time.Now().Format(time.RFC3339)
+	subj := mail.Header.Get("Subject")
+	mailDir := path.Join(h.root, to, subj)
+	os.MkdirAll(mailDir, os.ModeDir|0o700)
+	name := fmt.Sprintf("%s:%s.mail", from, now)
+
+	file, err := os.Create(path.Join(mailDir, name))
+	if err != nil {
+		return err
+	}
+
+	writer := textproto.NewWriter(bufio.NewWriter(file))
+	for k, v := range mail.Header {
+		writer.PrintfLine("%s: %s", k, strings.Join(v, ", "))
+	}
+	writer.PrintfLine("")
+	file.Write(content)
+
+	return file.Close()
+}
+
+
+func (f RawHandler) SaveSent(email interfaces.Email) error {
+	mailDir := path.Join(f.root, email.From, email.Subject)
+	os.MkdirAll(mailDir, os.ModeDir|0o700)
+
+	file, err := os.Create(path.Join(mailDir, email.ID))
+	if err != nil {
+		println("file create", err.Error())
+		return err
+	}
+	defer file.Close()
+
+	_, err = file.Write(email.Raw)
+	if err != nil {
+		println("file write", err.Error())
+		return err
+	}
+
+	return nil
+}
+
+func (h RawHandler) Send(from, to string, raw io.Reader) error {
+	email, err := mail.ReadMessage(raw)
+	if err != nil {
+		return err
+	}
+	addrs, err := mail.ParseAddressList(to)
+	if err != nil {
+		return err
+	}
+	tos := []string{}
+	for _, addr := range addrs {
+		tos = append(tos, addr.Address)
+	}
+
+	body, err := ioutil.ReadAll(email.Body)
+	if err != nil {
+		return err
+	}
+	for _, to := range addrs {
+		msg := string(body)
+
+		// dns mx for email
+		addr := strings.Split(to.Address, "@")
+		mxs, err := net.LookupMX(addr[1])
+		if err != nil {
+			return err
+		}
+		if len(mxs) == 0 {
+			return err
+		}
+
+		key, err := wkd.FetchPGPKey(addr[0], addr[1])
+		if err != nil {
+			return err
+		}
+		if key != "" {
+			email.Header["Content-Type"] = []string{"application/pgp-encrypted"}
+			msg, err = helper.EncryptMessageArmored(key, msg)
+			if err != nil {
+				return err
+			}
+		}
+
+		payload := bytes.Buffer{}
+		writer := textproto.NewWriter(bufio.NewWriter(&payload))
+		for k, v := range email.Header {
+			writer.PrintfLine("%s: %s", k, strings.Join(v, ", "))
+		}
+		writer.PrintfLine("")
+		payload.Write([]byte(msg))
+
+		// dkim
+		res := bytes.Buffer{}
+		options := &dkim.SignOptions{
+			Domain:   h.domain,
+			Selector: "dkim",
+			Signer:   h.privateKey,
+		}
+		err = dkim.Sign(&res, &payload, options)
+		if err != nil {
+			println("failed to sign body:", err.Error())
+		}
+
+		server := mxs[0].Host + ":smtp"
+		err = smtp.SendMail(
+			server,
+			nil,
+			from,
+			tos,
+			res.Bytes(),
+		)
+		if err != nil {
+			return err
+		}
+
+	}
+	return h.Save(from, to, raw)
+}
+
+func (f RawHandler) Mailboxes(folder string) ([]interfaces.Mailbox, error) {
+	fmt.Println("mailer mailboxes for", folder)
+	dir, err := os.ReadDir(path.Join(f.root, folder))
+	if err != nil {
+		fmt.Println("readDir error:", err.Error())
+		return nil, err
+	}
+
+	var threads []interfaces.Mailbox
+	for _, d := range dir {
+		if !d.IsDir() {
+			continue
+		}
+
+		info, err := d.Info()
+		if err != nil {
+			fmt.Println("dir info", err.Error())
+			continue
+		}
+		t := interfaces.Mailbox{
+			Title:   d.Name(),
+			LastMod: info.ModTime(),
+		}
+
+		threads = append(threads, t)
+	}
+	sort.Slice(
+		threads,
+		func(i, j int) bool {
+			return threads[i].LastMod.After(threads[j].LastMod)
+		},
+	)
+
+	return threads, nil
+}
+
+func (f RawHandler) Mails(folder string) ([]interfaces.Email, error) {
+	fmt.Println("mailer mails for", folder)
+
+	dir := path.Join(f.root, folder)
+	threadDir, err := os.ReadDir(dir)
+	if err != nil {
+		fmt.Println("readDir error:", err.Error())
+		return nil, err
+	}
+
+	var emails []interfaces.Email
+	for _, t := range threadDir {
+		mail, err := f.Mail(path.Join(folder, t.Name()))
+		if err != nil {
+			fmt.Println("mail error:", err.Error())
+			continue
+		}
+
+		emails = append(emails, mail)
+	}
+	sort.Slice(
+		emails,
+		func(i, j int) bool {
+			return emails[i].Date.Before(emails[j].Date)
+		},
+	)
+	fmt.Println("found", len(emails), "emails")
+
+	return emails, err
+}
+
+func (f RawHandler) Mail(file string) (interfaces.Email, error) {
+	fmt.Println("mailer getting", file)
+
+	mailFile, err := os.Open(path.Join(f.root, file))
+	if err != nil {
+		fmt.Println("open mail error:", err.Error())
+		return interfaces.Email{}, err
+	}
+	defer mailFile.Close()
+
+	mail, err := parsemail.Parse(mailFile)
+	if err != nil {
+		fmt.Println("email parse error:", err.Error())
+		return interfaces.Email{}, err
+	}
+
+	// try to decrypt
+	if f.privateKey != nil && pgp.IsPGPMessage(mail.TextBody) {
+		txt, err := helper.DecryptMessageArmored(f.keyText, nil, mail.TextBody)
+		if err != nil {
+			println(err)
+		}
+		mail.TextBody = txt
+	}
+	return interfaces.ToEmail(mail), nil
+}
+
+// Move is used to move messages between folders. The id parameter
+// is the message id to be moved and to is the destination folder.
+// Root is added automatically.
+func (f RawHandler) Move(id, to string) error {
+	return os.Rename(path.Join(f.root, id), path.Join(f.root, to, "/"))
+}
+
+// Delete is used to delete messages. The id parameter
+// is the message id to be deleted.
+// Root is added automatically.
+func (f RawHandler) Delete(id string) error {
+	return os.Remove(path.Join(f.root, id))
+}
+

www/assets/apple-touch-icon.png

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/www/assets/apple-touch-icon.png b/www/assets/apple-touch-icon.png
deleted file mode 100644
index 13b9018..0000000
Binary files a/www/assets/apple-touch-icon.png and /dev/null differ

www/assets/favicon.ico

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/www/assets/favicon.ico b/www/assets/favicon.ico
deleted file mode 100644
index 2e1f8df..0000000
Binary files a/www/assets/favicon.ico and /dev/null differ

www/assets/icon.png

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/www/assets/icon.png b/www/assets/icon.png
deleted file mode 100644
index 2798922..0000000
Binary files a/www/assets/icon.png and /dev/null differ

www/assets/logo.png

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/www/assets/logo.png b/www/assets/logo.png
deleted file mode 100644
index de112e2..0000000
Binary files a/www/assets/logo.png and /dev/null differ

www/assets/manifest.json

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/www/assets/manifest.json b/www/assets/manifest.json
deleted file mode 100644
index 59ade4c..0000000
--- a/www/assets/manifest.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-	"short_name": "dovel",
-	"name": "Dovel",
-	"icons": [
-		{
-			"src": "/assets/icon.png",
-			"type": "image/png",
-			"purpose": "maskable",
-			"sizes": "512x512"
-		}
-	],
-	"start_url": "../index.html",
-	"background_color": "#FFF",
-	"display": "standalone"
-}

www/compose.html

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/www/compose.html b/www/compose.html
deleted file mode 100644
index 09bf97e..0000000
--- a/www/compose.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<html>
-	<head>
-{{template "head.html"}}
-	</head>
-	<body>
-<b>composing</b><br>
-<br>
-<form action=/out method=post enctype=multipart/form-data>
-	<table>
-		<tr>
-			<td style="padding-left: 0"><label for="from">From: </label></td>
-			<td><input style="width:100%" name=from {{if .Query.inbox}}value="{{.Query.Get "inbox" }}"{{end}}></td>
-		</tr>
-		<tr>
-			<td style="padding-left: 0"><label>To: </label></td>
-			<td><input style="width:100%" name=to {{if .Query.to}}value="{{.Query.Get "to"}}"{{end}}></td>
-		</tr>
-		<tr>
-			<td style="padding-left: 0"><label>CC: </label></td>
-			<td><input style="width:100%" name=cc></td>
-		</tr>
-		<tr>
-			<td style="padding-left: 0"><label>CCo: </label></td>
-			<td><input style="width:100%" name=cco></td>
-		</tr>
-		<tr>
-			<td style="padding-left: 0"><label>Subject: </label></td>
-			<td><input style="width:100%" name=subject {{if .Query.subj}}value="{{.Query.Get "subj"}}"{{end}}></td>
-		</tr>
-		<tr>
-			<td colspan=2><textarea style="width:100%" name=body rows="7" cols="50" placeholder="Body:"></textarea></td>
-		</tr>
-		<tr>
-			<td colspan=2><label>Attachments: </label><input type=file name=files multiple></td>
-		</tr>
-		<tr>
-			<td colspan=2>
-				<details>
-					<summary><label>Encrypt? </label><input name=encrypt type=checkbox></summary>
-					Override OpenPGP key (optional):<br>
-					<textarea style="width:100%" name=key rows="7" cols="50" placeholder="Begin OpenGPG key..."></textarea>
-				</details>
-			</td>
-		</tr>
-	</table>
-	<input type=submit value=send>
-</form>
-	</body>
-</html>

www/head.html

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/www/head.html b/www/head.html
deleted file mode 100644
index e569140..0000000
--- a/www/head.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<title>dovel</title>
-<link rel=icon href=/assets/favicon.ico>
-<meta name="viewport" content="width=device-width, initial-scale=1"/>
-<meta name="color-scheme" content="light dark"/>
-{{template "style.html"}}

www/inboxes.html

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/www/inboxes.html b/www/inboxes.html
deleted file mode 100644
index 470e079..0000000
--- a/www/inboxes.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-{{template "head.html"}}
-</head>
-<body>
-<h2>{{.Query.Get "inbox"}}</h2>
-
-<a href="index.html">index</a>&emsp;
-<a href="compose.html?inbox={{.Query.Get "inbox"}}">compose</a>&emsp;
-<a href="delete?id={{.Query.Get "inbox"}}">delete</a>
-
-{{$format := "02 Jan 2006 15:04:05"}}
-{{range (.Mailer.Mailboxes (.Query.Get "inbox"))}}
-<p>
-	<a href="mails.html?inbox={{$.Query.Get "inbox"}}&subj={{.Title}}">{{.Title}}</a>
-	<small>{{(.LastMod.In $.TimeZone).Format $format}}</small>
-</p>
-{{end}}
-</body>
-</html>

www/index.html

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/www/index.html b/www/index.html
deleted file mode 100644
index a7cd774..0000000
--- a/www/index.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-	{{template "head.html"}}
-	<link rel="manifest" href="/assets/manifest.json">
-	<link rel="apple-touch-icon" sizes="200x200" href="/logo.png">
-</head>
-<body>
-	<center>
-<pre>
-_________                 ______
-______  /________   _________  /
-_  __  /_  __ \_ | / /  _ \_  / 
-/ /_/ / / /_/ /_ |/ //  __/  /  
-\__,_/  \____/_____/ \___//_/   
-
-</pre>
-	</center>
-<a href=compose.html>compose</a>&emsp;
-<a href=logout.html>logout</a>
-
-{{$format := "02 Jan 2006 15:04:05"}}
-{{range (.Mailer.Mailboxes ".")}}
-<p>
-	<a href="inboxes.html?inbox={{.Title}}">{{.Title}}</a>
-	<small>{{(.LastMod.In $.TimeZone).Format $format}}</small>
-</p>
-{{end}}
-</body>
-</html>

www/mails.html

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/www/mails.html b/www/mails.html
deleted file mode 100644
index 5a8164f..0000000
--- a/www/mails.html
+++ /dev/null
@@ -1,49 +0,0 @@
-<!DOCTYPE html>
-<html>
-	<head>
-{{template "head.html"}}
-	</head>
-	<body>
-<h2>{{.Query.Get "subj"}}</h2>
-
-{{$inbox := printf "%s/%s" (.Query.Get "inbox") (.Query.Get "subj")}}
-<a href="inboxes.html?inbox={{.Query.Get "inbox"}}">inbox</a>&emsp;
-<a href="compose.html?inbox={{.Query.Get "inbox"}}&subj={{.Query.Get "subj"}}">compose</a>&emsp;
-<a href="delete?id={{$inbox}}">delete</a>
-{{range (.Mailer.Mails $inbox)}}
-<p>
-	<details>
-		<summary>
-			{{if eq ($.Query.Get "inbox") .From}}
-			To: {{index .To 0}}
-			{{else}}
-			From: {{.From}}
-			{{end}}<br>
-			Date: {{.Date.Format "Mon, 02 Jan 2006 15:04:05 MST"}}<br>
-			{{if .Cc}}Cc: .Cc{{end}}
-		</summary>
-		<ul>
-		{{range $k, $vs := .Headers}}
-		<li>{{$k}}: {{$vs}}</li>
-		{{end}}
-		</ul>
-	</details>
-	{{if .Attachments}}
-	Attachments:
-	<ul>
-	{{range .Attachments}}
-	<li>
-		<a href="data:{{.ContentType}};base64,{{.Data}}" download="{{.Name}}">{{.Name}}</a>
-	</li>
-	{{end}}
-	</ul>
-	{{end}}
-	{{if .Body}}
-	<pre>{{.Body}}</pre>
-	{{end}}
-	<a href="compose.html?inbox={{$.Query.Get "inbox"}}&subj={{.Subject}}&to={{.From}}">reply</a>&emsp;
-	<a href="delete?id={{printf "%s/%s" $inbox .ID}}">delete</a>
-</p>
-{{end}}
-	</body>
-</html>

www/style.html

commit 1db678f848e36eda4dc38cf5f6c73428f44ab42e
Author: b <git@mail.blmayer.dev>
Date:   Tue Jul 18 20:03:41 2023 -0300

    Moved web part out
    
    - Added raw handler

diff --git a/www/style.html b/www/style.html
deleted file mode 100644
index 4cce72c..0000000
--- a/www/style.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<style>
-	body {
-		max-width: 600px;
-		padding: 0 15px;
-		margin: 40px auto;
-		font-family: sans-serif;
-	}
-	table {
-		width: 100%;
-	}
-	a {
-		color: #888
-	}
-	small {
-		float: right;
-	}
-	input, textarea {
-		color: #888;
-		font-family: monospace;
-	}
-	input {
-		border: none;
-		border-bottom: 1px solid #888;
-	}
-	input[type="submit"] {
-		text-decoration: underline;
-		cursor: pointer;
-		color: #888;
-	}
-	pre {
-		white-space: pre-wrap;
-	}
-</style>
-