mirror of
https://github.com/axllent/mailpit.git
synced 2025-01-04 00:15:54 +02:00
Merge branch 'release/0.0.9'
This commit is contained in:
commit
b57e340389
12
CHANGELOG.md
12
CHANGELOG.md
@ -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
|
||||
|
@ -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
|
||||
@ -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)
|
||||
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/tg123/go-htpasswd"
|
||||
@ -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
|
||||
@ -49,6 +52,10 @@ 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
|
||||
@ -56,5 +63,29 @@ func VerifyConfig() error {
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
@ -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
|
||||
|
@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
@ -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)
|
||||
@ -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-%d@example.com", i)).
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
@ -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)
|
||||
}
|
||||
@ -243,7 +287,6 @@ func setup() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
||||
|
Loading…
Reference in New Issue
Block a user