From 61241f11ac94eb33bd84e399129992250eff56ce Mon Sep 17 00:00:00 2001
From: Ralph Slooten <axllent@gmail.com>
Date: Sat, 23 Mar 2024 16:42:18 +1300
Subject: [PATCH 1/4] Chore: Add labels to Docker image (#267)

---
 Dockerfile | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/Dockerfile b/Dockerfile
index 13e7517..39515de 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,6 +12,13 @@ CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/axllent/mailpit/config.Vers
 
 FROM alpine:latest
 
+LABEL org.opencontainers.image.title="Mailpit" \
+  org.opencontainers.image.description="An email and SMTP testing tool with API for developers" \
+  org.opencontainers.image.source="https://github.com/axllent/mailpit" \
+  org.opencontainers.image.url="https://mailpit.axllent.org" \
+  org.opencontainers.image.documentation="https://mailpit.axllent.org/docs/" \
+  org.opencontainers.image.licenses="MIT"
+
 COPY --from=builder /mailpit /mailpit
 
 RUN apk add --no-cache tzdata

From 83c70aa7c11b8b51e945f9951728105fb7ae92a0 Mon Sep 17 00:00:00 2001
From: Ralph Slooten <axllent@gmail.com>
Date: Sun, 24 Mar 2024 21:37:37 +1300
Subject: [PATCH 2/4] Chore: Code cleanup, remove redundant functionality

---
 cmd/ingest.go                  |  4 +--
 cmd/root.go                    |  2 --
 internal/linkcheck/main.go     |  4 +--
 internal/storage/cron.go       |  2 +-
 internal/storage/database.go   | 34 +++++++++-----------
 internal/storage/messages.go   |  6 ++--
 internal/storage/migrations.go |  7 ++--
 internal/storage/reindex.go    |  2 +-
 internal/storage/search.go     |  4 +--
 internal/storage/settings.go   |  7 ++--
 internal/storage/tags.go       | 27 ++++++++--------
 internal/storage/utils.go      | 39 ----------------------
 internal/updater/targz.go      | 59 ++++------------------------------
 internal/updater/updater.go    | 10 ------
 server/apiv1/thumbnails.go     |  2 +-
 server/pop3/pop3.go            | 10 ++++--
 server/server_test.go          |  8 +----
 server/websockets/client.go    |  4 ---
 18 files changed, 61 insertions(+), 170 deletions(-)

diff --git a/cmd/ingest.go b/cmd/ingest.go
index c575668..cab2419 100644
--- a/cmd/ingest.go
+++ b/cmd/ingest.go
@@ -49,9 +49,7 @@ The --recent flag will only consider files with a modification date within the l
 						return nil
 					}
 
