Skip to content

Commit

Permalink
Merge branch 'release/v1.21.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
axllent committed Oct 24, 2024
2 parents 009d028 + 57d32c6 commit 10c20dd
Show file tree
Hide file tree
Showing 20 changed files with 1,338 additions and 181 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

Notable changes to Mailpit will be documented in this file.

## [v1.21.0]

### Feature
- Experimental Unix socket support for HTTPD & SMTPD ([#373](https://github.com/axllent/mailpit/issues/373))

### Fix
- Allow multiple item selection on macOS with Cmd-click ([#378](https://github.com/axllent/mailpit/issues/378))


## [v1.20.7]

### Chore
Expand Down
3 changes: 1 addition & 2 deletions cmd/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"io"
"net/mail"
"net/smtp"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -106,7 +105,7 @@ The --recent flag will only consider files with a modification date within the l
}
}

err = smtp.SendMail(sendmail.SMTPAddr, nil, returnPath, recipients, body)
err = sendmail.Send(sendmail.SMTPAddr, returnPath, recipients, body)
if err != nil {
logger.Log().Errorf("error sending mail: %s (%s)", err.Error(), path)
return nil
Expand Down
4 changes: 2 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,10 @@ func VerifyConfig() error {
}

re := regexp.MustCompile(`.*:\d+$`)
if !re.MatchString(SMTPListen) {
if _, _, isSocket := tools.UnixSocket(SMTPListen); !isSocket && !re.MatchString(SMTPListen) {
return errors.New("[smtp] bind should be in the format of <ip>:<port>")
}
if !re.MatchString(HTTPListen) {
if _, _, isSocket := tools.UnixSocket(HTTPListen); !isSocket && !re.MatchString(HTTPListen) {
return errors.New("[ui] HTTP bind should be in the format of <ip>:<port>")
}

Expand Down
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ require (
github.com/kovidgoyal/imaging v1.6.3
github.com/leporo/sqlf v1.4.0
github.com/lithammer/shortuuid/v4 v4.0.0
github.com/mhale/smtpd v0.8.3
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62
github.com/rqlite/gorqlite v0.0.0-20241013203532-4385768ae85d
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mhale/smtpd v0.8.3 h1:8j8YNXajksoSLZja3HdwvYVZPuJSqAxFsib3adzRRt8=
github.com/mhale/smtpd v0.8.3/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62 h1:XMG5DklHoioVYysfYglOB7vRBg/LOUJZy2mq2QyedLg=
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62/go.mod h1:niAM5cni0I/47IFA995xQfeK58Mkbb7FHJjacY4OGQg=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
Expand All @@ -80,8 +80,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
Expand Down
14 changes: 6 additions & 8 deletions internal/storage/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ import (
var (
db *sql.DB
dbFile string
dbIsTemp bool
sqlDriver string
dbLastAction time.Time

// zstd compression encoder & decoder
dbEncoder, _ = zstd.NewWriter(nil)
dbDecoder, _ = zstd.NewReader(nil)

temporaryFiles = []string{}
)

// InitDB will initialise the database
Expand All @@ -50,7 +51,8 @@ func InitDB() error {
// when no path is provided then we create a temporary file
// which will get deleted on Close(), SIGINT or SIGTERM
p = fmt.Sprintf("%s-%d.db", path.Join(os.TempDir(), "mailpit"), time.Now().UnixNano())
dbIsTemp = true
// delete the Unix socket file on exit
AddTempFile(p)
sqlDriver = "sqlite"
dsn = p
logger.Log().Debugf("[db] using temporary database: %s", p)
Expand Down Expand Up @@ -156,12 +158,8 @@ func Close() {
// allow SQLite to finish closing DB & write WAL logs if local
time.Sleep(100 * time.Millisecond)

if dbIsTemp && isFile(dbFile) {
logger.Log().Debugf("[db] deleting temporary file %s", dbFile)
if err := os.Remove(dbFile); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
}
}
// delete all temporary files
deleteTempFiles()
}

// Ping the database connection and return an error if unsuccessful
Expand Down
15 changes: 15 additions & 0 deletions internal/storage/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"

"github.com/axllent/mailpit/internal/html2text"
"github.com/axllent/mailpit/internal/logger"
"github.com/jhillyerd/enmime"
)

Expand All @@ -18,6 +19,20 @@ var (
StatsDeleted float64
)

// AddTempFile adds a file to the slice of files to delete on exit
func AddTempFile(s string) {
temporaryFiles = append(temporaryFiles, s)
}

// DeleteTempFiles will delete files added via AddTempFiles
func deleteTempFiles() {
for _, f := range temporaryFiles {
if err := os.Remove(f); err == nil {
logger.Log().Debugf("removed temporary file: %s", f)
}
}
}

// Return a header field as a []*mail.Address, or "null" is not found/empty
func addressToSlice(env *enmime.Envelope, key string) []*mail.Address {
data, err := env.AddressList(key)
Expand Down
49 changes: 49 additions & 0 deletions internal/tools/unixsocket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package tools

import (
"fmt"
"io/fs"
"net"
"os"
"path"
"regexp"
"strconv"
)

// UnixSocket returns a path and a FileMode if the address is in
// the format of unix:<path>:<permission>
func UnixSocket(address string) (string, fs.FileMode, bool) {
re := regexp.MustCompile(`^unix:(.*):(\d\d\d\d?)$`)

var f fs.FileMode

if !re.MatchString(address) {
return "", f, false
}

m := re.FindAllStringSubmatch(address, 1)

modeVal, err := strconv.ParseUint(m[0][2], 8, 32)

if err != nil {
return "", f, false
}

return path.Clean(m[0][1]), fs.FileMode(modeVal), true
}

// PrepareSocket returns an error if an active socket file already exists
func PrepareSocket(address string) error {
address = path.Clean(address)
if _, err := os.Stat(address); os.IsNotExist(err) {
// does not exist, OK
return nil
}

if _, err := net.Dial("unix", address); err == nil {
// socket is listening
return fmt.Errorf("socket already in use: %s", address)
}

return os.Remove(address)
}
2 changes: 1 addition & 1 deletion internal/updater/targz.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func extract(filePath string, directory string) error {
}

// set file permissions, timestamps & uid/gid
_ = os.Chmod(filename, os.FileMode(header.Mode))
_ = os.Chmod(filename, os.FileMode(header.Mode)) // #nosec
_ = os.Chtimes(filename, header.AccessTime, header.ModTime)
_ = os.Chown(filename, header.Uid, header.Gid)
}
Expand Down
38 changes: 30 additions & 8 deletions sendmail/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ import (
"fmt"
"io"
"net/mail"
"net/smtp"
"os"
"os/user"
"path"
"regexp"
"strings"

"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/reiver/go-telnet"
"github.com/mneis/go-telnet"
flag "github.com/spf13/pflag"
)

Expand Down Expand Up @@ -113,14 +113,23 @@ func Run() {
os.Exit(1)
}

socketAddr, isSocket := socketAddress(SMTPAddr)

// handles `sendmail -bs`
// telnet directly to SMTP
if UseB && UseS {
var caller telnet.Caller = telnet.StandardCaller

// telnet directly to SMTP
if err := telnet.DialToAndCall(SMTPAddr, caller); err != nil {
fmt.Println(err)
os.Exit(1)
if isSocket {
if err := telnet.DialToAndCallUnix(socketAddr, caller); err != nil {
fmt.Println(err)
os.Exit(1)
}
} else {
if err := telnet.DialToAndCall(SMTPAddr, caller); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

return
Expand Down Expand Up @@ -167,8 +176,7 @@ func Run() {
os.Exit(11)
}

err = smtp.SendMail(SMTPAddr, nil, from.Address, addresses, body)
if err != nil {
if err := Send(SMTPAddr, from.Address, addresses, body); err != nil {
fmt.Fprintln(os.Stderr, "error sending mail")
logger.Log().Fatal(err)
}
Expand All @@ -192,3 +200,17 @@ Flags:
-v Ignored
`, config.Version, strings.Join(args, " "), FromAddr)
}

// SocketAddress returns a path and a FileMode if the address is in
// the format of unix:<path>
func socketAddress(address string) (string, bool) {
re := regexp.MustCompile(`^unix:(.*)$`)

if !re.MatchString(address) {
return "", false
}

m := re.FindAllStringSubmatch(address, 1)

return path.Clean(m[0][1]), true
}
71 changes: 71 additions & 0 deletions sendmail/cmd/smtp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Package cmd is a wrapper library to send mail
package cmd

import (
"fmt"
"net"
"net/mail"
"net/smtp"
"os"

"github.com/axllent/mailpit/internal/logger"
)

// Send is a wrapper for smtp.SendMail() which also supports sending via unix sockets.
// Unix sockets must be set as unix:/path/to/socket
// It does not support authentication.
func Send(addr string, from string, to []string, msg []byte) error {
socketPath, isSocket := socketAddress(addr)

fromAddress, err := mail.ParseAddress(from)
if err != nil {
return fmt.Errorf("invalid from address: %s", from)
}

if len(to) == 0 {
return fmt.Errorf("no To addresses specified")
}

if !isSocket {
return smtp.SendMail(addr, nil, fromAddress.Address, to, msg)
}

conn, err := net.Dial("unix", socketPath)
if err != nil {
return fmt.Errorf("error connecting to %s", addr)
}

client, err := smtp.NewClient(conn, "")
if err != nil {
return err
}

// Set the sender
if err := client.Mail(fromAddress.Address); err != nil {
fmt.Fprintln(os.Stderr, "error sending mail")
logger.Log().Fatal(err)
}

// Set the recipient
for _, a := range to {
if err := client.Rcpt(a); err != nil {
return err
}
}

wc, err := client.Data()
if err != nil {
return err
}

_, err = wc.Write(msg)
if err != nil {
return err
}
err = wc.Close()
if err != nil {
return err
}

return nil
}
2 changes: 1 addition & 1 deletion server/apiv1/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
return
}

if err := smtpd.Send(from, data.To, msg); err != nil {
if err := smtpd.Relay(from, data.To, msg); err != nil {
logger.Log().Errorf("[smtp] error sending message: %s", err.Error())
httpError(w, "SMTP error: "+err.Error())
return
Expand Down
2 changes: 1 addition & 1 deletion server/apiv1/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,5 +277,5 @@ func (d SendRequest) Send(remoteAddr string) (string, error) {
return "", fmt.Errorf("error building message: %s", err.Error())
}

return smtpd.Store(ipAddr, d.From.Email, addresses, buff.Bytes())
return smtpd.SaveToDatabase(ipAddr, d.From.Email, addresses, buff.Bytes())
}
Loading

0 comments on commit 10c20dd

Please sign in to comment.