diff --git a/go.mod b/go.mod
index 918e9fc1bf0025fce0d0c50cd96a3c77646b8731..fad88a4698a4e0819181450639a060c2b155c7c9 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	firebase.google.com/go v3.13.0+incompatible
 	github.com/BurntSushi/toml v0.4.1 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
+	github.com/emersion/go-smtp v0.15.0
 	github.com/mattn/go-sqlite3 v1.14.9
 	github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
 	github.com/stretchr/testify v1.7.0
@@ -26,6 +27,7 @@ require (
 	github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
 	github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
 	github.com/envoyproxy/go-control-plane v0.10.1 // indirect
 	github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
diff --git a/go.sum b/go.sum
index c7b18e18006691fa53e5fa42f61c93ba83bb8be8..91718f40a5bffa18fcc6323a84c6807bbec388e9 100644
--- a/go.sum
+++ b/go.sum
@@ -89,6 +89,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
+github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
+github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
diff --git a/server/mailserver.go b/server/mailserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..08f2c193e7aaa0a9acb0a476a55e48b4994f1ec2
--- /dev/null
+++ b/server/mailserver.go
@@ -0,0 +1,102 @@
+package server
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/emersion/go-smtp"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/http/httptest"
+	"net/mail"
+	"strings"
+	"sync"
+)
+
+// mailBackend implements SMTP server methods.
+type mailBackend struct {
+	s *Server
+}
+
+func (b *mailBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
+	return &Session{s: b.s}, nil
+}
+
+func (b *mailBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
+	return &Session{s: b.s}, nil
+}
+
+// Session is returned after EHLO.
+type Session struct {
+	s        *Server
+	from, to string
+	mu       sync.Mutex
+}
+
+func (s *Session) AuthPlain(username, password string) error {
+	return nil
+}
+
+func (s *Session) Mail(from string, opts smtp.MailOptions) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.from = from
+	log.Println("Mail from:", from)
+	return nil
+}
+
+func (s *Session) Rcpt(to string) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.to = to
+	log.Println("Rcpt to:", to)
+	return nil
+}
+
+func (s *Session) Data(r io.Reader) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	b, err := ioutil.ReadAll(r)
+	if err != nil {
+		return err
+	}
+
+	log.Println("Data:", string(b))
+	msg, err := mail.ReadMessage(bytes.NewReader(b))
+	if err != nil {
+		return err
+	}
+	body, err := io.ReadAll(msg.Body)
+	if err != nil {
+		return err
+	}
+	topic := strings.TrimSuffix(s.to, "@ntfy.sh")
+	url := fmt.Sprintf("%s/%s", s.s.config.BaseURL, topic)
+	req, err := http.NewRequest("PUT", url, bytes.NewReader(body))
+	if err != nil {
+		return err
+	}
+	subject := msg.Header.Get("Subject")
+	if subject != "" {
+		req.Header.Set("Title", subject)
+	}
+	rr := httptest.NewRecorder()
+	s.s.handle(rr, req)
+	if rr.Code != http.StatusOK {
+		return errors.New("error: " + rr.Body.String())
+	}
+	return nil
+}
+
+func (s *Session) Reset() {
+	s.mu.Lock()
+	s.from = ""
+	s.to = ""
+	s.mu.Unlock()
+}
+
+func (s *Session) Logout() error {
+	return nil
+}
diff --git a/server/server.go b/server/server.go
index 78715d20f69077a5db2a5f5bead0106a396468f2..c2f8034b8c35b997bf9c466a22697923f4b43877 100644
--- a/server/server.go
+++ b/server/server.go
@@ -8,6 +8,7 @@ import (
 	firebase "firebase.google.com/go"
 	"firebase.google.com/go/messaging"
 	"fmt"
+	"github.com/emersion/go-smtp"
 	"google.golang.org/api/option"
 	"heckel.io/ntfy/util"
 	"html/template"
@@ -238,10 +239,16 @@ func (s *Server) Run() error {
 			errChan <- s.httpsServer.ListenAndServeTLS(s.config.CertFile, s.config.KeyFile)
 		}()
 	}
+	if true {
+		go func() {
+			errChan <- s.mailserver()
+		}()
+	}
 	s.mu.Unlock()
 	go s.runManager()
 	go s.runAtSender()
 	go s.runFirebaseKeepliver()
+
 	return <-errChan
 }
 
@@ -722,6 +729,21 @@ func (s *Server) updateStatsAndPrune() {
 		s.messages, len(s.topics), subscribers, messages, len(s.visitors))
 }
 
+func (s *Server) mailserver() error {
+	ms := smtp.NewServer(&mailBackend{s})
+
+	ms.Addr = ":1025"
+	ms.Domain = "localhost"
+	ms.ReadTimeout = 10 * time.Second
+	ms.WriteTimeout = 10 * time.Second
+	ms.MaxMessageBytes = 1024 * 1024
+	ms.MaxRecipients = 50
+	ms.AllowInsecureAuth = true
+
+	log.Println("Starting server at", ms.Addr)
+	return ms.ListenAndServe()
+}
+
 func (s *Server) runManager() {
 	for {
 		select {