This is the main dovel repository, it has the Go code to run dovel SMTP server.
Author: b (git@mail.blmayer.dev)
Date: Tue Jul 18 20:03:41 2023 -0300
Parent: 66c46aa
Moved web part out - Added raw handler
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
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
}
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
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)
- }
-}
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
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>
-  
- <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>
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) {
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) {
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)
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))
+}
+
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
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
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
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
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"
-}
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>
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"}}
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> 
-<a href="compose.html?inbox={{.Query.Get "inbox"}}">compose</a> 
-<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>
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> 
-<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>
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> 
-<a href="compose.html?inbox={{.Query.Get "inbox"}}&subj={{.Query.Get "subj"}}">compose</a> 
-<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> 
- <a href="delete?id={{printf "%s/%s" $inbox .ID}}">delete</a>
-</p>
-{{end}}
- </body>
-</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>
-