1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-03-19 21:28:07 +02:00

Merge branch 'release/v1.15.1'

This commit is contained in:
Ralph Slooten 2024-03-31 00:11:48 +13:00
commit ebe9195075
22 changed files with 164 additions and 172 deletions

View File

@ -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

View File

@ -12,10 +12,19 @@ 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
EXPOSE 1025/tcp 1110/tcp 8025/tcp
HEALTHCHECK --interval=15s CMD /mailpit readyz
ENTRYPOINT ["/mailpit"]

View File

@ -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
}

75
cmd/readyz.go Normal file
View File

@ -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)")
}

View File

@ -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",
@ -91,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")

View File

@ -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
}

View File

@ -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 {

View File

@ -2,6 +2,7 @@
package storage
import (
"context"
"database/sql"
"fmt"
"os"
@ -114,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 (
@ -137,7 +143,7 @@ func CountTotal() int {
_ = sqlf.From("mailbox").
Select("COUNT(*)").To(&total).
QueryRowAndClose(nil, db)
QueryRowAndClose(context.TODO(), db)
return total
}
@ -146,11 +152,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 +164,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 +189,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
}

View File

@ -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

View File

@ -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())
}
}

View File

@ -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)
})

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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)
}

View File

@ -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())
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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")

View File

@ -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

View File

@ -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