Skip to content

Commit

Permalink
Merge branch 'release/0.0.9'
Browse files Browse the repository at this point in the history
  • Loading branch information
axllent committed Aug 5, 2022
2 parents 9fc7202 + b9043b6 commit b57e340
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 34 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
Notable changes to Mailpit will be documented in this file.


## 0.0.9

### Bugfix
- Include read status in search results

### Feature
- HTTPS option for web UI

### Testing
- Memory & physical database tests


## 0.0.8

### Bugfix
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster.

![Mailpit](https://raw.githubusercontent.com/axllent/mailpit/develop/screenshot.png)


## Features

- Runs completely on a single binary
- SMTP server (default `0.0.0.0:1025`)
- Web UI to view emails (HTML format, text, source and MIME attachments, default `0.0.0.0:8025`)
- Optional HTTPS for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/HTTPS))
- Real-time web UI updates using web sockets for new mail
- Optional basic authentication for web UI (see [wiki](https://github.com/axllent/mailpit/wiki/Basic-authentication))
- Optional basic authentication for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/Basic-authentication))
- Email storage in either memory or disk (using [CloverDB](https://github.com/ostafen/clover)) - note that in-memory has a physical limit of 1MB per email
- Configurable automatic email pruning (default keeps the most recent 500 emails)
- Fast SMTP processing & storing - approximately 300-600 emails per second depending on CPU, network speed & email size
Expand All @@ -24,7 +26,6 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster.

## Planned features

- Optional HTTPS for web UI
- Browser notifications for new mail (HTTPS only)


Expand Down
8 changes: 8 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,19 @@ func init() {
if len(os.Getenv("MP_AUTH_FILE")) > 0 {
config.AuthFile = os.Getenv("MP_AUTH_FILE")
}
if len(os.Getenv("MP_SSL_CERT")) > 0 {
config.SSLCert = os.Getenv("MP_SSL_CERT")
}
if len(os.Getenv("MP_SSL_KEY")) > 0 {
config.SSLKey = os.Getenv("MP_SSL_KEY")
}

rootCmd.Flags().StringVarP(&config.DataDir, "data", "d", config.DataDir, "Optional path to store peristent data")
rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port")
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for UI")
rootCmd.Flags().IntVarP(&config.MaxMessages, "max", "m", config.MaxMessages, "Max number of messages to store")
rootCmd.Flags().StringVarP(&config.AuthFile, "auth-file", "a", config.AuthFile, "A password file for authentication (see wiki)")
rootCmd.Flags().StringVar(&config.SSLCert, "ssl-cert", config.SSLCert, "SSL certificate - requires ssl-key (see wiki)")
rootCmd.Flags().StringVar(&config.SSLKey, "ssl-key", config.SSLKey, "SSL key - requires ssl-cert (see wiki)")
rootCmd.Flags().BoolVarP(&config.VerboseLogging, "verbose", "v", false, "Verbose logging")
}
35 changes: 33 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package config

import (
"errors"
"fmt"
"os"
"regexp"

"github.com/tg123/go-htpasswd"
Expand All @@ -26,9 +28,10 @@ var (
// NoLogging for tests
NoLogging = false

// SSLCert @TODO
// SSLCert file
SSLCert string
// SSLKey @TODO

// SSLKey file
SSLKey string

// AuthFile for basic authentication
Expand All @@ -49,12 +52,40 @@ func VerifyConfig() error {
}

if AuthFile != "" {
if !isFile(AuthFile) {
return fmt.Errorf("password file not found: %s", AuthFile)
}

a, err := htpasswd.New(AuthFile, htpasswd.DefaultSystems, nil)
if err != nil {
return err
}
Auth = a
}

if SSLCert != "" && SSLKey == "" || SSLCert == "" && SSLKey != "" {
return errors.New("you must provide both an SSL certificate and a key")
}

if SSLCert != "" {
if !isFile(SSLCert) {
return fmt.Errorf("SSL certificate not found: %s", SSLCert)
}

if !isFile(SSLKey) {
return fmt.Errorf("SSL key not found: %s", SSLKey)
}
}

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
}
25 changes: 3 additions & 22 deletions storage/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,12 +327,12 @@ func Search(mailbox, search string, start, limit int) ([]data.Summary, error) {
results := []data.Summary{}

for _, d := range q {
cs := &CloverStore{}
cs := &data.Summary{}
if err := d.Unmarshal(cs); err != nil {
return nil, err
}

results = append(results, cs.Summary(d.ObjectId()))
cs.ID = d.ObjectId()
results = append(results, *cs)
}

return results, nil
Expand All @@ -351,25 +351,6 @@ func CountUnread(mailbox string) (int, error) {
)
}

// Summary generated a message summary. ID must be supplied
// as this is not stored within the CloverStore but rather the
// *clover.Document
func (c *CloverStore) Summary(id string) data.Summary {
s := data.Summary{
ID: id,
From: c.From,
To: c.To,
Cc: c.Cc,
Bcc: c.Bcc,
Subject: c.Subject,
Created: c.Created,
Size: c.Size,
Attachments: c.Attachments,
}

return s
}

// GetMessage returns a data.Message generated from the {mailbox}_data collection.
// ID must be supplied as this is not stored within the CloverStore but rather the
// *clover.Document
Expand Down
59 changes: 51 additions & 8 deletions storage/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"io/ioutil"
"math/rand"
"os"
"path"
"testing"
"time"

Expand All @@ -18,8 +20,10 @@ var (
)

func TestTextEmailInserts(t *testing.T) {
setup()
setup(false)
t.Log("Testing memory storage")

RepeatTest:
start := time.Now()
for i := 0; i < 1000; i++ {
if _, err := Store(DefaultMailbox, testTextEmail); err != nil {
Expand Down Expand Up @@ -55,11 +59,20 @@ func TestTextEmailInserts(t *testing.T) {
t.Logf("deleted 1,000 text emails in %s\n", time.Since(delStart))

db.Close()
if config.DataDir == "" {
setup(true)
t.Logf("Testing physical storage to %s", config.DataDir)
defer os.RemoveAll(config.DataDir)
goto RepeatTest
}

}

func TestMimeEmailInserts(t *testing.T) {
setup()
setup(false)
t.Log("Testing memory storage")

RepeatTest:
start := time.Now()
for i := 0; i < 1000; i++ {
if _, err := Store(DefaultMailbox, testMimeEmail); err != nil {
Expand Down Expand Up @@ -95,11 +108,19 @@ func TestMimeEmailInserts(t *testing.T) {
t.Logf("deleted 1,000 mime emails in %s\n", time.Since(delStart))

db.Close()
if config.DataDir == "" {
setup(true)
t.Logf("Testing physical storage to %s", config.DataDir)
defer os.RemoveAll(config.DataDir)
goto RepeatTest
}
}

func TestRetrieveMimeEmail(t *testing.T) {
setup()
setup(false)
t.Log("Testing memory storage")

RepeatTest:
id, err := Store(DefaultMailbox, testMimeEmail)
if err != nil {
t.Log("error ", err)
Expand Down Expand Up @@ -128,11 +149,20 @@ func TestRetrieveMimeEmail(t *testing.T) {
assertEqual(t, len(inlineData.Content), msg.Inline[0].Size, "inline attachment size does not match")

db.Close()

if config.DataDir == "" {
setup(true)
t.Logf("Testing physical storage to %s", config.DataDir)
defer os.RemoveAll(config.DataDir)
goto RepeatTest
}
}

func TestSearch(t *testing.T) {
setup()
setup(false)
t.Log("Testing memory storage")

RepeatTest:
for i := 0; i < 1000; i++ {
msg := enmime.Builder().
From(fmt.Sprintf("From %d", i), fmt.Sprintf("from-%[email protected]", i)).
Expand Down Expand Up @@ -198,10 +228,17 @@ func TestSearch(t *testing.T) {
assertEqual(t, len(summaries), 200, "200 search results expected")

db.Close()

if config.DataDir == "" {
setup(true)
t.Logf("Testing physical storage to %s", config.DataDir)
defer os.RemoveAll(config.DataDir)
goto RepeatTest
}
}

func BenchmarkImportText(b *testing.B) {
setup()
setup(false)

for i := 0; i < b.N; i++ {
if _, err := Store(DefaultMailbox, testTextEmail); err != nil {
Expand All @@ -214,7 +251,7 @@ func BenchmarkImportText(b *testing.B) {
}

func BenchmarkImportMime(b *testing.B) {
setup()
setup(false)

for i := 0; i < b.N; i++ {
if _, err := Store(DefaultMailbox, testMimeEmail); err != nil {
Expand All @@ -225,9 +262,16 @@ func BenchmarkImportMime(b *testing.B) {
db.Close()
}

func setup() {
func setup(dataDir bool) {
config.NoLogging = true
config.MaxMessages = 0

if dataDir {
config.DataDir = fmt.Sprintf("%s-%d", path.Join(os.TempDir(), "mailpit-tests"), time.Now().UnixNano())
} else {
config.DataDir = ""
}

if err := InitDB(); err != nil {
panic(err)
}
Expand All @@ -243,7 +287,6 @@ func setup() {
if err != nil {
panic(err)
}

}

func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
Expand Down

0 comments on commit b57e340

Please sign in to comment.