Initial prototype

Change-Id: I60a94e90aab48dfcf7c1f03fe5613d1db7d0df95
diff --git a/cmd/hichipbridge/.gitignore b/cmd/hichipbridge/.gitignore
new file mode 100644
index 0000000..3adf02a
--- /dev/null
+++ b/cmd/hichipbridge/.gitignore
@@ -0,0 +1,2 @@
+hichipbridge
+config.json
diff --git a/cmd/hichipbridge/Dockerfile b/cmd/hichipbridge/Dockerfile
new file mode 100644
index 0000000..f303928
--- /dev/null
+++ b/cmd/hichipbridge/Dockerfile
@@ -0,0 +1,35 @@
+FROM golang:latest as builder
+
+ENV CGO_ENABLED=0
+
+ENV GO111MODULE=on
+ENV GOPROXY=https://proxy.golang.org
+
+RUN mkdir /gocache
+ENV GOCACHE /gocache
+
+COPY go.mod /go/src/gomodules.avm99963.com/hichip2mqtt/go.mod
+COPY go.sum /go/src/gomodules.avm99963.com/hichip2mqtt/go.sum
+
+WORKDIR /go/src/gomodules.avm99963.com/hichip2mqtt
+
+# Optimization for iterative docker build speed, not necessary for correctness:
+RUN go install github.com/eclipse/paho.mqtt.golang
+RUN go install github.com/flashmob/go-guerrilla
+RUN go install github.com/go-sql-driver/mysql
+RUN go install github.com/sirupsen/logrus
+RUN go install github.com/spf13/cobra
+RUN go install gomodules.avm99963.com/forks/parsemail
+
+WORKDIR /go/src/gomodules.avm99963.com/hichip2mqtt/cmd/hichipbridge
+
+COPY . /go/src/gomodules.avm99963.com/hichip2mqtt/cmd/hichipbridge
+
+RUN go install gomodules.avm99963.com/hichip2mqtt/cmd/hichipbridge
+
+FROM alpine
+LABEL maintainer "me@avm99963.com"
+
+RUN apk add --no-cache tini
+COPY --from=builder /go/bin/hichipbridge /
+ENTRYPOINT ["/sbin/tini", "--", "/hichipbridge", "serve"]
diff --git a/cmd/hichipbridge/Makefile b/cmd/hichipbridge/Makefile
new file mode 100644
index 0000000..f3c5687
--- /dev/null
+++ b/cmd/hichipbridge/Makefile
@@ -0,0 +1,11 @@
+MUTABLE_VERSION ?= latest
+VERSION ?= $(shell git rev-parse --short HEAD)
+
+IMAGE_PROD := hichipbridge
+
+docker-prod: Dockerfile
+	docker build --force-rm -f Dockerfile --tag=$(IMAGE_PROD):$(VERSION) ../../
+	docker tag $(IMAGE_PROD):$(VERSION) $(IMAGE_PROD):$(MUTABLE_VERSION)
+
+push-prod: docker-prod
+	docker push $(IMAGE_PROD):$(VERSION)
diff --git a/cmd/hichipbridge/config.sample.json b/cmd/hichipbridge/config.sample.json
new file mode 100644
index 0000000..a0ad7d7
--- /dev/null
+++ b/cmd/hichipbridge/config.sample.json
@@ -0,0 +1,14 @@
+{
+    "log_file": "stderr",
+    "log_level": "debug",
+    "backend_config": {
+        "log_received_mails": false
+    },
+    "servers": [{
+        "is_enabled": true,
+        "max_size": 1000000,
+        "listen_interface": "0.0.0.0:25",
+        "timeout": 30,
+        "max_clients": 3
+    }]
+}
diff --git a/cmd/hichipbridge/docker-compose.yml b/cmd/hichipbridge/docker-compose.yml
new file mode 100644
index 0000000..92e8913
--- /dev/null
+++ b/cmd/hichipbridge/docker-compose.yml
@@ -0,0 +1,10 @@
+version: "3.9"
+services:
+  bridge:
+    image: "hichipbridge:latest"
+    ports:
+      - "8881:25"
+    env_file: .env
+    volumes:
+      - ./config.json:/config.json:ro
+    restart: unless-stopped
diff --git a/cmd/hichipbridge/env.sample b/cmd/hichipbridge/env.sample
new file mode 100644
index 0000000..4e0f25b
--- /dev/null
+++ b/cmd/hichipbridge/env.sample
@@ -0,0 +1,14 @@
+# Host for the SMTP server where you're going to receive email notifications
+HICHIPBRIDGE_SMTP_HOST="smtp-server.example.com"
+# MQTT broker endpoint
+HICHIPBRIDGE_MQTT_BROKER="mqtt://mqtt-broker.example.com:1883"
+# MQTT client id
+HICHIPBRIDGE_MQTT_CLIENTID="hichipbridge"
+# MQTT username
+HICHIPBRIDGE_MQTT_USERNAME="user"
+# MQTT password
+HICHIPBRIDGE_MQTT_PASSWORD="password"
+# Prefix for the MQTT topics where events will be saved
+HICHIPBRIDGE_MQTT_TOPIC="hichipbridge"
+# Token for the email notifications
+HICHIPBRIDGE_EMAIL_TOKEN="superconfidentialtoken;)"
diff --git a/cmd/hichipbridge/main.go b/cmd/hichipbridge/main.go
new file mode 100644
index 0000000..aa12bc4
--- /dev/null
+++ b/cmd/hichipbridge/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+func main() {
+	if err := rootCmd.Execute(); err != nil {
+		fmt.Println(err)
+		os.Exit(-1)
+	}
+}
diff --git a/cmd/hichipbridge/root.go b/cmd/hichipbridge/root.go
new file mode 100644
index 0000000..42eebdb
--- /dev/null
+++ b/cmd/hichipbridge/root.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+)
+
+var rootCmd = &cobra.Command{
+	Use:   "hichipbridge",
+	Short: "hichip2mqtt bridge daemon",
+	Long: `Daemon which serves as a bridge for hichip IP cameras and MQTT.
+Basically, it converts motion trigger emails notifications received into a state
+published in a MQTT topic.`,
+	Run: nil,
+}
+
+var (
+	verbose bool
+)
+
+func init() {
+	cobra.OnInitialize()
+	rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false,
+		"print out more debug information")
+	rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
+		if verbose {
+			logrus.SetLevel(logrus.DebugLevel)
+		} else {
+			logrus.SetLevel(logrus.InfoLevel)
+		}
+	}
+}
diff --git a/cmd/hichipbridge/serve.go b/cmd/hichipbridge/serve.go
new file mode 100644
index 0000000..b16de0a
--- /dev/null
+++ b/cmd/hichipbridge/serve.go
@@ -0,0 +1,205 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"github.com/flashmob/go-guerrilla"
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
+
+	// Choose iconv or mail/encoding package which uses golang.org/x/net/html/charset
+	//_ "github.com/flashmob/go-guerrilla/mail/iconv"
+	_ "github.com/flashmob/go-guerrilla/mail/encoding"
+
+	"github.com/spf13/cobra"
+
+	_ "github.com/go-sql-driver/mysql"
+
+	"gomodules.avm99963.com/hichip2mqtt/mqtt_processor"
+)
+
+const (
+	defaultPidFile = "/var/run/hichipbridge.pid"
+)
+
+var (
+	configPath string
+	pidFile    string
+
+	serveCmd = &cobra.Command{
+		Use:   "serve",
+		Short: "start the daemon and start all available SMTP servers",
+		Run:   serve,
+	}
+
+	signalChannel = make(chan os.Signal, 1) // for trapping SIGHUP and friends
+	mainlog       log.Logger
+
+	d guerrilla.Daemon
+)
+
+func init() {
+	// log to stderr on startup
+	var err error
+	mainlog, err = log.GetLogger(log.OutputStderr.String(), log.InfoLevel.String())
+	if err != nil && mainlog != nil {
+		mainlog.WithError(err).Errorf("Failed creating a logger to %s", log.OutputStderr)
+	}
+	cfgFile := "config.json"
+	serveCmd.PersistentFlags().StringVarP(&configPath, "config", "c",
+		cfgFile, "Path to the configuration file")
+	// intentionally didn't specify default pidFile; value from config is used if flag is empty
+	serveCmd.PersistentFlags().StringVarP(&pidFile, "pidFile", "p",
+		"", "Path to the pid file")
+	rootCmd.AddCommand(serveCmd)
+}
+
+func sigHandler() {
+	signal.Notify(signalChannel,
+		syscall.SIGHUP,
+		syscall.SIGTERM,
+		syscall.SIGQUIT,
+		syscall.SIGINT,
+		syscall.SIGKILL,
+		syscall.SIGUSR1,
+		os.Kill,
+	)
+	for sig := range signalChannel {
+		if sig == syscall.SIGHUP {
+			if ac, err := readConfig(configPath, pidFile); err == nil {
+				_ = d.ReloadConfig(*ac)
+			} else {
+				mainlog.WithError(err).Error("Could not reload config")
+			}
+		} else if sig == syscall.SIGUSR1 {
+			if err := d.ReopenLogs(); err != nil {
+				mainlog.WithError(err).Error("reopening logs failed")
+			}
+		} else if sig == syscall.SIGTERM || sig == syscall.SIGQUIT || sig == syscall.SIGINT || sig == os.Kill {
+			mainlog.Infof("Shutdown signal caught")
+			go func() {
+				select {
+				// exit if graceful shutdown not finished in 60 sec.
+				case <-time.After(time.Second * 60):
+					mainlog.Error("graceful shutdown timed out")
+					os.Exit(1)
+				}
+			}()
+			d.Shutdown()
+			mainlog.Infof("Shutdown completed, exiting.")
+			return
+		} else {
+			mainlog.Infof("Shutdown, unknown signal caught")
+			return
+		}
+	}
+}
+
+func serve(cmd *cobra.Command, args []string) {
+	logVersion()
+	d = guerrilla.Daemon{Logger: mainlog}
+	d.AddProcessor("MQTT", mqtt_processor.Processor)
+
+	c, err := readConfig(configPath, pidFile)
+	if err != nil {
+		mainlog.WithError(err).Fatal("Error while reading config")
+	}
+	_ = d.SetConfig(*c)
+
+	// Check that max clients is not greater than system open file limit.
+	if ok, maxClients, fileLimit := guerrilla.CheckFileLimit(c); !ok {
+		mainlog.Fatalf("Combined max clients for all servers (%d) is greater than open file limit (%d). "+
+			"Please increase your open file limit or decrease max clients.", maxClients, fileLimit)
+	}
+
+	err = d.Start()
+	if err != nil {
+		mainlog.WithError(err).Error("Error(s) when creating new server(s)")
+		os.Exit(1)
+	}
+	sigHandler()
+
+}
+
+func lookupPrefixedEnv(coreName string) (string, error) {
+	name := "HICHIPBRIDGE_" + coreName
+	v, ok := os.LookupEnv(name)
+	if !ok {
+		return v, fmt.Errorf("Environment variable %s isn't set", name)
+	}
+
+	return v, nil
+}
+
+// ReadConfig is called at startup, or when a SIG_HUP is caught
+func readConfig(path string, pidFile string) (*guerrilla.AppConfig, error) {
+	// Environment variables where some settings are configured (without the prefix "HICHIPBRIDGE_"):
+	envNames := []string{
+		"SMTP_HOST",
+		"MQTT_BROKER",
+		"MQTT_CLIENTID",
+		"MQTT_USERNAME",
+		"MQTT_PASSWORD",
+		"MQTT_TOPIC",
+		"EMAIL_TOKEN",
+	}
+
+	// Set default configuration (overridable)
+	defaultAppConfig := guerrilla.AppConfig{
+		BackendConfig: backends.BackendConfig{
+			"save_workers_size":  1,
+			"log_received_mails": false,
+		},
+	}
+
+	d.SetConfig(defaultAppConfig)
+
+	// Load in the config.
+	appConfig, err := d.LoadConfig(path)
+	if err != nil {
+		return &appConfig, fmt.Errorf("could not read config file: %s", err.Error())
+	}
+
+	// override config pidFile with with flag from the command line
+	if len(pidFile) > 0 {
+		appConfig.PidFile = pidFile
+	} else if len(appConfig.PidFile) == 0 {
+		appConfig.PidFile = defaultPidFile
+	}
+	if verbose {
+		appConfig.LogLevel = "debug"
+	}
+
+	// Override compulsory settings
+	var envs = make(map[string]string)
+	for _, env := range envNames {
+		v, err := lookupPrefixedEnv(env)
+		if err != nil {
+			return &appConfig, err
+		}
+		envs[env] = v
+	}
+
+	appConfig.AllowedHosts = []string{envs["SMTP_HOST"]}
+	appConfig.BackendConfig = backends.BackendConfig{
+		"save_process":      "HeadersParser|Hasher|Debugger|MQTT",
+		"validate_process":  "",
+		"primary_mail_host": envs["SMTP_HOST"],
+		"mqtt_broker":       envs["MQTT_BROKER"],
+		"mqtt_clientid":     envs["MQTT_CLIENTID"],
+		"mqtt_username":     envs["MQTT_USERNAME"],
+		"mqtt_password":     envs["MQTT_PASSWORD"],
+		"mqtt_topic":        envs["MQTT_TOPIC"],
+		"mqtt_email_token":  envs["EMAIL_TOKEN"],
+	}
+
+	for i, _ := range appConfig.Servers {
+		appConfig.Servers[i].Hostname = envs["SMTP_HOST"]
+	}
+
+	return &appConfig, nil
+}
diff --git a/cmd/hichipbridge/version.go b/cmd/hichipbridge/version.go
new file mode 100644
index 0000000..269a5fe
--- /dev/null
+++ b/cmd/hichipbridge/version.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/flashmob/go-guerrilla"
+)
+
+var versionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "Print the version info",
+	Long:  `Every software has a version. This is Guerrilla's`,
+	Run: func(cmd *cobra.Command, args []string) {
+		logVersion()
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(versionCmd)
+}
+
+func logVersion() {
+	mainlog.Infof("Using guerrilla %s", guerrilla.Version)
+	mainlog.Debugf("Build Time: %s", guerrilla.BuildTime)
+	mainlog.Debugf("Commit:     %s", guerrilla.Commit)
+}