Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
tystuyfzand committed Jul 18, 2021
0 parents commit ad04a46
Show file tree
Hide file tree
Showing 6 changed files with 357 additions and 0 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Tyler Stuyfzand

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
39 changes: 39 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
BUILDDIR=./build
GOTIFY_VERSION=master
PLUGIN_NAME=smtp
PLUGIN_ENTRY=plugin.go
GO_VERSION=`cat $(BUILDDIR)/gotify-server-go-version`
DOCKER_BUILD_IMAGE=gotify/build
DOCKER_WORKDIR=/proj
DOCKER_RUN=docker run --rm -v "$$PWD/.:${DOCKER_WORKDIR}" -v "`go env GOPATH`/pkg/mod/.:/go/pkg/mod:ro" -w ${DOCKER_WORKDIR}
DOCKER_GO_BUILD=go build -mod=readonly -a -installsuffix cgo -ldflags "$$LD_FLAGS" -buildmode=plugin

download-tools:
GO111MODULE=off go get -u github.com/gotify/plugin-api/cmd/gomod-cap

create-build-dir:
mkdir -p ${BUILDDIR} || true

update-go-mod: create-build-dir download-tools
wget -LO ${BUILDDIR}/gotify-server.mod https://raw.githubusercontent.com/gotify/server/${GOTIFY_VERSION}/go.mod
gomod-cap -from ${BUILDDIR}/gotify-server.mod -to go.mod
rm ${BUILDDIR}/gotify-server.mod || true
go mod edit -require=github.com/gotify/server$(shell echo "/${GOTIFY_VERSION}" | egrep -o "^/v[2-9][0-9]*")@${GOTIFY_VERSION}
go mod tidy

get-gotify-server-go-version: create-build-dir
rm ${BUILDDIR}/gotify-server-go-version || true
wget -LO ${BUILDDIR}/gotify-server-go-version https://raw.githubusercontent.com/gotify/server/${GOTIFY_VERSION}/GO_VERSION

build-linux-amd64: get-gotify-server-go-version update-go-mod
${DOCKER_RUN} ${DOCKER_BUILD_IMAGE}:$(GO_VERSION)-linux-amd64 ${DOCKER_GO_BUILD} -o ${BUILDDIR}/${PLUGIN_NAME}-linux-amd64${FILE_SUFFIX}.so ${DOCKER_WORKDIR}

build-linux-arm-7: get-gotify-server-go-version update-go-mod
${DOCKER_RUN} ${DOCKER_BUILD_IMAGE}:$(GO_VERSION)-linux-arm-7 ${DOCKER_GO_BUILD} -o ${BUILDDIR}/${PLUGIN_NAME}-linux-arm-7${FILE_SUFFIX}.so ${DOCKER_WORKDIR}

build-linux-arm64: get-gotify-server-go-version update-go-mod
${DOCKER_RUN} ${DOCKER_BUILD_IMAGE}:$(GO_VERSION)-linux-arm64 ${DOCKER_GO_BUILD} -o ${BUILDDIR}/${PLUGIN_NAME}-linux-arm64${FILE_SUFFIX}.so ${DOCKER_WORKDIR}

build: build-linux-arm-7 build-linux-amd64 build-linux-arm64

.PHONY: build
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Gotify-SMTP
===========

A plugin for piping email messages into [Gotify](https://gotify.net/) without ever hitting an email service. Inspiration for this comes from MailHog and similar implementations where there is no backing email service, it simply forwards/receives messages as needed.

There are other versions of this (specifically using the API), however this is a standalone plugin that can be loaded by Gotify.

Usage
-----

Download the plugin from the releases page, or build it from source (using the Makefile).

Point your application settings at GOTIFY_IP port 1025, with the username being the name of the account you'd like to send messages to.

Limitations
-----------

Currently, HTML messages aren't supported. Markdown might be possible, but currently not planned as most if not all messages include a text/plain variation.
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/tystuyfzand/gotify-smtp

go 1.16

require (
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.15.0
github.com/gotify/plugin-api v1.0.0
)
36 changes: 36 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
github.com/davecgh/go-spew v1.1.0/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/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gotify/plugin-api v1.0.0 h1:kab40p2TEPLzjmcafOc7JOz75aTsYQyS2PXtElH8xmI=
github.com/gotify/plugin-api v1.0.0/go.mod h1:xZfEyqVK/Zvu3RwA/CtpuiwFmzFDxifrrqMaH9BHnyU=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb h1:1w588/yEchbPNpa9sEvOcMZYbWHedwJjg4VOAdDHWHk=
golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
234 changes: 234 additions & 0 deletions plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package main

import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"github.com/emersion/go-smtp"
"github.com/gotify/plugin-api"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"mime/quotedprintable"
"net/mail"
"strings"
"sync"
"time"
)

