Skip to content

Commit

Permalink
SMTP: Adding very basic GUI for easier interactive testing
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas Hinderberger committed Jun 25, 2024
1 parent 1a8d9f5 commit 24e3c2d
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 1 deletion.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2617,6 +2617,10 @@ the messages received by the SMTP server can be reproduced in JSON format.
When both the SMTP server and the HTTP server are enabled, the following
additional endpoints are made available on the HTTP server:
#### /smtp/gui
A very basic HTML/JavaScript GUI that displays and auto-refreshes the received
messages is made available on the `/smtp/gui` endpoint.
#### /smtp
On the `/smtp` endpoint, an index of all received messages will be made
available as JSON in the following schema:
Expand Down
129 changes: 129 additions & 0 deletions internal/smtp/gui.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="en">
<head>

<title>apitest mock SMTP server GUI</title>
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; font-family: monospace; }
body { padding: 20px; box-sizing: border-box; }
noscript { color: red; font-weight: bold; }
.container { display: flex; flex-direction: row; gap: 20px; max-width: 1600px; margin: auto; }
.container nav { flex: 2; }
.container main { flex: 1; }
table, th, td { border: 2px solid grey; border-collapse: collapse; }
th, td { padding: 10px; }
.subject-hdr { width: 100%; }
pre { background-color: #eee; padding: 10px; box-sizing: border-box; max-width: 400px; overflow-x: auto; }
main > p:first-of-type { padding-top: 0; margin-top: 0; }
</style>

</head>

<body>
<noscript>This GUI needs JavaScript to function properly.</noscript>

<div class="container">
<nav>
<table>
<thead>
<tr>
<th>Index</th>
<th>Received</th>
<th>From</th>
<th>To</th>
<th class="subject-hdr">Subject</th>
<th>Details</th>
</tr>
</thead>
<tbody id="indexrows"></tbody>
</table>
</nav>

<main>
<p>
<strong>Message Metadata:</strong>
<pre id="msgmeta"></pre>
</p>

<p>
<strong>Body:</strong><br>
<a id="bodylink" href="javascript:void(0)" target="_blank">Show</a>
</p>

<p>
<strong>Multipart Metadata:</strong>
<pre id="multimeta"></pre>
</p>

<p>
<strong>Multiparts:</strong><br>
<ul id="partlist"></ul>
</p>
</main>
</div>

<script>
const prefix = "{{ .prefix }}"

const bodylink = document.getElementById("bodylink")
const indexrows = document.getElementById("indexrows")
const msgmeta = document.getElementById("msgmeta")
const multimeta = document.getElementById("multimeta")
const partlist = document.getElementById("partlist")

let n_received = 0
let index = {"count": 0, "messages": []}

async function updateIndex() {
response = await fetch(prefix + "/")
index = await response.json()

for (; n_received < index["count"]; n_received++) {
const msg = index["messages"][n_received]

let row = indexrows.insertRow()
let cell = row.insertCell()
cell.textContent = msg["idx"]
cell = row.insertCell()
cell.textContent = msg["receivedAt"]
cell = row.insertCell()
cell.textContent = msg["from"]
cell = row.insertCell()
cell.textContent = msg["to"]
cell = row.insertCell()
cell.textContent = msg["subject"]
cell = row.insertCell()
cell.innerHTML = `<button onclick="javascript:showDetails(${n_received})">Show</button>`
}
}

async function showDetails(idx) {
response = await fetch(`${prefix}/${idx}`)
metadata = await response.json()
msgmeta.textContent = JSON.stringify(metadata, null, 2)

bodylink.href = `${prefix}/${idx}/body`

partlist.innerHTML=""

if (metadata["isMultipart"]) {
response = await fetch(`${prefix}/${idx}/multipart`)
multipart_meta = await response.json()
multimeta.textContent = JSON.stringify(multipart_meta, null, 2)

for (let i=0; i < multipart_meta["count"]; i++) {
let li = document.createElement("li")
li.innerHTML = `<a href="${prefix}/${idx}/multipart/${i}/body" target="_blank">Show Body of Part ${i}</a>`
partlist.append(li)
}
} else {
multimeta.textContent = "Not a multipart message"
}
}

updateIndex()
setInterval(updateIndex, 1000)
</script>

</body>
</html>
24 changes: 23 additions & 1 deletion internal/smtp/http.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
package smtp

import (
_ "embed"
"fmt"
"html/template"
"net/http"
"path"
"regexp"
"strconv"
"strings"

"github.com/programmfabrik/apitest/internal/handlerutil"
"github.com/sirupsen/logrus"
)

//go:embed gui.html
var guiTemplateSrc string
var guiTemplate = template.Must(template.New("gui").Parse(guiTemplateSrc))

type smtpHTTPHandler struct {
server *Server
prefix string
Expand Down Expand Up @@ -41,8 +48,14 @@ func (h *smtpHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
pathParts := strings.Split(path, "/")
fmt.Println(pathParts)

// We know that pathParts must have at least length 1, since empty path
// We now know that pathParts must have at least length 1, since empty path
// was already handled above.

if pathParts[0] == "gui" {
h.handleGUI(w, r)
return
}

idx, err := strconv.Atoi(pathParts[0])
if err != nil {
handlerutil.RespondWithErr(
Expand Down Expand Up @@ -90,6 +103,15 @@ func (h *smtpHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}

func (h *smtpHTTPHandler) handleGUI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")

err := guiTemplate.Execute(w, map[string]any{"prefix": h.prefix})
if err != nil {
logrus.Error("error rendering GUI:", err)
}
}

func (h *smtpHTTPHandler) handleMessageIndex(w http.ResponseWriter, r *http.Request) {
var receivedMessages []*ReceivedMessage

Expand Down

0 comments on commit 24e3c2d

Please sign in to comment.