Repository: bitmaelum/bitmaelum-suite
Pull request #191 (2021-03-10 10:44:43)

Run bm-bridge and bm-server as a service
This will allow to install a service to run `bm-server` or `bm-bridge` automatically.

It will also use now a config file for `bm-bridge` and add support to run `bm-bridge` in your own organization, this way you can host your own `mailgateway!` account  to translate BitMaelum messages to email addresses on your own organization.


Patch:
diff --git a/cmd/bm-bridge/internal/common.go b/cmd/bm-bridge/internal/common.go
index 5fc44f0..a283b70 100644
--- a/cmd/bm-bridge/internal/common.go
+++ b/cmd/bm-bridge/internal/common.go
@@ -35,9 +35,11 @@ import (
 
 	"github.com/bitmaelum/bitmaelum-suite/internal"
 	"github.com/bitmaelum/bitmaelum-suite/internal/api"
+	"github.com/bitmaelum/bitmaelum-suite/internal/config"
 	"github.com/bitmaelum/bitmaelum-suite/internal/container"
 	"github.com/bitmaelum/bitmaelum-suite/internal/vault"
 	"github.com/google/uuid"
+	"github.com/sirupsen/logrus"
 )
 
 var (
@@ -45,16 +47,21 @@ var (
 	errRoutingNotFound = errors.New("cannot find routing ID for this account")
 )
 
-const dateLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
+const (
+	dateLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
 
-// DefaultDomain is the default (dummy?) domain to be used to translate
-// the BitMaelum address to/from
-const DefaultDomain = "@bitmaelum.network"
+	// the boundary used when converting mails to MIME format
+	dummyBoundary = "stop-using-email-and-start-using-bitmaelum"
 
-// GatewayAddress is the gateway address to send external email messages to
-const GatewayAddress = "mailgateway!"
+	// DefaultDomain to be used to translate messages to regular emails
+	DefaultDomain = "bitmaelum.network"
 
-const dummyBoundary = "stop-using-email-and-start-using-bitmaelum"
+	// DefaultGatewayAccount is the gateway address to send external email messages to
+	DefaultGatewayAccount = "mailgateway!"
+
+	// DestinationBlock is the block used to set the final destination of a relayed email message
+	DestinationBlock = "destination"
+)
 
 // MimeMessage contains the struct to encode or decode a mime message
 // the attachments are in the format "filename" -> base64 data
@@ -78,6 +85,7 @@ func GetClientAndInfo(v *vault.Vault, acc string) (*vault.AccountInfo, *api.API,
 	resolver := container.Instance.GetResolveService()
 	routingInfo, err := resolver.ResolveRouting(info.RoutingID)
 	if err != nil {
+		logrus.Error(err)
 		return nil, nil, errRoutingNotFound
 	}
 
@@ -91,15 +99,23 @@ func GetClientAndInfo(v *vault.Vault, acc string) (*vault.AccountInfo, *api.API,
 
 // AddrToEmail will translate an address string to a valid email using DefaultDomain
 func AddrToEmail(address string) string {
+	if config.Bridge.Server.SMTP.Organization != "" {
+		return address[:strings.IndexByte(address, '@')] + "@" + config.Bridge.Server.SMTP.Domain
+	}
+
 	address = strings.Replace(address, "@", "_", -1)
 	address = strings.Replace(address, "!", "", -1)
 
-	return address + DefaultDomain
+	return address + "@" + config.Bridge.Server.SMTP.Domain
 }
 
-// EmailToAddr will translate a (mocked?) domain on the DefaultDomain to an address string
+// EmailToAddr will translate a domain on the DefaultDomain to an address string
 func EmailToAddr(email string) string {
-	address := strings.Replace(email, DefaultDomain, "!", -1)
+	if config.Bridge.Server.SMTP.Organization != "" {
+		return strings.Replace(email, config.Bridge.Server.SMTP.Domain, config.Bridge.Server.SMTP.Organization+"!", -1)
+	}
+
+	address := strings.Replace(email, "@"+config.Bridge.Server.SMTP.Domain, "!", -1)
 	address = strings.Replace(address, "_", "@", -1)
 	return address
 }
diff --git a/cmd/bm-bridge/internal/fetcher.go b/cmd/bm-bridge/internal/fetcher.go
index 458f231..f486c6d 100644
--- a/cmd/bm-bridge/internal/fetcher.go
+++ b/cmd/bm-bridge/internal/fetcher.go
@@ -31,6 +31,7 @@ import (
 
 	"github.com/bitmaelum/bitmaelum-suite/internal"
 	"github.com/bitmaelum/bitmaelum-suite/internal/api"
+	"github.com/bitmaelum/bitmaelum-suite/internal/config"
 	"github.com/bitmaelum/bitmaelum-suite/internal/container"
 	"github.com/bitmaelum/bitmaelum-suite/internal/message"
 	"github.com/bitmaelum/bitmaelum-suite/internal/messages"
@@ -47,8 +48,6 @@ type Fetcher struct {
 	Vault   *vault.Vault
 }
 
-const destinationBlock = "destination"
-
 // CheckMail will check if there is new mail waiting to be sent to the outside
 func (fe *Fetcher) CheckMail() {
 	msgList, err := fe.Client.GetMailboxMessages(fe.Info.Address.Hash(), "1", time.Time{})
@@ -87,7 +86,7 @@ func (fe *Fetcher) sendMailForMessage(mailboxMessage api.MailboxMessagesMessage)
 
 	// Check if there is a mimeparts and destination block because we need that to reconstruct the mime message
 	for _, block := range dm.Catalog.Blocks {
-		if block.Type == destinationBlock {
+		if block.Type == DestinationBlock {
 			// Use this block as destination address
 			recipientAddress := make([]byte, block.Size)
 			if block.Reader != nil {
@@ -95,22 +94,23 @@ func (fe *Fetcher) sendMailForMessage(mailboxMessage api.MailboxMessagesMessage)
 			}
 
 			if err := processMIMEMessage(string(recipientAddress), dm.Catalog, mailboxMessage.ID); err != nil {
-				logrus.Debugf("error delivering message %s - %v", mailboxMessage.ID, err)
+				logrus.Infof("error delivering message %s - %v", mailboxMessage.ID, err)
 				err = fe.sendPostmasterResponse(dm.Catalog.From.Address, dm.Catalog, err)
 				if err != nil {
 					logrus.Debugf("error sending postmaster - %v", err)
 				}
+			} else {
+				logrus.Infof("message %s processed, sent to %s", mailboxMessage.ID, recipientAddress)
 			}
 
 			// Delete the message
 			fe.Client.RemoveMessage(fe.Info.Address.Hash(), mailboxMessage.ID)
-			logrus.Infof("message %s processed", mailboxMessage.ID)
 
 			return err
 		}
 	}
 
-	logrus.Debugf("the message %s does not contain a \"destination\" block. ignoring", mailboxMessage.ID)
+	logrus.Infof("the message %s does not contain a \"%s\" block. ignoring", mailboxMessage.ID, DestinationBlock)
 	// Delete the message
 	return fe.Client.RemoveMessage(fe.Info.Address.Hash(), mailboxMessage.ID)
 }
@@ -126,13 +126,13 @@ func processMIMEMessage(toMail string, catalog *message.Catalog, msgID string) e
 			Address: toMail,
 		}},
 
-		ID:      "<" + msgID + DefaultDomain + ">",
+		ID:      "<" + msgID + "@" + config.Bridge.Server.SMTP.Domain + ">",
 		Subject: catalog.Subject,
 		Date:    catalog.CreatedAt,
 	}
 
 	for _, block := range catalog.Blocks {
-		if block.Type == destinationBlock {
+		if block.Type == DestinationBlock {
 			// ignore the "destination" block
 			continue
 		}
diff --git a/cmd/bm-bridge/internal/imap/backend/backend.go b/cmd/bm-bridge/internal/imap/backend/backend.go
index ebf1e6b..e0b690d 100644
--- a/cmd/bm-bridge/internal/imap/backend/backend.go
+++ b/cmd/bm-bridge/internal/imap/backend/backend.go
@@ -32,6 +32,7 @@ import (
 	"github.com/bitmaelum/bitmaelum-suite/pkg/address"
 	"github.com/emersion/go-imap"
 	"github.com/emersion/go-imap/backend"
+	"github.com/sirupsen/logrus"
 )
 
 var (
@@ -53,7 +54,8 @@ const (
 
 // Backend will hold the Vault to use
 type Backend struct {
-	Vault *vault.Vault
+	Vault    *vault.Vault
+	Database *Storable
 }
 
 // Login is called when logging in to IMAP. Any password is valid as long as the account is found in the vault
@@ -68,20 +70,23 @@ func (be *Backend) Login(_ *imap.ConnInfo, username, password string) (backend.U
 		return nil, errAccountNotFound
 	}
 
-	user := &User{Account: account, Vault: be.Vault}
+	user := &User{Account: account, Vault: be.Vault, Database: be.Database}
 
 	user.Info, user.Client, err = common.GetClientAndInfo(be.Vault, account)
 	if err != nil {
 		return nil, err
 	}
 
+	logrus.Infof("IMAP: user %s logged in", username)
+
 	return user, nil
 }
 
 // New will create a new Backend
-func New(v *vault.Vault) *Backend {
+func New(v *vault.Vault, d Storable) *Backend {
 	return &Backend{
-		Vault: v,
+		Vault:    v,
+		Database: &d,
 	}
 }
 
@@ -126,9 +131,10 @@ func refreshMailbox(u *User, boxid int, currentMessages []*Message) ([]*Message,
 		}
 
 		message := Message{
-			UID:  uint32(i),
-			ID:   msg.ID,
-			User: u,
+			UID:   uint32(i),
+			ID:    msg.ID,
+			User:  u,
+			Flags: (*u.Database).Retrieve(msg.ID),
 		}
 
 		messages = append(messages, &message)
diff --git a/cmd/bm-bridge/internal/imap/backend/bolt.go b/cmd/bm-bridge/internal/imap/backend/bolt.go
new file mode 100644
index 0000000..ddb97ae
--- /dev/null
+++ b/cmd/bm-bridge/internal/imap/backend/bolt.go
@@ -0,0 +1,106 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package imapgw
+
+import (
+	"encoding/json"
+	"errors"
+	"os"
+	"path/filepath"
+
+	"github.com/sirupsen/logrus"
+	bolt "go.etcd.io/bbolt"
+)
+
+var (
+	errNotFound = errors.New("not found")
+)
+
+type boltStorage struct {
+	client *bolt.DB
+}
+
+// BucketName is the bucket name to store the flags on the bolt db
+const BucketName = "flags"
+
+// BoltDBFile is the filename to store the boltdb database
+const BoltDBFile = "flags.db"
+
+// NewBolt initializes a new repository
+func NewBolt(dbpath *string) Storable {
+	if _, err := os.Stat(*dbpath); os.IsNotExist(err) {
+		os.Mkdir(*dbpath, os.ModeDir)
+	}
+
+	dbFile := filepath.Join(*dbpath, BoltDBFile)
+	db, err := bolt.Open(dbFile, 0600, nil)
+	if err != nil {
+		logrus.Error("Unable to open filepath ", dbFile, err)
+		return nil
+	}
+
+	return &boltStorage{
+		client: db,
+	}
+}
+
+func (b *boltStorage) Retrieve(messageid string) (flags []string) {
+	b.client.View(func(tx *bolt.Tx) error {
+		bucket := tx.Bucket([]byte(BucketName))
+		if bucket == nil {
+			logrus.Trace("messageid not found in BOLT: ", messageid)
+			return errNotFound
+		}
+
+		data := bucket.Get([]byte(messageid))
+		if data == nil {
+			logrus.Trace("messageid not found in BOLT: ", messageid)
+			return errNotFound
+		}
+
+		err := json.Unmarshal([]byte(data), &flags)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	})
+
+	return
+}
+
+func (b *boltStorage) Store(messageid string, flags []string) error {
+	data, err := json.Marshal(flags)
+	if err != nil {
+		return err
+	}
+
+	err = b.client.Update(func(tx *bolt.Tx) error {
+		bucket, err := tx.CreateBucketIfNotExists([]byte(BucketName))
+		if err != nil {
+			logrus.Trace("unable to create bucket on BOLT: ", BucketName, err)
+			return err
+		}
+
+		return bucket.Put([]byte(messageid), data)
+	})
+
+	return err
+}
diff --git a/cmd/bm-bridge/internal/imap/backend/mailbox.go b/cmd/bm-bridge/internal/imap/backend/mailbox.go
index d2a4fc3..787b80e 100644
--- a/cmd/bm-bridge/internal/imap/backend/mailbox.go
+++ b/cmd/bm-bridge/internal/imap/backend/mailbox.go
@@ -20,10 +20,14 @@
 package imapgw
 
 import (
+	"strconv"
+	"strings"
 	"time"
 
 	"github.com/emersion/go-imap"
+	"github.com/emersion/go-imap/backend"
 	"github.com/emersion/go-imap/backend/backendutil"
+	"github.com/sirupsen/logrus"
 )
 
 // Delimiter for mailboxes
@@ -104,7 +108,7 @@ func (mbox *Mailbox) unseenSeqNum() uint32 {
 func (mbox *Mailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) {
 	status := imap.NewMailboxStatus(mbox.name, items)
 	status.Flags = mbox.flags()
-	status.PermanentFlags = []string{"\\*"}
+	status.PermanentFlags = []string{"\\Seen \\Answered \\Flagged \\Deleted \\*"}
 	status.UnseenSeqNum = mbox.unseenSeqNum()
 
 	for _, name := range items {
@@ -199,6 +203,7 @@ func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Lit
 
 // UpdateMessagesFlags will update flags
 func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, flags []string) error {
+
 	for i, msg := range mbox.Messages {
 		var id uint32
 		if uid {
@@ -211,15 +216,51 @@ func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.
 		}
 
 		msg.Flags = backendutil.UpdateFlags(msg.Flags, op, flags)
+
+		logrus.Infof("IMAP: updating flags for message %s", msg.ID)
+		(*msg.User.Database).Store(msg.ID, msg.Flags)
+
 	}
 
 	return nil
 }
 
-// CopyMessages is not implemented yet
+func getIDFromName(boxName string) int {
+	switch boxName {
+	case folderInbox:
+		return 1
+	case folderSent:
+		return 2
+	case folderTrash:
+		return 3
+	default:
+		boxID, _ := strconv.Atoi(strings.TrimPrefix(boxName, "BOX_"))
+		return boxID
+	}
+}
+
+// CopyMessages will copy a message
 func (mbox *Mailbox) CopyMessages(uid bool, seqset *imap.SeqSet, destName string) error {
-	return errUnimplemented
+	boxID := getIDFromName(destName)
+	if boxID == 0 {
+		return backend.ErrNoSuchMailbox
+	}
+
+	for i, msg := range mbox.Messages {
+		var id uint32
+		if uid {
+			id = msg.UID
+		} else {
+			id = uint32(i + 1)
+		}
+		if !seqset.Contains(id) {
+			continue
+		}
 
+		mbox.user.Client.MoveMessage(mbox.user.Info.Address.Hash(), msg.ID, mbox.id, boxID)
+	}
+
+	return nil
 }
 
 // Expunge will delete a message
@@ -230,7 +271,8 @@ func (mbox *Mailbox) Expunge() error {
 		deleted := false
 		for _, flag := range msg.Flags {
 			if flag == imap.DeletedFlag {
-				err := mbox.user.Client.RemoveMessage(mbox.user.Info.Address.Hash(), msg.ID)
+				logrus.Infof("IMAP: deleting message %s", msg.ID)
+				err := mbox.user.Client.RemoveMessageFromBox(mbox.user.Info.Address.Hash(), msg.ID, mbox.id)
 				if err != nil {
 					return err
 				}
diff --git a/cmd/bm-bridge/internal/imap/backend/message.go b/cmd/bm-bridge/internal/imap/backend/message.go
index 6a3345c..159b886 100644
--- a/cmd/bm-bridge/internal/imap/backend/message.go
+++ b/cmd/bm-bridge/internal/imap/backend/message.go
@@ -34,6 +34,7 @@ import (
 	"github.com/emersion/go-imap/backend/backendutil"
 	"github.com/emersion/go-message"
 	"github.com/emersion/go-message/textproto"
+	"github.com/sirupsen/logrus"
 )
 
 // Message holds all the information needed to retrieve a full message
@@ -144,6 +145,7 @@ func (m *Message) Fetch(seqNum uint32, items []imap.FetchItem, user *User) (*ima
 		case imap.FetchBody, imap.FetchBodyStructure:
 			hdr, body, _ := m.headerAndBody()
 			fetched.BodyStructure, _ = backendutil.FetchBodyStructure(hdr, body, item == imap.FetchBodyStructure)
+			logrus.Infof("IMAP: fetching message %s", m.ID)
 		case imap.FetchFlags:
 			fetched.Flags = m.Flags
 		case imap.FetchInternalDate:
diff --git a/cmd/bm-bridge/internal/imap/backend/storage.go b/cmd/bm-bridge/internal/imap/backend/storage.go
new file mode 100644
index 0000000..c7529b8
--- /dev/null
+++ b/cmd/bm-bridge/internal/imap/backend/storage.go
@@ -0,0 +1,28 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package imapgw
+
+// Storable interface is the main interface to store and retrieve flags
+type Storable interface {
+	// Retrieve retrieves the given messageid from the storage and returns its flags info
+	Retrieve(messageid string) []string
+	// Store stores the given proof of work in the storage
+	Store(messageid string, flags []string) error
+}
diff --git a/cmd/bm-bridge/internal/imap/backend/user.go b/cmd/bm-bridge/internal/imap/backend/user.go
index 3c2e416..207ddb1 100644
--- a/cmd/bm-bridge/internal/imap/backend/user.go
+++ b/cmd/bm-bridge/internal/imap/backend/user.go
@@ -30,10 +30,11 @@ import (
 
 // User holds the user info
 type User struct {
-	Account string
-	Vault   *vault.Vault
-	Info    *vault.AccountInfo
-	Client  *api.API
+	Account  string
+	Vault    *vault.Vault
+	Info     *vault.AccountInfo
+	Client   *api.API
+	Database *Storable
 }
 
 // Username from account
@@ -41,6 +42,19 @@ func (u *User) Username() string {
 	return strings.Replace(u.Account, "!", "", -1)
 }
 
+func getNameFromID(boxID int) string {
+	switch boxID {
+	case 1:
+		return folderInbox
+	case 2:
+		return folderSent
+	case 3:
+		return folderTrash
+	default:
+		return "BOX_" + strconv.Itoa(boxID)
+	}
+}
+
 // ListMailboxes for the user
 func (u *User) ListMailboxes(subscribed bool) (mailboxes []backend.Mailbox, err error) {
 	mbl, err := u.Client.GetMailboxList(u.Info.Address.Hash())
@@ -49,18 +63,7 @@ func (u *User) ListMailboxes(subscribed bool) (mailboxes []backend.Mailbox, err
 	}
 
 	for _, box := range mbl.Boxes {
-		boxName := strconv.Itoa(box.ID)
-		switch box.ID {
-		case 1:
-			boxName = folderInbox
-		case 2:
-			boxName = folderSent
-		case 3:
-			boxName = folderTrash
-		default:
-			boxName = "BOX_" + boxName
-		}
-		mailbox, err := u.GetMailbox(boxName)
+		mailbox, err := u.GetMailbox(getNameFromID(box.ID))
 		if err != nil {
 			return nil, err
 		}
@@ -78,25 +81,14 @@ func (u *User) GetMailbox(name string) (backend.Mailbox, error) {
 	}
 
 	for _, box := range mbl.Boxes {
-		boxName := strconv.Itoa(box.ID)
-		switch box.ID {
-		case 1:
-			boxName = folderInbox
-		case 2:
-			boxName = folderSent
-		case 3:
-			boxName = folderTrash
-		default:
-			boxName = "BOX_" + boxName
-		}
-		if name == boxName {
+		if name == getNameFromID(box.ID) {
 			messages, err := refreshMailbox(u, box.ID, nil)
 			if err != nil {
 				return nil, err
 			}
 
 			mailbox := &Mailbox{
-				name:     boxName,
+				name:     getNameFromID(box.ID),
 				id:       box.ID,
 				user:     u,
 				Messages: messages,
diff --git a/cmd/bm-bridge/internal/smtp/backend/backend.go b/cmd/bm-bridge/internal/smtp/backend/backend.go
index 504ae86..947b4a7 100644
--- a/cmd/bm-bridge/internal/smtp/backend/backend.go
+++ b/cmd/bm-bridge/internal/smtp/backend/backend.go
@@ -27,6 +27,7 @@ import (
 	"github.com/bitmaelum/bitmaelum-suite/internal/vault"
 	"github.com/bitmaelum/bitmaelum-suite/pkg/address"
 	"github.com/emersion/go-smtp"
+	"github.com/sirupsen/logrus"
 )
 
 var (
@@ -60,7 +61,15 @@ func (be *Backend) Login(state *smtp.ConnectionState, username, password string)
 		username = username + "!"
 	}
 
-	return be.getSessionFromAccount(username)
+	session, err := be.getSessionFromAccount(username)
+
+	if err != nil {
+		logrus.Errorf("SMTP: user %s error when login - %s", username, err.Error())
+	} else {
+		logrus.Infof("SMTP: user %s logged in", username)
+	}
+
+	return session, err
 }
 
 // AnonymousLogin will accept mails if this server allows to be a email<->bitmaelum gateway
@@ -73,6 +82,9 @@ func (be *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, er
 
 		session.IsGateway = true
 		session.RemoteAddr = state.RemoteAddr
+
+		logrus.Infof("SMTP: external connection received from %s", state.RemoteAddr.String())
+
 		return session, err
 	}
 
diff --git a/cmd/bm-bridge/internal/smtp/backend/session.go b/cmd/bm-bridge/internal/smtp/backend/session.go
index 03599c3..a127613 100644
--- a/cmd/bm-bridge/internal/smtp/backend/session.go
+++ b/cmd/bm-bridge/internal/smtp/backend/session.go
@@ -29,6 +29,8 @@ import (
 	"regexp"
 	"strings"
 
+	"github.com/bitmaelum/bitmaelum-suite/internal/config"
+
 	common "github.com/bitmaelum/bitmaelum-suite/cmd/bm-bridge/internal"
 	"github.com/bitmaelum/bitmaelum-suite/internal"
 	"github.com/bitmaelum/bitmaelum-suite/internal/api"
@@ -43,7 +45,7 @@ import (
 )
 
 var (
-	errInvalidDomain  = errors.New("you can only send to " + common.DefaultDomain)
+	errInvalidDomain  = errors.New("you can only send to @" + config.Bridge.Server.SMTP.Domain)
 	errInvalidFrom    = errors.New("invalid from address, account not found on vault")
 	errInvalidAddress = errors.New("invalid email address")
 	errSPFnotPass     = errors.New("the SPF record not passed validation, are you a spammer? your days are over [https://github.com/bitmaelum/bitmaelum-suite/wiki/How-does-it-differ]")
@@ -69,13 +71,13 @@ func (s *Session) Mail(from string, opts smtp.MailOptions) error {
 	// is from outside bitmaelum network
 	if s.IsGateway {
 		if !isEmailValid(from) {
-			logrus.Tracef("discarding incoming mail from %s, invalid address", from)
+			logrus.Tracef("SMTP: discarding incoming mail from %s, invalid address", from)
 			return errInvalidAddress
 		}
 
 		// Check that the mails comes from outside
-		if strings.HasSuffix(s.From, common.DefaultDomain) {
-			logrus.Tracef("discarding incoming mail from %s, the email is coming from inside", from)
+		if isEmailFromBitMaelumNetwork(s.From) {
+			logrus.Tracef("SMTP: discarding incoming mail from %s, the email is coming from inside", from)
 			return nil
 		}
 
@@ -108,12 +110,12 @@ func (s *Session) Rcpt(to string) error {
 	// belongs to bitmaelum network
 	if s.IsGateway {
 		// Check that the mails goes to @bitmaelum.network
-		if strings.HasSuffix(to, common.DefaultDomain) {
+		if strings.HasSuffix(to, "@"+config.Bridge.Server.SMTP.Domain) {
 			s.To = common.EmailToAddr(to)
 			return nil
 		}
 
-		logrus.Tracef("discarding incoming mail to %s, address is not for ", common.DefaultDomain)
+		logrus.Tracef("SMTP: discarding incoming mail to %s, address is not for ", config.Bridge.Server.SMTP.Domain)
 		return errInvalidAddress
 	}
 
@@ -123,14 +125,19 @@ func (s *Session) Rcpt(to string) error {
 			return errInvalidDomain
 		}
 	} else {
-		if !strings.HasSuffix(to, common.DefaultDomain) {
+		if !strings.HasSuffix(to, "@"+config.Bridge.Server.SMTP.Domain) {
 			// If recipient is outside bitmaelum network then send the message to
 			// the default gateway address
 			if !isEmailValid(to) {
 				return errInvalidAddress
 			}
 
-			s.To = common.GatewayAddress
+			if config.Bridge.Server.SMTP.GatewayAccount == "" {
+				s.To = common.DefaultGatewayAccount
+			} else {
+				s.To = config.Bridge.Server.SMTP.GatewayAccount
+			}
+
 			return nil
 		}
 	}
@@ -147,16 +154,40 @@ func (s *Session) Data(r io.Reader) error {
 		parts := strings.Split(s.From, "@")
 		res := spf.CheckHost(net.ParseIP(strings.Split(s.RemoteAddr.String(), ":")[0]), parts[1], s.From, "")
 		if res != spf.Pass {
-			logrus.Tracef("discarding incoming mail from %s, SPF did not pass", s.From)
+			logrus.Tracef("SMTP: discarding incoming mail from %s, SPF did not pass", s.From)
 			return errSPFnotPass
 		}
-
-		logrus.Infof("processing incoming email. From: %s, To: %s", s.From, s.To)
 	}
 
+	logrus.Infof("SMTP: processing incoming email. From: %s, To: %s", s.From, s.To)
+
 	return s.sendTo(r)
 }
 
+func isDomainHostedOnBitMaelumNetwork(domainName string) bool {
+	// @TODO: We should probably check the DNS to check somehow if the domain is being actually hosted in the BitMaelum network
+	if domainName == config.Bridge.Server.SMTP.Domain || domainName == common.DefaultDomain {
+		return true
+	}
+	return false
+}
+
+func isEmailFromBitMaelumNetwork(emailAddress string) bool {
+	atIndex := strings.LastIndex(emailAddress, "@")
+	return isDomainHostedOnBitMaelumNetwork(emailAddress[atIndex+1:])
+}
+
+func isGatewayDestination(t string) bool {
+	switch t {
+	case config.Bridge.Server.SMTP.GatewayAccount:
+		return true
+	case common.DefaultGatewayAccount:
+		return true
+	}
+
+	return false
+}
+
 // sendTo sends the mail to the bitmaelum network
 func (s *Session) sendTo(r io.Reader) error {
 	// Set up blocks & attachments
@@ -208,11 +239,11 @@ func (s *Session) sendTo(r io.Reader) error {
 	addressing.AddRecipient(toAddr, nil, &recipientInfo.PublicKey)
 
 	blocks = decodedMessage.Blocks
-	if s.To == common.GatewayAddress {
+	if isGatewayDestination(s.To) {
 		if decodedMessage.To == nil || len(decodedMessage.To) < 1 {
 			return errIncorrectFormat
 		}
-		blocks = append(blocks, "destination,"+decodedMessage.To[0].Address)
+		blocks = append(blocks, common.DestinationBlock+","+decodedMessage.To[0].Address)
 	}
 
 	for filename, base64Data := range decodedMessage.Attachments {
@@ -267,7 +298,7 @@ func isEmailValid(e string) bool {
 	if !emailRegex.MatchString(e) {
 		return false
 	}
-	if strings.HasSuffix(e, common.DefaultDomain) {
+	if isEmailFromBitMaelumNetwork(e) {
 		return true
 	}
 	parts := strings.Split(e, "@")
diff --git a/cmd/bm-bridge/main.go b/cmd/bm-bridge/main.go
index f330e58..b49ce14 100644
--- a/cmd/bm-bridge/main.go
+++ b/cmd/bm-bridge/main.go
@@ -22,6 +22,7 @@ package main
 import (
 	"context"
 	"fmt"
+	"io/ioutil"
 	"log"
 	"math/rand"
 	"os"
@@ -31,6 +32,7 @@ import (
 
 	common "github.com/bitmaelum/bitmaelum-suite/cmd/bm-bridge/internal"
 	"github.com/bitmaelum/bitmaelum-suite/internal"
+	"github.com/kardianos/service"
 
 	imapgw "github.com/bitmaelum/bitmaelum-suite/cmd/bm-bridge/internal/imap/backend"
 	smtpgw "github.com/bitmaelum/bitmaelum-suite/cmd/bm-bridge/internal/smtp/backend"
@@ -42,20 +44,36 @@ import (
 )
 
 type options struct {
-	ImapHost       string `long:"imaphost" description:"Host:Port to imap server from" required:"false"`
-	SMTPHost       string `long:"smtphost" description:"Host:Port to smtp server from" required:"false"`
-	GatewayAccount string `long:"gatewayaccount" description:"Account to be used for a email->bitmaelum gateway. If empty it will require authentication to send mails." default:""`
-	Config         string `short:"c" long:"config" description:"Path to your configuration file"`
-	Password       string `short:"p" long:"password" description:"Vault password" default:""`
-	Vault          string `long:"vault" description:"Custom vault file" default:""`
-	Version        bool   `short:"v" long:"version" description:"Display version information"`
-	Debug          bool   `long:"debug" description:"It will print all the communications to this imap/smtp server"`
+	Config   string `short:"c" long:"config" description:"Path to your configuration file"`
+	Password string `short:"p" long:"password" description:"Vault password" default:""`
+	Vault    string `long:"vault" description:"Custom vault file" default:""`
+	Version  bool   `short:"v" long:"version" description:"Display version information"`
+	Debug    bool   `long:"debug" description:"It will print all the communications to this imap/smtp server"`
+	Service  bool   `long:"service" description:"Execute as a service"`
 }
 
 // Opts are the options set through the command line
 var opts options
 
-func main() {
+type program struct {
+	context    context.Context
+	cancelFunc context.CancelFunc
+}
+
+func (p *program) Start(s service.Service) error {
+	// Start should not block. Do the actual work async.
+	go p.Run()
+	return nil
+}
+
+func (p *program) Stop(s service.Service) error {
+	// Stop should not block. Return with a few seconds.
+	p.context.Done()
+	<-time.After(time.Second * 2)
+	return nil
+}
+
+func (p *program) Run() {
 	rand.Seed(time.Now().UnixNano())
 
 	internal.ParseOptions(&opts)
@@ -65,16 +83,19 @@ func main() {
 		os.Exit(0)
 	}
 
-	if opts.ImapHost == "" && opts.SMTPHost == "" {
-		fmt.Println(" error: either --imaphost or --smtphost needs to be specified")
-		os.Exit(0)
+	config.LoadBridgeConfig(opts.Config)
+
+	if !config.Bridge.Server.SMTP.Enabled && !config.Bridge.Server.IMAP.Enabled {
+		logrus.Fatal("neither smtp nor imap are enabled in config file")
 	}
 
-	config.LoadClientConfig(opts.Config)
+	if config.Bridge.Server.SMTP.Domain == "" {
+		config.Bridge.Server.SMTP.Domain = common.DefaultDomain
+	}
 
 	// Set default vault info if set in config
 	vault.VaultPassword = opts.Password
-	vault.VaultPath = config.Client.Vault.Path
+	vault.VaultPath = config.Bridge.Vault.Path
 	if opts.Vault != "" {
 		vault.VaultPath = opts.Vault
 	}
@@ -92,29 +113,51 @@ func main() {
 	// setup context so we can easily stop all components of the server
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
+	p.cancelFunc = cancel
+	p.context = ctx
 
 	// Wait for signals and cancel context
 	go setupSignals(cancel)
 
 	// Start the SMTP server if needed
-	if opts.GatewayAccount != "" || opts.SMTPHost != "" {
+	if config.Bridge.Server.SMTP.Enabled {
 		go startSMTPServer(v, cancel)
 	}
 
 	// Start the IMAP server if needed
-	if opts.ImapHost != "" {
+	if config.Bridge.Server.IMAP.Enabled {
 		go startImapServer(v, cancel)
 	}
 
-	if opts.GatewayAccount != "" {
+	if config.Bridge.Server.SMTP.Gateway {
 		// If gateway mode then start to fetch for pending mails
-		go startFetcher(ctx, cancel, v, opts.GatewayAccount)
+		go startFetcher(ctx, cancel, v, config.Bridge.Server.SMTP.GatewayAccount)
 	}
 
 	<-ctx.Done()
 
 }
 
+func main() {
+	prg := &program{}
+	internal.ParseOptions(&opts)
+
+	if opts.Service {
+		s, err := service.New(prg, internal.GetBMBridgeService(""))
+		if err != nil {
+			logrus.Fatal(err)
+		}
+
+		err = s.Run()
+		if err != nil {
+			logrus.Error(err)
+		}
+		return
+	}
+
+	prg.Run()
+}
+
 func setupSignals(cancel context.CancelFunc) {
 	go func() {
 		// Capture INT and TERM signals
@@ -130,11 +173,11 @@ func setupSignals(cancel context.CancelFunc) {
 }
 
 func startImapServer(v *vault.Vault, cancel context.CancelFunc) {
-	be := imapgw.New(v)
+	be := imapgw.New(v, imapgw.NewBolt(&config.Bridge.Server.IMAP.Path))
 	s := server.New(be)
-	s.Addr = opts.ImapHost
+	s.Addr = fmt.Sprintf("%s:%d", config.Bridge.Server.IMAP.Host, config.Bridge.Server.IMAP.Port)
 	s.AllowInsecureAuth = true // We should run on TLS
-	if opts.Debug {
+	if config.Bridge.Server.IMAP.Debug {
 		s.Debug = os.Stdout
 	}
 
@@ -147,20 +190,25 @@ func startImapServer(v *vault.Vault, cancel context.CancelFunc) {
 }
 
 func startSMTPServer(v *vault.Vault, cancel context.CancelFunc) {
-	be := smtpgw.New(v, opts.GatewayAccount)
+	be := smtpgw.New(v, config.Bridge.Server.SMTP.GatewayAccount)
 	s := smtp.NewServer(be)
-	s.Addr = opts.SMTPHost
-	s.Domain = "bitmaelum.network"
+	s.Addr = fmt.Sprintf("%s:%d", config.Bridge.Server.SMTP.Host, config.Bridge.Server.SMTP.Port)
+	s.Domain = config.Bridge.Server.SMTP.Domain
 	s.ReadTimeout = 10 * time.Second
 	s.WriteTimeout = 10 * time.Second
 	s.MaxMessageBytes = 10 * 1024 * 1024 // 10 MB
 	s.MaxRecipients = 1
 	s.AllowInsecureAuth = true
-	if opts.Debug {
+
+	// This is needed since package SPF will use log
+	log.SetOutput(ioutil.Discard)
+
+	if config.Bridge.Server.SMTP.Debug {
 		s.Debug = os.Stdout
+		log.SetOutput(os.Stdout)
 	}
 
-	if opts.GatewayAccount != "" {
+	if config.Bridge.Server.SMTP.Gateway {
 		logrus.Infof("Starting SMTP server (gw mode)")
 	} else {
 		logrus.Infof("Starting SMTP server")
diff --git a/cmd/bm-bridge/main_windows.go b/cmd/bm-bridge/main_windows.go
new file mode 100644
index 0000000..8c973f3
--- /dev/null
+++ b/cmd/bm-bridge/main_windows.go
@@ -0,0 +1,38 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package main
+
+import (
+	"github.com/Freman/eventloghook"
+	"github.com/bitmaelum/bitmaelum-suite/internal"
+	"github.com/sirupsen/logrus"
+)
+
+func init() {
+	internal.ParseOptions(&opts)
+
+	if opts.Service {
+		elog, err = eventlog.Open("BitMaelum Bridge")
+		if err == nil {
+			defer elog.Close()
+			logrus.AddHook(eventloghook.NewHook(elog))
+		}
+	}
+}
diff --git a/cmd/bm-config/cmd/init_config.go b/cmd/bm-config/cmd/init_config.go
index dc967f9..add2211 100644
--- a/cmd/bm-config/cmd/init_config.go
+++ b/cmd/bm-config/cmd/init_config.go
@@ -40,18 +40,23 @@ This command creates default templates that you can use as a starting point.`,
 	Run: func(cmd *cobra.Command, args []string) {
 		genC, _ := cmd.Flags().GetBool("client")
 		genS, _ := cmd.Flags().GetBool("server")
+		genB, _ := cmd.Flags().GetBool("bridge")
 
 		// If not client or server selected, generate them both
-		genBoth := !genC && !genS
+		genAll := !genC && !genS && !genB
 
-		if genBoth || genC {
+		if genAll || genC {
 			createFile("./"+config.ClientConfigFile, config.GenerateClientConfig)
 			fmt.Println("Generated client configuration file")
 		}
-		if genBoth || genS {
+		if genAll || genS {
 			createFile("./"+config.ServerConfigFile, config.GenerateServerConfig)
 			fmt.Println("Generated server configuration file")
 		}
+		if genAll || genB {
+			createFile("./"+config.BridgeConfigFile, config.GenerateBridgeConfig)
+			fmt.Println("Generated bridge configuration file")
+		}
 	},
 }
 
@@ -77,4 +82,5 @@ func init() {
 
 	initConfigCmd.Flags().Bool("client", false, "Generate only the client configuration")
 	initConfigCmd.Flags().Bool("server", false, "Generate only the server configuration")
+	initConfigCmd.Flags().Bool("bridge", false, "Generate only the bridge configuration")
 }
diff --git a/cmd/bm-config/cmd/service.go b/cmd/bm-config/cmd/service.go
new file mode 100644
index 0000000..94295e2
--- /dev/null
+++ b/cmd/bm-config/cmd/service.go
@@ -0,0 +1,69 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package cmd
+
+import (
+	"os"
+
+	"github.com/bitmaelum/bitmaelum-suite/internal"
+	"github.com/kardianos/service"
+	"github.com/spf13/cobra"
+)
+
+// serviceCmd represents the serviceCmd command
+var serviceCmd = &cobra.Command{
+	Use:   "service",
+	Short: "Service management",
+}
+
+func getServiceName(cmd *cobra.Command) *service.Config {
+	if i, _ := cmd.InheritedFlags().GetBool("bm-server"); i {
+		return internal.GetBMServerService("")
+	}
+
+	if i, _ := cmd.InheritedFlags().GetBool("bm-bridge"); i {
+		return internal.GetBMBridgeService("")
+	}
+
+	cmd.Help()
+	os.Exit(0)
+	return nil
+}
+
+func getServiceNameForInstall(cmd *cobra.Command) *service.Config {
+	if i, _ := cmd.InheritedFlags().GetBool("bm-server"); i {
+		return internal.GetBMServerService("bm-server")
+	}
+
+	if i, _ := cmd.InheritedFlags().GetBool("bm-bridge"); i {
+		return internal.GetBMBridgeService("bm-bridge")
+	}
+
+	cmd.Help()
+	os.Exit(0)
+	return nil
+}
+
+func init() {
+	serviceCmd.PersistentFlags().Bool("bm-server", false, "Manage bm-server service")
+	serviceCmd.PersistentFlags().Bool("bm-bridge", false, "Manage bm-bridge service")
+
+	rootCmd.AddCommand(serviceCmd)
+}
diff --git a/cmd/bm-config/cmd/service_install.go b/cmd/bm-config/cmd/service_install.go
new file mode 100644
index 0000000..9961a8b
--- /dev/null
+++ b/cmd/bm-config/cmd/service_install.go
@@ -0,0 +1,86 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"runtime"
+
+	"github.com/kardianos/service"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+	"golang.org/x/term"
+)
+
+// serviceInstallCmd represents the serviceInstallCmd command
+var serviceInstallCmd = &cobra.Command{
+	Use:   "install",
+	Short: "Installs the service into the system",
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Print("Installing service... ")
+
+		err := installService(getServiceNameForInstall(cmd))
+		if err != nil {
+			fmt.Println("ERR")
+			logrus.Fatalf("Unable to install service: %v", err)
+		}
+
+		fmt.Println("OK")
+
+		if i, _ := cmd.Flags().GetBool("start"); i {
+			fmt.Print("Starting service... ")
+
+			err = startService(getServiceName(cmd))
+			if err != nil {
+				fmt.Println("ERR")
+				logrus.Fatalf("Unable to start service: %v", err)
+			}
+
+			fmt.Println("OK")
+		}
+	},
+}
+
+func installService(svc *service.Config) error {
+	if svc.UserName != "" && runtime.GOOS == "windows" {
+		// On windows we need the user password
+		fmt.Print("\nPlease enter the password for your username \"" + svc.UserName + "\": ")
+		b, _ := term.ReadPassword(int(os.Stdin.Fd()))
+		fmt.Println("")
+
+		svc.Option["Password"] = string(b)
+	}
+
+	s, err := service.New(nil, svc)
+	if err != nil {
+		return err
+	}
+
+	return service.Control(s, "install")
+}
+
+func init() {
+	serviceInstallCmd.Flags().String("password", "", "Specify the vault password (bm-bridge) (probably not needed if running the service as a user. This is not the user password, but the vault password)")
+	serviceInstallCmd.Flags().String("username", "", "Set the username to run the service as")
+	serviceInstallCmd.Flags().Bool("start", false, "Start the service after install")
+
+	serviceCmd.AddCommand(serviceInstallCmd)
+}
diff --git a/cmd/bm-config/cmd/service_remove.go b/cmd/bm-config/cmd/service_remove.go
new file mode 100644
index 0000000..35e1052
--- /dev/null
+++ b/cmd/bm-config/cmd/service_remove.go
@@ -0,0 +1,66 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/kardianos/service"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+)
+
+// serviceRemoveCmd represents the serviceRemoveCmd command
+var serviceRemoveCmd = &cobra.Command{
+	Use:   "remove",
+	Short: "Remove the service from the system",
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Print("Stopping service... ")
+		err := stopService(getServiceName(cmd))
+		if err != nil {
+			fmt.Println("ERR")
+			logrus.Fatalf("Unable to stop service: %v", err)
+		}
+
+		fmt.Println("OK")
+
+		fmt.Print("Removing service... ")
+		err = removeService(getServiceName(cmd))
+		if err != nil {
+			fmt.Println("ERR")
+			logrus.Fatalf("Unable to remove service: %v", err)
+		}
+
+		fmt.Println("OK")
+	},
+}
+
+func removeService(svc *service.Config) error {
+	s, err := service.New(nil, svc)
+	if err != nil {
+		return err
+	}
+
+	return service.Control(s, "uninstall")
+}
+
+func init() {
+	serviceCmd.AddCommand(serviceRemoveCmd)
+}
diff --git a/cmd/bm-config/cmd/service_restart.go b/cmd/bm-config/cmd/service_restart.go
new file mode 100644
index 0000000..79d63b5
--- /dev/null
+++ b/cmd/bm-config/cmd/service_restart.go
@@ -0,0 +1,56 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+)
+
+// serviceRestartCmd represents the serviceStartCmd command
+var serviceRestartCmd = &cobra.Command{
+	Use:   "restart",
+	Short: "Restarts the service",
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Print("Stopping service... ")
+		err := stopService(getServiceName(cmd))
+		if err != nil {
+			fmt.Println("ERR")
+			logrus.Fatalf("Unable to stop service: %v", err)
+		}
+
+		fmt.Println("OK")
+
+		fmt.Print("Starting service... ")
+		err = startService(getServiceName(cmd))
+		if err != nil {
+			fmt.Println("ERR")
+			logrus.Fatalf("Unable to start service: %v", err)
+		}
+
+		fmt.Println("OK")
+	},
+}
+
+func init() {
+	serviceCmd.AddCommand(serviceRestartCmd)
+}
diff --git a/cmd/bm-config/cmd/service_start.go b/cmd/bm-config/cmd/service_start.go
new file mode 100644
index 0000000..83798d9
--- /dev/null
+++ b/cmd/bm-config/cmd/service_start.go
@@ -0,0 +1,57 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/kardianos/service"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+)
+
+// serviceStartCmd represents the serviceStartCmd command
+var serviceStartCmd = &cobra.Command{
+	Use:   "start",
+	Short: "Starts the service",
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Print("Starting service... ")
+		err := startService(getServiceName(cmd))
+		if err != nil {
+			fmt.Println("ERR")
+			logrus.Fatalf("Unable to start service: %v", err)
+		}
+
+		fmt.Println("OK")
+	},
+}
+
+func startService(svc *service.Config) error {
+	s, err := service.New(nil, svc)
+	if err != nil {
+		return err
+	}
+
+	return service.Control(s, "start")
+}
+
+func init() {
+	serviceCmd.AddCommand(serviceStartCmd)
+}
diff --git a/cmd/bm-config/cmd/service_stop.go b/cmd/bm-config/cmd/service_stop.go
new file mode 100644
index 0000000..455efaf
--- /dev/null
+++ b/cmd/bm-config/cmd/service_stop.go
@@ -0,0 +1,57 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/kardianos/service"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+)
+
+// serviceStopCmd represents the serviceStopCmd command
+var serviceStopCmd = &cobra.Command{
+	Use:   "stop",
+	Short: "Stops the service",
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Print("Stopping service... ")
+		err := stopService(getServiceName(cmd))
+		if err != nil {
+			fmt.Println("ERR")
+			logrus.Fatalf("Unable to stop service: %v", err)
+		}
+
+		fmt.Println("OK")
+	},
+}
+
+func stopService(svc *service.Config) error {
+	s, err := service.New(nil, svc)
+	if err != nil {
+		return err
+	}
+
+	return service.Control(s, "stop")
+}
+
+func init() {
+	serviceCmd.AddCommand(serviceStopCmd)
+}
diff --git a/cmd/bm-server/handler/message.go b/cmd/bm-server/handler/message.go
index bf264a0..75d158f 100644
--- a/cmd/bm-server/handler/message.go
+++ b/cmd/bm-server/handler/message.go
@@ -35,6 +35,11 @@ const (
 	incorrectBlock string = "incorrect block ID"
 )
 
+type moveMessageInput struct {
+	From int `json:"from"`
+	To   int `json:"to"`
+}
+
 // DeleteMessage will delete a message
 func DeleteMessage(w http.ResponseWriter, req *http.Request) {
 	haddr, err := hash.NewFromHash(mux.Vars(req)["addr"])
@@ -86,6 +91,101 @@ func GetMessage(w http.ResponseWriter, req *http.Request) {
 	_ = httputils.JSONOut(w, http.StatusOK, output)
 }
 
+// CopyMessage will copy a message to a box
+func CopyMessage(w http.ResponseWriter, req *http.Request) {
+	haddr, err := hash.NewFromHash(mux.Vars(req)["addr"])
+	if err != nil {
+		httputils.ErrorOut(w, http.StatusNotFound, accountNotFound)
+		return
+	}
+
+	messageID := mux.Vars(req)["message"]
+
+	var input moveMessageInput
+	err = httputils.DecodeBody(w, req.Body, &input)
+	if err != nil {
+		httputils.ErrorOut(w, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	ar := container.Instance.GetAccountRepo()
+	err = ar.CopyMessage(*haddr, messageID, input.To)
+	if err != nil {
+		httputils.ErrorOut(w, http.StatusNotFound, err.Error())
+		return
+	}
+
+	httputils.JSONOut(w, http.StatusOK, "")
+}
+
+// MoveMessage will move a message to a box
+func MoveMessage(w http.ResponseWriter, req *http.Request) {
+	haddr, err := hash.NewFromHash(mux.Vars(req)["addr"])
+	if err != nil {
+		httputils.ErrorOut(w, http.StatusNotFound, accountNotFound)
+		return
+	}
+
+	messageID := mux.Vars(req)["message"]
+
+	var input moveMessageInput
+	err = httputils.DecodeBody(w, req.Body, &input)
+	if err != nil {
+		httputils.ErrorOut(w, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	ar := container.Instance.GetAccountRepo()
+	err = ar.MoveMessage(*haddr, messageID, input.From, input.To)
+	if err != nil {
+		httputils.ErrorOut(w, http.StatusNotFound, err.Error())
+		return
+	}
+
+	httputils.JSONOut(w, http.StatusOK, "")
+}
+
+// RemoveMessageFromBox will remove a message from a box
+func RemoveMessageFromBox(w http.ResponseWriter, req *http.Request) {
+	haddr, err := hash.NewFromHash(mux.Vars(req)["addr"])
+	if err != nil {
+		httputils.ErrorOut(w, http.StatusNotFound, accountNotFound)
+		return
+	}
+
+	messageID := mux.Vars(req)["message"]
+
+	var input moveMessageInput
+	err = httputils.DecodeBody(w, req.Body, &input)
+	if err != nil {
+		httputils.ErrorOut(w, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	ar := container.Instance.GetAccountRepo()
+	err = ar.RemoveFromBox(*haddr, input.From, messageID)
+	if err != nil {
+		httputils.ErrorOut(w, http.StatusNotFound, err.Error())
+		return
+	}
+
+	// Should the message be removed if it's not linked to any mailbox?
+	boxes, err := ar.GetAllBoxes(*haddr)
+	if err == nil {
+		var found bool
+		for box := range boxes {
+			if ar.ExistsInBox(*haddr, box, messageID) {
+				found = true
+			}
+		}
+		if !found {
+			ar.RemoveMessage(*haddr, messageID)
+		}
+	}
+
+	httputils.JSONOut(w, http.StatusOK, "")
+}
+
 // GetMessageBlock will return a message block
 func GetMessageBlock(w http.ResponseWriter, req *http.Request) {
 	haddr, err := hash.NewFromHash(mux.Vars(req)["addr"])
diff --git a/cmd/bm-server/internal/account/file_message.go b/cmd/bm-server/internal/account/file_message.go
index a51ed29..1183eba 100644
--- a/cmd/bm-server/internal/account/file_message.go
+++ b/cmd/bm-server/internal/account/file_message.go
@@ -175,6 +175,80 @@ func (r *fileRepo) RemoveMessage(addr hash.Hash, msgID string) error {
 	return nil
 }
 
+// CopyMessage copies a message to a box
+func (r *fileRepo) CopyMessage(addr hash.Hash, msgID string, boxID int) error {
+	m := r.getPath(addr, filepath.Join(messageDir, msgID))
+	// Check if message exists
+	_, err := r.fs.Stat(m)
+	if err != nil {
+		return err
+	}
+
+	// Check if box exists
+	var boxExists bool
+	boxes, err := r.GetAllBoxes(addr)
+	if err != nil {
+		return err
+	}
+	for _, box := range boxes {
+		if box.ID == boxID {
+			boxExists = true
+			break
+		}
+	}
+	if !boxExists {
+		return errors.New("box not found")
+	}
+
+	return r.AddToBox(addr, boxID, msgID)
+}
+
+// MoveMessage copies a message to a box
+func (r *fileRepo) MoveMessage(addr hash.Hash, msgID string, fromBoxID, toBoxID int) error {
+	m := r.getPath(addr, filepath.Join(messageDir, msgID))
+	// Check if message exists
+	_, err := r.fs.Stat(m)
+	if err != nil {
+		return err
+	}
+
+	// Check if box exists
+	var boxExists int
+	boxes, err := r.GetAllBoxes(addr)
+	if err != nil {
+		return err
+	}
+	for _, box := range boxes {
+		if box.ID == fromBoxID || box.ID == toBoxID {
+			boxExists++
+			if boxExists > 1 {
+				break
+			}
+		}
+	}
+	if boxExists < 2 {
+		return errors.New("box not found")
+	}
+
+	err = r.AddToBox(addr, toBoxID, msgID)
+	if err == nil {
+		return r.RemoveFromBox(addr, fromBoxID, msgID)
+	}
+
+	return err
+}
+
+// ExistsInBox checks if the message exists on the box
+func (r *fileRepo) ExistsInBox(addr hash.Hash, boxID int, msgID string) bool {
+	boxPath := r.getPath(addr, filepath.Join(getBoxAsString(boxID), msgID))
+
+	if _, err := r.fs.Stat(boxPath); os.IsNotExist(err) {
+		return false
+	}
+
+	return true
+}
+
 // AddToBox Symlinks the message to the box
 func (r *fileRepo) AddToBox(addr hash.Hash, boxID int, msgID string) error {
 	// Check if we can symlink
diff --git a/cmd/bm-server/internal/account/repository.go b/cmd/bm-server/internal/account/repository.go
index efa9ff7..509e053 100644
--- a/cmd/bm-server/internal/account/repository.go
+++ b/cmd/bm-server/internal/account/repository.go
@@ -103,8 +103,11 @@ type BoxRepository interface {
 type MessageRepository interface {
 	CreateMessage(addr hash.Hash, msgID string) error
 	RemoveMessage(addr hash.Hash, msgID string) error
+	CopyMessage(addr hash.Hash, msgID string, boxID int) error
+	MoveMessage(addr hash.Hash, msgID string, fromBoxID, toBoxID int) error
 	AddToBox(addr hash.Hash, boxID int, msgID string) error
 	RemoveFromBox(addr hash.Hash, boxID int, msgID string) error
+	ExistsInBox(addr hash.Hash, boxID int, msgID string) bool
 
 	// Message boxes
 	FetchListFromBox(addr hash.Hash, box int, since time.Time, offset, limit int) (*MessageList, error)
diff --git a/cmd/bm-server/main.go b/cmd/bm-server/main.go
index 252f548..46d2eaa 100644
--- a/cmd/bm-server/main.go
+++ b/cmd/bm-server/main.go
@@ -42,17 +42,37 @@ import (
 	"github.com/bitmaelum/bitmaelum-suite/internal/resolver"
 	"github.com/gorilla/handlers"
 	"github.com/gorilla/mux"
+	"github.com/kardianos/service"
 	"github.com/sirupsen/logrus"
 )
 
 type options struct {
 	Config  string `short:"c" long:"config" description:"Configuration file"`
 	Version bool   `short:"v" long:"version" description:"Display version information"`
+	Service bool   `long:"service" description:"Execute as a service"`
 }
 
 var opts options
 
-func main() {
+type program struct {
+	context    context.Context
+	cancelFunc context.CancelFunc
+}
+
+func (p *program) Start(s service.Service) error {
+	// Start should not block. Do the actual work async.
+	go p.Run()
+	return nil
+}
+
+func (p *program) Stop(s service.Service) error {
+	// Stop should not block. Return with a few seconds.
+	p.context.Done()
+	<-time.After(time.Second * 2)
+	return nil
+}
+
+func (p *program) Run() {
 	rand.Seed(time.Now().UnixNano())
 
 	internal.ParseOptions(&opts)
@@ -74,6 +94,8 @@ func main() {
 	// setup context so we can easily stop all components of the server
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
+	p.cancelFunc = cancel
+	p.context = ctx
 
 	// Wait for signals and cancel context
 	setupSignals(cancel)
@@ -98,6 +120,26 @@ func main() {
 	logrus.Tracef("Context is done. Exiting")
 }
 
+func main() {
+	prg := &program{}
+	internal.ParseOptions(&opts)
+
+	if opts.Service {
+		s, err := service.New(prg, internal.GetBMServerService(""))
+		if err != nil {
+			logrus.Fatal(err)
+		}
+
+		err = s.Run()
+		if err != nil {
+			logrus.Error(err)
+		}
+		return
+	}
+
+	prg.Run()
+}
+
 func initDispatcher() {
 	logrus.Tracef("Starting webhooks")
 	if config.Server.Webhooks.System == "default" {
@@ -221,10 +263,14 @@ func setupRouter() *mux.Router {
 	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/box/{box:[0-9]+}", handler.RetrieveMessagesFromBox).Methods("GET")
 	// Message fetching
 	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/ticket", handler.GetClientToServerTicket).Methods("POST")
-	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/message/{message:[A-Za-z0-9-]+}", handler.DeleteMessage).Methods("DELETE")
 	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/message/{message:[A-Za-z0-9-]+}", handler.GetMessage).Methods("GET")
 	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/message/{message:[A-Za-z0-9-]+}/block/{block:[A-Za-z0-9-]+}", handler.GetMessageBlock).Methods("GET")
 	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/message/{message:[A-Za-z0-9-]+}/attachment/{attachment:[A-Za-z0-9-]+}", handler.GetMessageAttachment).Methods("GET")
+	// Message handling
+	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/message/{message:[A-Za-z0-9-]+}", handler.DeleteMessage).Methods("DELETE")
+	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/message/{message:[A-Za-z0-9-]+}/move", handler.MoveMessage).Methods("POST")
+	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/message/{message:[A-Za-z0-9-]+}/copy", handler.CopyMessage).Methods("POST")
+	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/message/{message:[A-Za-z0-9-]+}/delete", handler.RemoveMessageFromBox).Methods("POST")
 	// Api keys
 	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/apikey", handler.CreateAPIKey).Methods("POST")
 	authRouter.HandleFunc("/account/{addr:[A-Za-z0-9]{64}}/apikey", handler.ListAPIKeys).Methods("GET")
diff --git a/cmd/bm-server/main_windows.go b/cmd/bm-server/main_windows.go
new file mode 100644
index 0000000..a440daf
--- /dev/null
+++ b/cmd/bm-server/main_windows.go
@@ -0,0 +1,38 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package main
+
+import (
+	"github.com/Freman/eventloghook"
+	"github.com/bitmaelum/bitmaelum-suite/internal"
+	"github.com/sirupsen/logrus"
+)
+
+func init() {
+	internal.ParseOptions(&opts)
+
+	if opts.Service {
+		elog, err = eventlog.Open("BitMaelum Server")
+		if err == nil {
+			defer elog.Close()
+			logrus.AddHook(eventloghook.NewHook(elog))
+		}
+	}
+}
diff --git a/go.mod b/go.mod
index 1280aea..f33b4e5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/bitmaelum/bitmaelum-suite
 go 1.13
 
 require (
+	github.com/Freman/eventloghook v0.0.0-20191003051739-e4d803b6b48b
 	github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
 	github.com/cloudflare/gokey v0.1.0
 	github.com/coreos/go-semver v0.3.0
@@ -25,6 +26,7 @@ require (
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/jessevdk/go-flags v1.4.0
 	github.com/jhillyerd/enmime v0.8.4
+	github.com/kardianos/service v1.2.0
 	github.com/mattn/go-colorable v0.1.8 // indirect
 	github.com/mattn/go-sqlite3 v2.0.3+incompatible
 	github.com/mborders/artifex v0.4.0
@@ -49,7 +51,7 @@ require (
 	golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
 	golang.org/x/mod v0.4.1 // indirect
 	golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
-	golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
+	golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
 	golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
 	golang.org/x/text v0.3.4 // indirect
 	golang.org/x/tools v0.1.0 // indirect
diff --git a/go.sum b/go.sum
index de5f02d..922aa2f 100644
--- a/go.sum
+++ b/go.sum
@@ -14,6 +14,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Freman/eventloghook v0.0.0-20191003051739-e4d803b6b48b h1:IltY1fRcdIshI/c8KOdmaO8P4lBwDXHJYPymMisvvDs=
+github.com/Freman/eventloghook v0.0.0-20191003051739-e4d803b6b48b/go.mod h1:VGwG8f2pQ8SAFjTSH3PEDmLdlvi0XTd7a4C4AZn+pVw=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -188,6 +190,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g=
+github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -449,6 +453,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
 golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201231184435-2d18734c6014 h1:joucsQqXmyBVxViHCPFjG3hx8JzIFSaym3l3MM/Jsdg=
diff --git a/internal/api/message.go b/internal/api/message.go
index 6a4df66..c9f24ab 100644
--- a/internal/api/message.go
+++ b/internal/api/message.go
@@ -52,6 +52,80 @@ func (api *API) RemoveMessage(addr hash.Hash, messageID string) error {
 	return nil
 }
 
+// RemoveMessageFromBox deletes a message on the server
+func (api *API) RemoveMessageFromBox(addr hash.Hash, messageID string, boxID int) error {
+	type inputRemoveMessage struct {
+		From int `json:"from"`
+	}
+
+	input := &inputRemoveMessage{
+		From: boxID,
+	}
+
+	url := fmt.Sprintf("/account/%s/message/%s/delete", addr.String(), messageID)
+	resp, statusCode, err := api.PostJSON(url, input)
+	if err != nil {
+		logrus.Trace(err)
+		return err
+	}
+
+	if statusCode < 200 || statusCode > 299 {
+		return GetErrorFromResponse(resp)
+	}
+
+	return nil
+}
+
+// CopyMessage copies a message to a mailbox
+func (api *API) CopyMessage(addr hash.Hash, messageID string, to int) error {
+	type inputCopyMessage struct {
+		To int `json:"to"`
+	}
+
+	input := &inputCopyMessage{
+		To: to,
+	}
+
+	url := fmt.Sprintf("/account/%s/message/%s/copy", addr.String(), messageID)
+	resp, statusCode, err := api.PostJSON(url, input)
+	if err != nil {
+		logrus.Trace(err)
+		return err
+	}
+
+	if statusCode < 200 || statusCode > 299 {
+		return GetErrorFromResponse(resp)
+	}
+
+	return nil
+}
+
+// MoveMessage moves a message frome one mailbox to another
+func (api *API) MoveMessage(addr hash.Hash, messageID string, from, to int) error {
+	type inputMoveMessage struct {
+		From int `json:"from"`
+		To   int `json:"to"`
+	}
+
+	input := &inputMoveMessage{
+		From: from,
+		To:   to,
+	}
+
+	url := fmt.Sprintf("/account/%s/message/%s/move", addr.String(), messageID)
+	resp, statusCode, err := api.PostJSON(url, input)
+	if err != nil {
+		logrus.Trace(err)
+		return err
+	}
+
+	if statusCode < 200 || statusCode > 299 {
+		return GetErrorFromResponse(resp)
+	}
+
+	return nil
+}
+
 // GetMessage retrieves a message header + catalog from a message
 func (api *API) GetMessage(addr hash.Hash, messageID string) (*Message, error) {
 	in := &Message{}
diff --git a/internal/config/config.go b/internal/config/config.go
index 6e630f4..1170d5e 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -36,6 +36,7 @@ var (
 	errConfigNotFound       = errors.New("cannot find config file")
 	errClientConfigNotFound = errors.New("client config file not found")
 	errServerConfigNotFound = errors.New("server config file not found")
+	errBridgeConfigNotFound = errors.New("bridge config file not found")
 )
 
 var triedPaths []string
@@ -45,12 +46,15 @@ var (
 	ClientConfigFile string = "bitmaelum-client-config.yml"
 	// ServerConfigFile Filename of server configuration
 	ServerConfigFile string = "bitmaelum-server-config.yml"
+	// BridgeConfigFile Filename of bridge configuration
+	BridgeConfigFile string = "bitmaelum-bridge-config.yml"
 )
 
 // Absolute paths of the loaded configurations
 var (
 	LoadedClientConfigPath string
 	LoadedServerConfigPath string
+	LoadedBridgeConfigPath string
 )
 
 // LoadClientConfig loads client configuration from given path or panic if cannot load
@@ -76,6 +80,17 @@ func LoadServerConfig(configPath string) {
 	}
 }
 
+// LoadBridgeConfig loads bridge configuration from given path or panic if cannot load
+func LoadBridgeConfig(configPath string) {
+	err := LoadBridgeConfigOrPass(configPath)
+	if err != nil {
+		for _, p := range triedPaths {
+			logrus.Errorf("Tried path: %s", p)
+		}
+		logrus.Fatalf("could not load bridge configuration. You can generate a new configuration with: 'bm-config init-config --bridge'")
+	}
+}
+
 // LoadClientConfigOrPass loads client configuration, but return false if not able
 func LoadClientConfigOrPass(configPath string) error {
 	var err error
@@ -133,9 +148,36 @@ func LoadServerConfigOrPass(configPath string) error {
 	return errServerConfigNotFound
 }
 
+// LoadBridgeConfigOrPass loads bridge configuration, but return false if not able
+func LoadBridgeConfigOrPass(configPath string) error {
+	var err error
+
+	// Try custom path first
+	if configPath != "" {
+		return readConfigPath(configPath, "from commandline", Bridge.LoadConfig, &LoadedBridgeConfigPath)
+	}
+
+	configPath = os.Getenv("BITMAELUM_BRIDGE_CONFIG")
+	if configPath != "" {
+		return readConfigPath(configPath, "from BITMAELUM_BRIDGE_CONFIG environment", Bridge.LoadConfig, &LoadedBridgeConfigPath)
+	}
+
+	// try on our search paths
+	for _, p := range getSearchPaths() {
+		p = filepath.Join(p, BridgeConfigFile)
+		err = readConfigPath(p, "from hardcoded search path", Bridge.LoadConfig, &LoadedBridgeConfigPath)
+		if err == nil || err != errConfigNotFound {
+			return err
+		}
+	}
+
+	return errBridgeConfigNotFound
+}
+
 // Expands the given path and loads the configuration
 func readConfigPath(p, src string, loader func(r io.Reader) error, loadedPath *string) error {
 	p, _ = homedir.Expand(p)
+	p, _ = filepath.Abs(p)
 
 	triedPaths = append(triedPaths, p+" ("+src+")")
 
diff --git a/internal/config/config_bridge.go b/internal/config/config_bridge.go
new file mode 100644
index 0000000..ba73738
--- /dev/null
+++ b/internal/config/config_bridge.go
@@ -0,0 +1,101 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package config
+
+import (
+	"io"
+	"io/ioutil"
+
+	"github.com/mitchellh/go-homedir"
+	"gopkg.in/yaml.v2"
+)
+
+// Bridge keeps all client configuration settings
+var Bridge BridgeConfig = BridgeConfig{}
+
+// Basically, our config is inside the "config" section. So we load the whole file and only store the Cfg section
+type wrappedBridgeConfig struct {
+	Cfg BridgeConfig `yaml:"config"`
+}
+
+// BridgeConfig is the representation of the bridge configuration
+type BridgeConfig struct {
+	Vault struct {
+		Path string `yaml:"path"`
+	} `yaml:"vault"`
+
+	Server struct {
+		SMTP struct {
+			Enabled        bool   `yaml:"enabled"`
+			Host           string `yaml:"host"`
+			Port           int    `yaml:"port"`
+			Gateway        bool   `yaml:"gateway"`
+			Domain         string `yaml:"domain"`
+			Organization   string `yaml:"organization"`
+			GatewayAccount string `yaml:"gateway_account"`
+			Debug          bool   `yaml:"debug"`
+		} `yaml:"smtp"`
+
+		IMAP struct {
+			Enabled bool   `yaml:"enabled"`
+			Host    string `yaml:"host"`
+			Port    int    `yaml:"port"`
+			Path    string `yaml:"path"`
+			Debug   bool   `yaml:"debug"`
+		} `yaml:"imap"`
+	} `yaml:"server"`
+
+	Resolver struct {
+		Sqlite struct {
+			Enabled bool   `yaml:"enabled"`
+			Dsn     string `yaml:"dsn"`
+		} `yaml:"sqlite"`
+
+		Remote struct {
+			Enabled       bool   `yaml:"enabled"`
+			URL           string `yaml:"url"`
+			AllowInsecure bool   `yaml:"allow_insecure"`
+		} `yaml:"remote"`
+	}
+}
+
+// LoadConfig loads the client configuration from the given path
+func (c *BridgeConfig) LoadConfig(r io.Reader) error {
+	data, err := ioutil.ReadAll(r)
+	if err != nil {
+		return err
+	}
+
+	var lc wrappedBridgeConfig = wrappedBridgeConfig{}
+	err = yaml.Unmarshal(data, &lc)
+	if err != nil {
+		return err
+	}
+
+	// We only care about the Cfg section. This keeps our "config:" section in the yaml file but we can still use
+	// config.Client.Logger.Level instead of config.Client.Cfg.Logger.Level
+	*c = lc.Cfg
+
+	// Expand homedirs in configuration
+	c.Vault.Path, _ = homedir.Expand(c.Vault.Path)
+	c.Server.IMAP.Path, _ = homedir.Expand(c.Server.IMAP.Path)
+
+	return nil
+}
diff --git a/internal/config/config_templates.go b/internal/config/config_templates.go
index f4878a8..21863d9 100644
--- a/internal/config/config_templates.go
+++ b/internal/config/config_templates.go
@@ -46,6 +46,78 @@ config:
 
 `
 
+const bridgeConfigTemplate string = `# BitMaelum Bridge Configuration Template. Edit for your own needs.
+config:
+    vault:
+        # where are our accounts stored?
+        path: "~/.bitmaelum/accounts.vault.json"
+
+    server:
+        smtp: 
+            # Enable SMTP server to allow outgoing messages
+            enabled: true
+
+            # Host and port to listen for incoming connections
+            host: "localhost"
+            port: 1025
+
+            # Run the SMTP server in gateway mode for the specified domain. Gateway mode is used to
+            # translate regular emails to BitMaelum addresses. The BitMaelum team is running a SMTP 
+            # in gateway mode on the domain "bitmaelum.network" and using "mailgateway!" as the gateway
+            # account. But if you want to use your own domain you can run this bridge in gateway mode,
+            # set your own domain name and gateway account, and set the appropriate DNS pointing to this
+            # SMTP server to allow for incoming mail. When a message is received to "something@yourdomain.com"
+            # the bridge will send the message to the BitMaelum account "something!". However if you also
+            # set the organization variable then the mails received to "something@yourdomain.com" will be
+            # delivered to the address "something@yourorganization!" making the bridge a fully replacement
+            # for your mail system.
+            gateway: false
+            domain: ""
+            organization: ""
+
+            # Account from the vault to use to process incoming or outgoing mail. This is the account
+            # that will relay the messages from BitMaelum network to regular email. So, when using the
+            # gateway mode, you need to have this account on your vault.
+            # The default gateway_account is "mailgateway!" that will translate BitMaelum addresses to
+            # email addresses like this "account!" <-> "account@bitmaelum.network".
+            gateway_account: ""
+
+            # Display SMTP communication between client and server
+            debug: false
+
+        imap: 
+            # Enable IMAP4 server to allow outgoing messages
+            enabled: true
+
+            # Host and port to listen for incoming connections
+            host: "localhost"
+            port: 1143
+
+            # Path to store a database that contains message flags (flags.db file). This
+            # file should persists across reboots
+            path: "~/.bitmaelum/bm-bridge"
+
+            # Display IMAP communication between client and server
+            debug: false
+            
+    resolver:
+        # SQLite local resolver cache 
+        sqlite:
+            # Enable sqlite resolving
+            enabled: false
+            # Note: DSN currently does not support ~ homedir expansion
+            dsn: "file:/tmp/keyresolve.db"
+        # Remove resolver
+        remote:
+            # Enable remote resolving
+            enabled: true
+            # URL to the remote resolver
+            url: "https://resolver.bitmaelum.com"
+            # Allow insecure connections (to selfsigned certs)
+            allow_insecure: false
+    
+`
+
 const serverConfigTemplate string = `# BitMaelum Server Configuration Template. Edit for your own needs.
 config:
     # Logging of information
@@ -188,3 +260,10 @@ func GenerateServerConfig(w io.Writer) error {
 
 	return err
 }
+
+// GenerateBridgeConfig Generates a default bridge configuration
+func GenerateBridgeConfig(w io.Writer) error {
+	_, err := w.Write([]byte(bridgeConfigTemplate))
+
+	return err
+}
diff --git a/internal/container/resolver.go b/internal/container/resolver.go
index c1f95df..ff881fc 100644
--- a/internal/container/resolver.go
+++ b/internal/container/resolver.go
@@ -51,6 +51,9 @@ func setupResolverService() (interface{}, error) {
 		if config.Server.Resolver.Remote.Enabled {
 			_ = repo.Add(*getRemoteRepository(config.Server.Resolver.Remote.URL, false, config.Server.Resolver.Remote.AllowInsecure))
 		}
+		if config.Bridge.Resolver.Remote.Enabled {
+			_ = repo.Add(*getRemoteRepository(config.Bridge.Resolver.Remote.URL, false, config.Bridge.Resolver.Remote.AllowInsecure))
+		}
 
 		resolveService = resolver.KeyRetrievalService(repo)
 	})
diff --git a/internal/resolver/service.go b/internal/resolver/service.go
index 03012cf..8359b44 100644
--- a/internal/resolver/service.go
+++ b/internal/resolver/service.go
@@ -37,6 +37,7 @@ import (
 type Service struct {
 	repo         Repository
 	routingCache *lru.Cache
+	addressCache *lru.Cache
 }
 
 // AddressInfo is a structure returned by the external resolver system
@@ -70,19 +71,43 @@ func KeyRetrievalService(repo Repository) *Service {
 		c = nil
 	}
 
+	a, err := lru.New(64)
+	if err != nil {
+		a = nil
+	}
+
 	return &Service{
 		repo:         repo,
 		routingCache: c,
+		addressCache: a,
 	}
 }
 
 // ResolveAddress resolves an address.
 func (s *Service) ResolveAddress(addr hash.Hash) (*AddressInfo, error) {
-	logrus.Debugf("Resolving address %s", addr.String())
-	info, err := s.repo.ResolveAddress(addr)
-	if err != nil {
-		logrus.Debugf("Error while resolving address %s: %s", addr.String(), err)
-		return nil, err
+	var info *AddressInfo
+	// Fetch from cache if available
+	if s.addressCache != nil {
+		res, ok := s.addressCache.Get(addr.String())
+		if ok {
+			info = res.(*AddressInfo)
+			logrus.Debugf("Resolving cached %s", addr.String())
+		}
+	}
+
+	if info == nil {
+		logrus.Debugf("Resolving address %s", addr.String())
+		var err error
+		info, err = s.repo.ResolveAddress(addr)
+		if err != nil {
+			logrus.Debugf("Error while resolving address %s: %s", addr.String(), err)
+			return nil, err
+		}
+
+		// Store in cache if available
+		if s.addressCache != nil {
+			_ = s.addressCache.Add(addr.String(), info)
+		}
 	}
 
 	// Resolve routing info, we often need it
diff --git a/internal/service.go b/internal/service.go
new file mode 100644
index 0000000..bf5d369
--- /dev/null
+++ b/internal/service.go
@@ -0,0 +1,159 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// 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.
+
+package internal
+
+import (
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"runtime"
+
+	"github.com/bitmaelum/bitmaelum-suite/internal/config"
+	"github.com/kardianos/service"
+	"github.com/sirupsen/logrus"
+)
+
+const (
+	windowsOS = "windows"
+	linuxOS   = "linux"
+	macOS     = "darwin"
+)
+
+type options struct {
+	ImapHost       string `long:"imaphost" description:"Host:Port to imap server from" required:"false"`
+	SMTPHost       string `long:"smtphost" description:"Host:Port to smtp server from" required:"false"`
+	Config         string `short:"c" long:"config" description:"Path to your configuration file"`
+	Password       string `short:"p" long:"password" description:"Vault password" default:""`
+	UserName       string `long:"username" description:"Username to run the service as" default:""`
+	GatewayAccount string `long:"gatewayaccount" description:"Account to use to check for pending outgoing mails" required:"false"`
+}
+
+var opts options
+
+// GetBMServerService will return the service info
+func GetBMServerService(executable string) *service.Config {
+	ParseOptions(&opts)
+
+	var arguments []string
+	arguments = append(arguments, "--service")
+
+	config.LoadServerConfig(opts.Config)
+	arguments = append(arguments, "--config="+config.LoadedServerConfigPath)
+
+	svcConfig := getServiceConfig("BM-Server", "BitMaelum server", "BitMaelum server service", executable, arguments)
+
+	if svcConfig == nil {
+		return nil
+	}
+
+	if opts.UserName != "" {
+		svcConfig.UserName = opts.UserName
+	}
+
+	return svcConfig
+}
+
+// GetBMBridgeService will return the service info
+func GetBMBridgeService(executable string) *service.Config {
+	var arguments []string
+
+	if executable != "" {
+		// install mode
+		ParseOptions(&opts)
+
+		arguments = append(arguments, "--service")
+
+		if opts.Password != "" {
+			arguments = append(arguments, "--password="+opts.Password)
+		}
+
+		config.LoadBridgeConfig(opts.Config)
+		arguments = append(arguments, "--config="+config.LoadedBridgeConfigPath)
+	}
+
+	// Get current user
+	user, err := user.Current()
+	if err != nil {
+		return nil
+	}
+
+	svcConfig := getServiceConfig("BM-Bridge", "BitMaelum email bridge", "BitMaelum email bridge service", executable, arguments)
+
+	if svcConfig == nil {
+		return nil
+	}
+
+	if opts.UserName != "" {
+		svcConfig.UserName = opts.UserName
+	} else {
+		svcConfig.UserName = user.Username
+	}
+
+	return svcConfig
+}
+
+func getServiceConfig(name, displayName, description, executable string, arguments []string) *service.Config {
+	options := make(service.KeyValue)
+	options["Restart"] = "on-success"
+	options["SuccessExitStatus"] = "1 2 8 SIGKILL"
+
+	svcConfig := &service.Config{
+		Name:        name,
+		DisplayName: displayName,
+		Description: description,
+		Arguments:   arguments,
+		Option:      options,
+	}
+
+	switch runtime.GOOS {
+	case linuxOS:
+		svcConfig.Dependencies = []string{
+			"Requires=network.target",
+			"After=network-online.target syslog.target"}
+
+	case windowsOS:
+		svcConfig.Dependencies = []string{"Tcpip"}
+	}
+
+	if executable != "" {
+		if runtime.GOOS == windowsOS {
+			executable = executable + ".exe"
+		}
+		svcConfig.Executable = executable
+
+		if _, err := os.Stat(svcConfig.Executable); os.IsNotExist(err) {
+			// file not found, try on path
+			svcConfig.Executable, err = exec.LookPath(svcConfig.Executable)
+			if err != nil {
+				// file not found, try on the same path where we are
+				dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
+				svcConfig.Executable = filepath.Join(dir, executable)
+				if _, err := os.Stat(svcConfig.Executable); os.IsNotExist(err) {
+					logrus.Fatalf("Please run this command on the same path where \"%s\" is located or add it to your path", executable)
+					return nil
+				}
+			}
+		}
+
+	}
+
+	return svcConfig
+}


Test patch:
diff --git a/internal/config/config_templates_test.go b/internal/config/config_templates_test.go
index 718a981..8ba878b 100644
--- a/internal/config/config_templates_test.go
+++ b/internal/config/config_templates_test.go
@@ -45,4 +45,14 @@ func TestTemplates(t *testing.T) {
 	err = Client.LoadConfig(&buf)
 	assert.NoError(t, err)
 	assert.Equal(t, "https://resolver.bitmaelum.com", Client.Resolver.Remote.URL)
+
+	err = GenerateBridgeConfig(&buf)
+	assert.NoError(t, err)
+	assert.NotEmpty(t, buf.String())
+
+	assert.Empty(t, Bridge.Server.SMTP.Host)
+	err = Bridge.LoadConfig(&buf)
+	assert.NoError(t, err)
+	assert.Equal(t, "localhost", Bridge.Server.SMTP.Host)
+
 }