var (
s *smtp.Server
users = make(map[string]*Plugin)
userLock = &sync.RWMutex{}
)

// GetGotifyPluginInfo returns gotify plugin info
func GetGotifyPluginInfo() plugin.Info {
return plugin.Info{
Name: "SMTP",
ModulePath: "github.com/tystuyfzand/gotify-smtp",
Author: "Tyler Stuyfzand",
Website: "https://meow.tf",
}
}

// startServer sets up the SMTP server.
// This is only called once, and uses usernames to authenticate to different users.
func startServer() {
s = smtp.NewServer(&Backend{})

s.Addr = ":1025"
s.Domain = "0.0.0.0"
s.ReadTimeout = 10 * time.Second
s.WriteTimeout = 10 * time.Second
s.MaxMessageBytes = 1024 * 1024
s.MaxRecipients = 50
s.AllowInsecureAuth = true

go s.ListenAndServe()
}

// Plugin is plugin instance
type Plugin struct {
userCtx plugin.UserContext
msgHandler plugin.MessageHandler
}

// SetMessageHandler implements plugin.Messenger
// Invoked during initialization
func (c *Plugin) SetMessageHandler(h plugin.MessageHandler) {
c.msgHandler = h
}

// Enable adds users to the context map which maps to a Plugin.
func (c *Plugin) Enable() error {
userLock.Lock()
users[c.userCtx.Name] = c
userLock.Unlock()
return nil
}

// Disable removes users from the context map.
func (c *Plugin) Disable() error {
userLock.Lock()
delete(users, c.userCtx.Name)
userLock.Unlock()
return nil
}

// NewGotifyPluginInstance creates a plugin instance for a user context.
func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin {
if s == nil {
startServer()
}

return &Plugin{userCtx: ctx}
}

// The Backend implements SMTP server methods.
type Backend struct {
}

// Login handles a login command with username and password.
func (bkd *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
userLock.RLock()
defer userLock.RUnlock()

if instance, ok := users[username]; ok {
return &Session{instance}, nil
}

return nil, errors.New("user not found")
}

// AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails
func (bkd *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
return nil, smtp.ErrAuthRequired
}

type Session struct {
c *Plugin
}

func (s *Session) Mail(from string, opts smtp.MailOptions) error {
return nil
}

func (s *Session) Rcpt(to string) error {
return nil
}

func (s *Session) Data(r io.Reader) error {
if m, err := mail.ReadMessage(r); err != nil {
return err
} else {
var subject string

if subjectHeader, ok := m.Header["Subject"]; ok && len(subjectHeader) > 0 {
subject = subjectHeader[0]
}

mediaType, params, err := mime.ParseMediaType(m.Header.Get("Content-Type"))

var message string

if err == nil && strings.HasPrefix(mediaType, "multipart/") {
message = ParsePart(m.Body, params["boundary"])
} else {
b, err := ioutil.ReadAll(m.Body)

if err != nil {
return err
}

message = string(b)
}

if s.c != nil && s.c.msgHandler != nil {
s.c.msgHandler.SendMessage(plugin.Message{
Title: subject,
Message: message,
})
}
}

return nil
}

func (s *Session) Reset() {

}

func (s *Session) Logout() error {
return nil
}


// ParsePart will find the first text/plain part from a multipart body.
// Adapted from https://github.com/kirabou/parseMIMEemail.go
func ParsePart(body io.Reader, boundary string) string {
reader := multipart.NewReader(body, boundary)

if reader == nil {
return ""
}

// Go through each of the MIME part of the message Body with NextPart(),
for {
part, err := reader.NextPart()

if err == io.EOF {
break
}

if err != nil {
fmt.Println("Error going through the MIME parts -", err)
break
}

mediaType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))

if err == nil && strings.HasPrefix(mediaType, "multipart/") {
// This is a new multipart to be handled recursively
str := ParsePart(part, params["boundary"])

if str != "" {
return str
}
} else {
if strings.HasPrefix(mediaType, "text/plain") {
b, err := ioutil.ReadAll(part)

if err != nil {
continue
}

encoding := strings.ToLower(part.Header.Get("Content-Transfer-Encoding"))

switch {
case strings.Compare(encoding, "base64") == 0:
b, err = base64.StdEncoding.DecodeString(string(b))
if err != nil {
continue
}

case strings.Compare(encoding, "quoted-printable") == 0:
b, err = ioutil.ReadAll(quotedprintable.NewReader(bytes.NewReader(b)))
if err != nil {
continue
}
}

return string(b)
}
}
}

return ""
}

func main() {
panic("Program must be compiled as a Go plugin")
}

0 comments on commit ad04a46

Please sign in to comment.