-					info.ModTime()
-
-					if ingestRecent > 0 && time.Now().Sub(info.ModTime()) > time.Duration(ingestRecent)*24*time.Hour {
+					if ingestRecent > 0 && time.Since(info.ModTime()) > time.Duration(ingestRecent)*24*time.Hour {
 						return nil
 					}
 
diff --git a/cmd/root.go b/cmd/root.go
index 98658af..c2d4c27 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -17,8 +17,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var cfgFile string
-
 // rootCmd represents the base command when called without any subcommands
 var rootCmd = &cobra.Command{
 	Use:   "mailpit",
diff --git a/internal/linkcheck/main.go b/internal/linkcheck/main.go
index d00c632..8a1f2ab 100644
--- a/internal/linkcheck/main.go
+++ b/internal/linkcheck/main.go
@@ -32,9 +32,7 @@ func RunTests(msg *storage.Message, followRedirects bool) (Response, error) {
 func extractTextLinks(msg *storage.Message) []string {
 	links := []string{}
 
-	for _, match := range linkRe.FindAllString(msg.Text, -1) {
-		links = append(links, match)
-	}
+	links = append(links, linkRe.FindAllString(msg.Text, -1)...)
 
 	return links
 }
diff --git a/internal/storage/cron.go b/internal/storage/cron.go
index 0a39a32..f2d5ca1 100644
--- a/internal/storage/cron.go
+++ b/internal/storage/cron.go
@@ -63,7 +63,7 @@ func pruneMessages() {
 	ids := []string{}
 	var prunedSize int64
 	var size int
-	if err := q.Query(nil, db, func(row *sql.Rows) {
+	if err := q.Query(context.TODO(), db, func(row *sql.Rows) {
 		var id string
 
 		if err := row.Scan(&id, &size); err != nil {
diff --git a/internal/storage/database.go b/internal/storage/database.go
index 540d67c..0bc45bf 100644
--- a/internal/storage/database.go
+++ b/internal/storage/database.go
@@ -2,6 +2,7 @@
 package storage
 
 import (
+	"context"
 	"database/sql"
 	"fmt"
 	"os"
@@ -137,7 +138,7 @@ func CountTotal() int {
 
 	_ = sqlf.From("mailbox").
 		Select("COUNT(*)").To(&total).
-		QueryRowAndClose(nil, db)
+		QueryRowAndClose(context.TODO(), db)
 
 	return total
 }
@@ -146,11 +147,10 @@ func CountTotal() int {
 func CountUnread() int {
 	var total int
 
-	q := sqlf.From("mailbox").
+	_ = sqlf.From("mailbox").
 		Select("COUNT(*)").To(&total).
-		Where("Read = ?", 0)
-
-	_ = q.QueryRowAndClose(nil, db)
+		Where("Read = ?", 0).
+		QueryRowAndClose(context.TODO(), db)
 
 	return total
 }
@@ -159,26 +159,23 @@ func CountUnread() int {
 func CountRead() int {
 	var total int
 
-	q := sqlf.From("mailbox").
+	_ = sqlf.From("mailbox").
 		Select("COUNT(*)").To(&total).
-		Where("Read = ?", 1)
-
-	_ = q.QueryRowAndClose(nil, db)
+		Where("Read = ?", 1).
+		QueryRowAndClose(context.TODO(), db)
 
 	return total
 }
 
-// IsUnread returns the number of emails in the database that are unread.
-// If an ID is supplied, then it is just limited to that message.
+// IsUnread returns whether a message is unread or not.
 func IsUnread(id string) bool {
 	var unread int
 
-	q := sqlf.From("mailbox").
+	_ = sqlf.From("mailbox").
 		Select("COUNT(*)").To(&unread).
 		Where("Read = ?", 0).
-		Where("ID = ?", id)
-
-	_ = q.QueryRowAndClose(nil, db)
+		Where("ID = ?", id).
+		QueryRowAndClose(context.TODO(), db)
 
 	return unread == 1
 }
@@ -187,11 +184,10 @@ func IsUnread(id string) bool {
 func MessageIDExists(id string) bool {
 	var total int
 
-	q := sqlf.From("mailbox").
+	_ = sqlf.From("mailbox").
 		Select("COUNT(*)").To(&total).
-		Where("MessageID = ?", id)
-
-	_ = q.QueryRowAndClose(nil, db)
+		Where("MessageID = ?", id).
+		QueryRowAndClose(context.TODO(), db)
 
 	return total != 0
 }
diff --git a/internal/storage/messages.go b/internal/storage/messages.go
index 03da204..2228f06 100644
--- a/internal/storage/messages.go
+++ b/internal/storage/messages.go
@@ -154,7 +154,7 @@ func List(start, limit int) ([]MessageSummary, error) {
 		Limit(limit).
 		Offset(start)
 
-	if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
+	if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
 		var created int64
 		var id string
 		var messageID string
@@ -245,7 +245,7 @@ func GetMessage(id string) (*Message, error) {
 			Select(`Created`).
 			Where(`ID = ?`, id)
 
-		if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
+		if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
 			var created int64
 
 			if err := row.Scan(&created); err != nil {
@@ -564,7 +564,7 @@ func DeleteAllMessages() error {
 
 	_ = sqlf.From("mailbox").
 		Select("COUNT(*)").To(&total).
-		QueryRowAndClose(nil, db)
+		QueryRowAndClose(context.TODO(), db)
 
 	// begin a transaction to ensure both the message
 	// summaries and data are deleted successfully
diff --git a/internal/storage/migrations.go b/internal/storage/migrations.go
index c83e14d..a6e6087 100644
--- a/internal/storage/migrations.go
+++ b/internal/storage/migrations.go
@@ -1,6 +1,7 @@
 package storage
 
 import (
+	"context"
 	"database/sql"
 	"encoding/json"
 
@@ -140,7 +141,7 @@ func migrateTagsToManyMany() {
 		Where("Tags != ?", "[]").
 		Where("Tags IS NOT NULL")
 
-	if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
+	if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
 		var id string
 		var jsonTags string
 		if err := row.Scan(&id, &jsonTags); err != nil {
@@ -169,7 +170,7 @@ func migrateTagsToManyMany() {
 				if _, err := sqlf.Update("mailbox").
 					Set("Tags", nil).
 					Where("ID = ?", id).
-					ExecAndClose(nil, db); err != nil {
+					ExecAndClose(context.TODO(), db); err != nil {
 					logger.Log().Errorf("[migration] %s", err.Error())
 				}
 			}
@@ -182,7 +183,7 @@ func migrateTagsToManyMany() {
 	if _, err := sqlf.Update("mailbox").
 		Set("Tags", nil).
 		Where("Tags = ?", "[]").
-		ExecAndClose(nil, db); err != nil {
+		ExecAndClose(context.TODO(), db); err != nil {
 		logger.Log().Errorf("[migration] %s", err.Error())
 	}
 }
diff --git a/internal/storage/reindex.go b/internal/storage/reindex.go
index f099bb2..8f1a3c4 100644
--- a/internal/storage/reindex.go
+++ b/internal/storage/reindex.go
@@ -26,7 +26,7 @@ func ReindexAll() {
 	err := sqlf.Select("ID").To(&i).
 		From("mailbox").
 		OrderBy("Created DESC").
-		QueryAndClose(nil, db, func(row *sql.Rows) {
+		QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
 			ids = append(ids, i)
 		})
 
diff --git a/internal/storage/search.go b/internal/storage/search.go
index 8068521..3c869f6 100644
--- a/internal/storage/search.go
+++ b/internal/storage/search.go
@@ -29,7 +29,7 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
 	q := searchQueryBuilder(search)
 	var err error
 
-	if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
+	if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
 		var created int64
 		var id string
 		var messageID string
@@ -101,7 +101,7 @@ func DeleteSearch(search string) error {
 	ids := []string{}
 	deleteSize := 0
 
-	if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
+	if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
 		var created int64
 		var id string
 		var messageID string
diff --git a/internal/storage/settings.go b/internal/storage/settings.go
index 5a8a218..c81da5e 100644
--- a/internal/storage/settings.go
+++ b/internal/storage/settings.go
@@ -1,6 +1,7 @@
 package storage
 
 import (
+	"context"
 	"database/sql"
 
 	"github.com/axllent/mailpit/internal/logger"
@@ -14,7 +15,7 @@ func SettingGet(k string) string {
 		Select("Value").To(&result).
 		Where("Key = ?", k).
 		Limit(1).
-		QueryAndClose(nil, db, func(row *sql.Rows) {})
+		QueryAndClose(context.TODO(), db, func(row *sql.Rows) {})
 	if err != nil {
 		logger.Log().Errorf("[db] %s", err.Error())
 		return ""
@@ -40,7 +41,7 @@ func getDeletedSize() int64 {
 		Select("Value").To(&result).
 		Where("Key = ?", "DeletedSize").
 		Limit(1).
-		QueryAndClose(nil, db, func(row *sql.Rows) {})
+		QueryAndClose(context.TODO(), db, func(row *sql.Rows) {})
 	if err != nil {
 		logger.Log().Errorf("[db] %s", err.Error())
 		return 0
@@ -54,7 +55,7 @@ func totalMessagesSize() int64 {
 	var result sql.NullInt64
 	err := sqlf.From("mailbox").
 		Select("SUM(Size)").To(&result).
-		QueryAndClose(nil, db, func(row *sql.Rows) {})
+		QueryAndClose(context.TODO(), db, func(row *sql.Rows) {})
 	if err != nil {
 		logger.Log().Errorf("[db] %s", err.Error())
 		return 0
diff --git a/internal/storage/tags.go b/internal/storage/tags.go
index 2dec4db..a2f8b71 100644
--- a/internal/storage/tags.go
+++ b/internal/storage/tags.go
@@ -1,6 +1,7 @@
 package storage
 
 import (
+	"context"
 	"database/sql"
 	"regexp"
 	"sort"
@@ -64,14 +65,14 @@ func AddMessageTag(id, name string) error {
 		Where("Name = ?", name)
 
 	// tag exists - add tag to message
-	if err := q.QueryRowAndClose(nil, db); err == nil {
+	if err := q.QueryRowAndClose(context.TODO(), db); err == nil {
 		// check message does not already have this tag
 		var count int
 		if _, err := sqlf.From("message_tags").
 			Select("COUNT(ID)").To(&count).
 			Where("ID = ?", id).
 			Where("TagID = ?", tagID).
-			ExecAndClose(nil, db); err != nil {
+			ExecAndClose(context.TODO(), db); err != nil {
 			return err
 		}
 		if count != 0 {
@@ -84,7 +85,7 @@ func AddMessageTag(id, name string) error {
 		_, err := sqlf.InsertInto("message_tags").
 			Set("ID", id).
 			Set("TagID", tagID).
-			ExecAndClose(nil, db)
+			ExecAndClose(context.TODO(), db)
 		return err
 	}
 
@@ -94,7 +95,7 @@ func AddMessageTag(id, name string) error {
 	if err := sqlf.InsertInto("tags").
 		Set("Name", name).
 		Returning("ID").To(&tagID).
-		QueryRowAndClose(nil, db); err != nil {
+		QueryRowAndClose(context.TODO(), db); err != nil {
 		return err
 	}
 
@@ -104,7 +105,7 @@ func AddMessageTag(id, name string) error {
 		Select("COUNT(ID)").To(&count).
 		Where("ID = ?", id).
 		Where("TagID = ?", tagID).
-		ExecAndClose(nil, db); err != nil {
+		ExecAndClose(context.TODO(), db); err != nil {
 		return err
 	}
 	if count != 0 {
@@ -115,7 +116,7 @@ func AddMessageTag(id, name string) error {
 	_, err := sqlf.InsertInto("message_tags").
 		Set("ID", id).
 		Set("TagID", tagID).
-		ExecAndClose(nil, db)
+		ExecAndClose(context.TODO(), db)
 	return err
 }
 
@@ -124,7 +125,7 @@ func DeleteMessageTag(id, name string) error {
 	if _, err := sqlf.DeleteFrom("message_tags").
 		Where("message_tags.ID = ?", id).
 		Where(`message_tags.Key IN (SELECT Key FROM message_tags LEFT JOIN tags ON TagID=tags.ID WHERE Name = ?)`, name).
-		ExecAndClose(nil, db); err != nil {
+		ExecAndClose(context.TODO(), db); err != nil {
 		return err
 	}
 
@@ -135,7 +136,7 @@ func DeleteMessageTag(id, name string) error {
 func DeleteAllMessageTags(id string) error {
 	if _, err := sqlf.DeleteFrom("message_tags").
 		Where("message_tags.ID = ?", id).
-		ExecAndClose(nil, db); err != nil {
+		ExecAndClose(context.TODO(), db); err != nil {
 		return err
 	}
 
@@ -151,7 +152,7 @@ func GetAllTags() []string {
 		Select(`DISTINCT Name`).
 		From("tags").To(&name).
 		OrderBy("Name").
-		QueryAndClose(nil, db, func(row *sql.Rows) {
+		QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
 			tags = append(tags, name)
 		}); err != nil {
 		logger.Log().Errorf("[db] %s", err.Error())
@@ -173,7 +174,7 @@ func GetAllTagsCount() map[string]int64 {
 		LeftJoin("message_tags", "tags.ID = message_tags.TagID").
 		GroupBy("message_tags.TagID").
 		OrderBy("Name").
-		QueryAndClose(nil, db, func(row *sql.Rows) {
+		QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
 			tags[name] = total
 			// tags = append(tags, name)
 		}); err != nil {
@@ -192,7 +193,7 @@ func pruneUnusedTags() error {
 
 	toDel := []int{}
 
-	if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
+	if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
 		var n string
 		var id int
 		var c int
@@ -214,7 +215,7 @@ func pruneUnusedTags() error {
 		for _, id := range toDel {
 			if _, err := sqlf.DeleteFrom("tags").
 				Where("ID = ?", id).
-				ExecAndClose(nil, db); err != nil {
+				ExecAndClose(context.TODO(), db); err != nil {
 				return err
 			}
 		}
@@ -282,7 +283,7 @@ func getMessageTags(id string) []string {
 		LeftJoin("message_tags", "Tags.ID=message_tags.TagID").
 		Where(`message_tags.ID = ?`, id).
 		OrderBy("Name").
-		QueryAndClose(nil, db, func(row *sql.Rows) {
+		QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
 			tags = append(tags, name)
 		}); err != nil {
 		logger.Log().Errorf("[tags] %s", err.Error())
diff --git a/internal/storage/utils.go b/internal/storage/utils.go
index 6dbdd8b..f57001e 100644
--- a/internal/storage/utils.go
+++ b/internal/storage/utils.go
@@ -103,42 +103,3 @@ func inArray(k string, arr []string) bool {
 func escPercentChar(s string) string {
 	return strings.ReplaceAll(s, "%", "%%")
 }
-
-// Escape certain characters in search phrases
-func escSearch(str string) string {
-	dest := make([]byte, 0, 2*len(str))
-	var escape byte
-	for i := 0; i < len(str); i++ {
-		c := str[i]
-
-		escape = 0
-
-		switch c {
-		case 0: /* Must be escaped for 'mysql' */
-			escape = '0'
-			break
-		case '\n': /* Must be escaped for logs */
-			escape = 'n'
-			break
-		case '\r':
-			escape = 'r'
-			break
-		case '\\':
-			escape = '\\'
-			break
-		case '\'':
-			escape = '\''
-			break
-		case '\032': //十进制26,八进制32,十六进制1a, /* This gives problems on Win32 */
-			escape = 'Z'
-		}
-
-		if escape != 0 {
-			dest = append(dest, '\\', escape)
-		} else {
-			dest = append(dest, c)
-		}
-	}
-
-	return string(dest)
-}
diff --git a/internal/updater/targz.go b/internal/updater/targz.go
index 02d30c1..ad4c527 100644
--- a/internal/updater/targz.go
+++ b/internal/updater/targz.go
@@ -98,53 +98,6 @@ func makeAbsolute(inputFilePath, outputFilePath string) (string, string, error)
 	return inputFilePath, outputFilePath, err
 }
 
-// Write path without the prefix in subPath to tar writer.
-func writeTarGz(path string, tarWriter *tar.Writer, fileInfo os.FileInfo, subPath string) error {
-	file, err := os.Open(filepath.Clean(path))
-	if err != nil {
-		return err
-	}
-
-	defer func() {
-		if err := file.Close(); err != nil {
-			fmt.Printf("Error closing file: %s\n", err)
-		}
-	}()
-
-	evaledPath, err := filepath.EvalSymlinks(path)
-	if err != nil {
-		return err
-	}
-
-	subPath, err = filepath.EvalSymlinks(subPath)
-	if err != nil {
-		return err
-	}
-
-	link := ""
-	if evaledPath != path {
-		link = evaledPath
-	}
-
-	header, err := tar.FileInfoHeader(fileInfo, link)
-	if err != nil {
-		return err
-	}
-	header.Name = evaledPath[len(subPath):]
-
-	err = tarWriter.WriteHeader(header)
-	if err != nil {
-		return err
-	}
-
-	_, err = io.Copy(tarWriter, file)
-	if err != nil {
-		return err
-	}
-
-	return err
-}
-
 // Extract the file in filePath to directory.
 func extract(filePath string, directory string) error {
 	file, err := os.Open(filepath.Clean(filePath))
@@ -200,7 +153,7 @@ func extract(filePath string, directory string) error {
 
 			// set file ownership (if allowed)
 			// Chtimes() && Chmod() only set after once extraction is complete
-			os.Chown(filename, header.Uid, header.Gid) // #nosec
+			_ = os.Chown(filename, header.Uid, header.Gid)
 
 			// add directory info to slice to process afterwards
 			postExtraction = append(postExtraction, DirInfo{filename, header})
@@ -249,15 +202,15 @@ func extract(filePath string, directory string) error {
 		}
 
 		// set file permissions, timestamps & uid/gid
-		os.Chmod(filename, os.FileMode(header.Mode))            // #nosec
-		os.Chtimes(filename, header.AccessTime, header.ModTime) // #nosec
-		os.Chown(filename, header.Uid, header.Gid)              // #nosec
+		_ = os.Chmod(filename, os.FileMode(header.Mode))
+		_ = os.Chtimes(filename, header.AccessTime, header.ModTime)
+		_ = os.Chown(filename, header.Uid, header.Gid)
 	}
 
 	if len(postExtraction) > 0 {
 		for _, dir := range postExtraction {
-			os.Chtimes(dir.Path, dir.Header.AccessTime, dir.Header.ModTime) // #nosec
-			os.Chmod(dir.Path, dir.Header.FileInfo().Mode().Perm())         // #nosec
+			_ = os.Chtimes(dir.Path, dir.Header.AccessTime, dir.Header.ModTime)
+			_ = os.Chmod(dir.Path, dir.Header.FileInfo().Mode().Perm())
 		}
 	}
 
diff --git a/internal/updater/updater.go b/internal/updater/updater.go
index da83201..1c0fc90 100644
--- a/internal/updater/updater.go
+++ b/internal/updater/updater.go
@@ -335,16 +335,6 @@ func mkDirIfNotExists(path string) error {
 	return nil
 }
 
-// IsFile returns if a path is a file
-func isFile(path string) bool {
-	info, err := os.Stat(path)
-	if os.IsNotExist(err) || !info.Mode().IsRegular() {
-		return false
-	}
-
-	return true
-}
-
 // IsDir returns if a path is a directory
 func isDir(path string) bool {
 	info, err := os.Stat(path)
diff --git a/server/apiv1/thumbnails.go b/server/apiv1/thumbnails.go
index 8a1291e..a53190c 100644
--- a/server/apiv1/thumbnails.go
+++ b/server/apiv1/thumbnails.go
@@ -114,7 +114,7 @@ func blankImage(a *enmime.Part, w http.ResponseWriter) {
 	rect := image.Rect(0, 0, thumbWidth, thumbHeight)
 	img := image.NewRGBA(rect)
 	background := color.RGBA{255, 255, 255, 255}
-	draw.Draw(img, img.Bounds(), &image.Uniform{background}, image.ZP, draw.Src)
+	draw.Draw(img, img.Bounds(), &image.Uniform{background}, image.Point{}, draw.Src)
 	var b bytes.Buffer
 	foo := bufio.NewWriter(&b)
 	dstImageFill := imaging.Fill(img, thumbWidth, thumbHeight, imaging.Center, imaging.Lanczos)
diff --git a/server/pop3/pop3.go b/server/pop3/pop3.go
index 43bf339..bc70db3 100644
--- a/server/pop3/pop3.go
+++ b/server/pop3/pop3.go
@@ -41,9 +41,9 @@ func Run() {
 	var err error
 
 	if config.POP3TLSCert != "" {
-		cer, err := tls.LoadX509KeyPair(config.POP3TLSCert, config.POP3TLSKey)
-		if err != nil {
-			logger.Log().Errorf("[pop3] %s", err.Error())
+		cer, err2 := tls.LoadX509KeyPair(config.POP3TLSCert, config.POP3TLSKey)
+		if err2 != nil {
+			logger.Log().Errorf("[pop3] %s", err2.Error())
 			return
 		}
 
@@ -273,6 +273,10 @@ func handleClient(conn net.Conn) {
 
 			m := messages[nr-1]
 			headers, body, err := getTop(m.ID, lines)
+			if err != nil {
+				sendResponse(conn, err.Error())
+				return
+			}
 
 			sendData(conn, "+OK Top of message follows")
 			sendData(conn, headers+"\r\n")
diff --git a/server/server_test.go b/server/server_test.go
index f3250f5..c65c830 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -47,8 +47,6 @@ func TestAPIv1Messages(t *testing.T) {
 	insertEmailData(t)
 	assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)
 
-	// store this for later tests
-
 	m, err = fetchMessages(ts.URL + "/api/v1/messages")
 	if err != nil {
 		t.Errorf(err.Error())
@@ -56,7 +54,6 @@ func TestAPIv1Messages(t *testing.T) {
 
 	// read first 10 messages
 	t.Log("Read first 10 messages including raw & headers")
-	putIDS := []string{}
 	for idx, msg := range m.Messages {
 		if idx == 10 {
 			break
@@ -71,13 +68,10 @@ func TestAPIv1Messages(t *testing.T) {
 			t.Errorf(err.Error())
 		}
 
-		// het headers
+		// get headers
 		if _, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID + "/headers"); err != nil {
 			t.Errorf(err.Error())
 		}
-
-		// store for later
-		putIDS = append(putIDS, msg.ID)
 	}
 
 	// 10 should be marked as read
diff --git a/server/websockets/client.go b/server/websockets/client.go
index c4523c0..231008e 100644
--- a/server/websockets/client.go
+++ b/server/websockets/client.go
@@ -22,14 +22,10 @@ const (
 
 	// Send pings to peer with this period. Must be less than pongWait.
 	pingPeriod = (pongWait * 9) / 10
-
-	// Maximum message size allowed from peer.
-	maxMessageSize = 512
 )
 
 var (
 	newline = []byte{'\n'}
-	space   = []byte{' '}
 
 	// MessageHub global
 	MessageHub *Hub

From a805567810a54b187aea458d8d64c59ecf26064b Mon Sep 17 00:00:00 2001
From: Ralph Slooten <axllent@gmail.com>
Date: Sun, 31 Mar 2024 00:06:25 +1300
Subject: [PATCH 3/4] Feature: Add readyz subcommand for Docker healthcheck
 (#270)

---
 Dockerfile                   |  2 +
 cmd/readyz.go                | 75 ++++++++++++++++++++++++++++++++++++
 cmd/root.go                  |  2 +-
 internal/storage/database.go |  5 +++
 server/handlers/k8sready.go  |  4 +-
 5 files changed, 86 insertions(+), 2 deletions(-)
 create mode 100644 cmd/readyz.go

diff --git a/Dockerfile b/Dockerfile
index 39515de..fa7f27f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -25,4 +25,6 @@ RUN apk add --no-cache tzdata
 
 EXPOSE 1025/tcp 1110/tcp 8025/tcp
 
+HEALTHCHECK --interval=15s CMD /mailpit readyz
+
 ENTRYPOINT ["/mailpit"]
diff --git a/cmd/readyz.go b/cmd/readyz.go
new file mode 100644
index 0000000..d65a7a2
--- /dev/null
+++ b/cmd/readyz.go
@@ -0,0 +1,75 @@
+package cmd
+
+import (
+	"crypto/tls"
+	"fmt"
+	"net/http"
+	"os"
+	"path"
+	"strings"
+	"time"
+
+	"github.com/axllent/mailpit/config"
+	"github.com/spf13/cobra"
+)
+
+var (
+	useHTTPS bool
+)
+
+// readyzCmd represents the healthcheck command
+var readyzCmd = &cobra.Command{
+	Use:   "readyz",
+	Short: "Run a healthcheck to test if Mailpit is running",
+	Long: `This command connects to the /readyz endpoint of a running Mailpit server
+and exits with a status of 0 if the connection is successful, else with a 
+status 1 if unhealthy.
+
+If running within Docker, it should automatically detect environment
+settings to determine the HTTP bind interface & port.
+`,
+	Run: func(cmd *cobra.Command, args []string) {
+		webroot := strings.TrimRight(path.Join("/", config.Webroot, "/"), "/") + "/"
+		proto := "http"
+		if useHTTPS {
+			proto = "https"
+		}
+
+		uri := fmt.Sprintf("%s://%s%sreadyz", proto, config.HTTPListen, webroot)
+
+		conf := &http.Transport{
+			IdleConnTimeout:       time.Second * 5,
+			ExpectContinueTimeout: time.Second * 5,
+			TLSHandshakeTimeout:   time.Second * 5,
+			TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
+		}
+		client := &http.Client{Transport: conf}
+
+		res, err := client.Get(uri)
+		if err != nil || res.StatusCode != 200 {
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(readyzCmd)
+
+	if len(os.Getenv("MP_UI_BIND_ADDR")) > 0 {
+		config.HTTPListen = os.Getenv("MP_UI_BIND_ADDR")
+	}
+
+	if len(os.Getenv("MP_WEBROOT")) > 0 {
+		config.Webroot = os.Getenv("MP_WEBROOT")
+	}
+
+	config.UITLSCert = os.Getenv("MP_UI_TLS_CERT")
+
+	if config.UITLSCert != "" {
+		useHTTPS = true
+	}
+
+	readyzCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "Set the HTTP bind interface & port")
+	readyzCmd.Flags().StringVar(&config.Webroot, "webroot", config.Webroot, "Set the webroot for web UI & API")
+	readyzCmd.Flags().BoolVar(&useHTTPS, "https", useHTTPS, "Connect via HTTPS (ignores HTTPS validation)")
+}
diff --git a/cmd/root.go b/cmd/root.go
index c2d4c27..792ce3b 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -89,7 +89,7 @@ func init() {
 	rootCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging")
 
 	// Web UI / API
-	rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for UI")
+	rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface & port for UI")
 	rootCmd.Flags().StringVar(&config.Webroot, "webroot", config.Webroot, "Set the webroot for web UI & API")
 	rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI & API authentication")
 	rootCmd.Flags().StringVar(&config.UITLSCert, "ui-tls-cert", config.UITLSCert, "TLS certificate for web UI (HTTPS) - requires ui-tls-key")
diff --git a/internal/storage/database.go b/internal/storage/database.go
index 0bc45bf..583cffb 100644
--- a/internal/storage/database.go
+++ b/internal/storage/database.go
@@ -115,6 +115,11 @@ func Close() {
 	}
 }
 
+// Ping the database connection and return an error if unsuccessful
+func Ping() error {
+	return db.Ping()
+}
+
 // StatsGet returns the total/unread statistics for a mailbox
 func StatsGet() MailboxStats {
 	var (
diff --git a/server/handlers/k8sready.go b/server/handlers/k8sready.go
index 875b2b4..9ab8500 100644
--- a/server/handlers/k8sready.go
+++ b/server/handlers/k8sready.go
@@ -3,12 +3,14 @@ package handlers
 import (
 	"net/http"
 	"sync/atomic"
+
+	"github.com/axllent/mailpit/internal/storage"
 )
 
 // ReadyzHandler is a ready probe that signals k8s to be able to retrieve traffic
 func ReadyzHandler(isReady *atomic.Value) http.HandlerFunc {
 	return func(w http.ResponseWriter, _ *http.Request) {
-		if isReady == nil || !isReady.Load().(bool) {
+		if isReady == nil || !isReady.Load().(bool) || storage.Ping() != nil {
 			http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
 			return
 		}

From 6a27e230a1c25dbdf3676cf839ca8ae0f8db77e3 Mon Sep 17 00:00:00 2001
From: Ralph Slooten <axllent@gmail.com>
Date: Sun, 31 Mar 2024 00:11:46 +1300
Subject: [PATCH 4/4] Release v1.15.1

---
 CHANGELOG.md | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86b107a..0af4b51 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
 
 Notable changes to Mailpit will be documented in this file.
 
+## [v1.15.1]
+
+### Chore
+- Code cleanup, remove redundant functionality
+- Add labels to Docker image ([#267](https://github.com/axllent/mailpit/issues/267))
+
+### Feature
+- Add readyz subcommand for Docker healthcheck ([#270](https://github.com/axllent/mailpit/issues/270))
+
+
 ## [v1.15.0]
 
 ### Chore