Browse Source

v1.3 - see Readme for changes

flashmob 9 years ago
parent
commit
f3a9a199ee
2 changed files with 78 additions and 54 deletions
  1. 10 1
      README.md
  2. 68 53
      goguerrilla.go

+ 10 - 1
README.md

@@ -67,7 +67,7 @@ To build, you will need to install the following Go libs:
 	$ go get github.com/ziutek/mymysql/thrsafe
 	$ go get github.com/ziutek/mymysql/thrsafe
 	$ go get github.com/ziutek/mymysql/autorc
 	$ go get github.com/ziutek/mymysql/autorc
 	$ go get github.com/ziutek/mymysql/godrv
 	$ go get github.com/ziutek/mymysql/godrv
-	$ go get github.com/sloonz/go-iconv
+	$ go get gopkg.in/iconv.v1
 	$ go get github.com/garyburd/redigo/redis
 	$ go get github.com/garyburd/redigo/redis
 
 
 Rename goguerrilla.conf.sample to goguerrilla.conf
 Rename goguerrilla.conf.sample to goguerrilla.conf
@@ -131,6 +131,15 @@ Copy goguerrilla.conf.sample to goguerrilla.conf
 Releases
 Releases
 =========================================================
 =========================================================
 
 
+1.3
+- Number of saveMail workers added to config (GM_SAVE_WORKERS) 
+- convenience function for reading int values form config
+- advertise PIPELINING
+- added HELP command
+- rcpt to host validation: now case insensitive and done earlier (after DATA)
+- iconv switched to: go get gopkg.in/iconv.v1
+
+1.2
 - Reload config on SIGHUP
 - Reload config on SIGHUP
 - Write current process id (pid) to a file, /var/run/go-guerrilla.pid by default
 - Write current process id (pid) to a file, /var/run/go-guerrilla.pid by default
 
 

+ 68 - 53
goguerrilla.go

@@ -1,7 +1,7 @@
 /** 
 /** 
 Go-Guerrilla SMTPd
 Go-Guerrilla SMTPd
 
 
-Version: 1.2
+Version: 1.3
 Author: Flashmob, GuerrillaMail.com
 Author: Flashmob, GuerrillaMail.com
 Contact: [email protected]
 Contact: [email protected]
 License: MIT
 License: MIT
@@ -29,12 +29,12 @@ import (
 	"flag"
 	"flag"
 	"fmt"
 	"fmt"
 	"github.com/garyburd/redigo/redis"
 	"github.com/garyburd/redigo/redis"
-	"github.com/sloonz/go-iconv"
 	"github.com/sloonz/go-qprintable"
 	"github.com/sloonz/go-qprintable"
 	"github.com/ziutek/mymysql/autorc"
 	"github.com/ziutek/mymysql/autorc"
 	_ "github.com/ziutek/mymysql/godrv"
 	_ "github.com/ziutek/mymysql/godrv"
 	"io"
 	"io"
 	"io/ioutil"
 	"io/ioutil"
+	"gopkg.in/iconv.v1"
 	"log"
 	"log"
 	"net"
 	"net"
 	"net/http"
 	"net/http"
@@ -99,6 +99,15 @@ var gConfig = map[string]string{
 	"NGINX_AUTH_ENABLED":     "N",              // Y or N
 	"NGINX_AUTH_ENABLED":     "N",              // Y or N
 	"NGINX_AUTH":             "127.0.0.1:8025", // If using Nginx proxy, ip and port to serve Auth requsts
 	"NGINX_AUTH":             "127.0.0.1:8025", // If using Nginx proxy, ip and port to serve Auth requsts
 	"PID_FILE":		  "/var/run/go-guerrilla.pid",
 	"PID_FILE":		  "/var/run/go-guerrilla.pid",
+	"GSMTP_MAIL_EXPIRE_SECONDS" : "72000",
+}
+
+func getConfigInt(key string, defaultVal int) int {
+	ret := defaultVal
+	if n, n_err := strconv.Atoi(gConfig[key]); n_err == nil {
+		ret = n
+	}
+	return ret
 }
 }
 
 
 type redisClient struct {
 type redisClient struct {
@@ -112,12 +121,15 @@ func logln(level int, s string) {
 	if gConfig["GSMTP_VERBOSE"] == "Y" {
 	if gConfig["GSMTP_VERBOSE"] == "Y" {
 		fmt.Println(s)
 		fmt.Println(s)
 	}
 	}
+	// fatal errors
 	if level == 2 {
 	if level == 2 {
 		log.Fatalf(s)
 		log.Fatalf(s)
 	}
 	}
-	if len(gConfig["GSMTP_LOG_FILE"]) > 0 {
+	// warnings
+	if level == 1 && len(gConfig["GSMTP_LOG_FILE"]) > 0 {
 		log.Println(s)
 		log.Println(s)
 	}
 	}
+
 }
 }
 
 
 func readConfig() {
 func readConfig() {
@@ -126,10 +138,9 @@ func readConfig() {
 	if !flag.Parsed() {
 	if !flag.Parsed() {
 		flag.StringVar(&flagConfigFile, "config", "goguerrilla.conf", "Path to the configuration file")
 		flag.StringVar(&flagConfigFile, "config", "goguerrilla.conf", "Path to the configuration file")
 		flag.StringVar(&flagVerbouse, "v", "n", "Verbose, [y | n] ")
 		flag.StringVar(&flagVerbouse, "v", "n", "Verbose, [y | n] ")
-		flag.StringVar(&flagIface, "if", "127.0.0.1:2525", "Interface and port to listen on, eg. 127.0.0.1:2525 ")
+		flag.StringVar(&flagIface, "if", "", "Interface and port to listen on, eg. 127.0.0.1:2525 ")
 		flag.Parse()
 		flag.Parse()
 	}
 	}
-
 	// load in the config.
 	// load in the config.
 	b, err := ioutil.ReadFile(flagConfigFile)
 	b, err := ioutil.ReadFile(flagConfigFile)
 	if err != nil {
 	if err != nil {
@@ -157,19 +168,8 @@ func readConfig() {
 			allowedHosts[arr[i]] = true
 			allowedHosts[arr[i]] = true
 		}
 		}
 	}
 	}
-	var n int
-	var n_err error
-
-	// timeout for reads
-	if n, n_err = strconv.Atoi(gConfig["GSMTP_TIMEOUT"]); n_err != nil {
-		timeout = time.Duration(10)
-	} else {
-		timeout = time.Duration(n)
-	}
-	// max email size
-	if max_size, n_err = strconv.Atoi(gConfig["GSMTP_MAX_SIZE"]); n_err != nil {
-		max_size = 131072
-	}
+	timeout = time.Duration(getConfigInt("GSMTP_TIMEOUT", 10))
+	max_size = getConfigInt("GSMTP_MAX_SIZE", 131072)
 	return
 	return
 }
 }
 
 
@@ -181,16 +181,10 @@ func sigHandler() {
 }
 }
 
 
 func initialise() {
 func initialise() {
-	var n int
-	var n_err error
-	// sem is an active clients channel used for counting clients
-	if n, n_err = strconv.Atoi(gConfig["GM_MAX_CLIENTS"]); n_err != nil {
-		n = 50
-	}
 	// currently active client list
 	// currently active client list
-	sem = make(chan int, n)
+	sem = make(chan int, getConfigInt("GM_MAX_CLIENTS", 50))
 	// database writing workers
 	// database writing workers
-	SaveMailChan = make(chan *Client, 5)
+	SaveMailChan = make(chan *Client, getConfigInt("GM_SAVE_WORKERS", 3))
 	// custom log file
 	// custom log file
 	if len(gConfig["GSMTP_LOG_FILE"]) > 0 {
 	if len(gConfig["GSMTP_LOG_FILE"]) > 0 {
 		logfile, err := os.OpenFile(gConfig["GSMTP_LOG_FILE"], os.O_WRONLY|os.O_APPEND|os.O_CREATE|os.O_SYNC, 0600)
 		logfile, err := os.OpenFile(gConfig["GSMTP_LOG_FILE"], os.O_WRONLY|os.O_APPEND|os.O_CREATE|os.O_SYNC, 0600)
@@ -206,10 +200,8 @@ func initialise() {
 		}
 		}
 	}
 	}
 	// handle SIGHUP for reloading the configuration while running
 	// handle SIGHUP for reloading the configuration while running
-
 	signal.Notify(signalChannel, syscall.SIGHUP)
 	signal.Notify(signalChannel, syscall.SIGHUP)
 	go sigHandler()
 	go sigHandler()
-
 	return
 	return
 }
 }
 
 
@@ -246,7 +238,7 @@ func main() {
 			logln(1, fmt.Sprintf("Accept error: %s", err))
 			logln(1, fmt.Sprintf("Accept error: %s", err))
 			continue
 			continue
 		}
 		}
-		logln(1, fmt.Sprintf(" There are now "+strconv.Itoa(runtime.NumGoroutine())+" serving goroutines"))
+		logln(0, fmt.Sprintf(" There are now "+strconv.Itoa(runtime.NumGoroutine())+" serving goroutines"))
 		sem <- 1 // Wait for active queue to drain.
 		sem <- 1 // Wait for active queue to drain.
 		go handleClient(&Client{
 		go handleClient(&Client{
 			conn:        conn,
 			conn:        conn,
@@ -275,19 +267,25 @@ func handleClient(client *Client) {
 		case 1:
 		case 1:
 			input, err := readSmtp(client)
 			input, err := readSmtp(client)
 			if err != nil {
 			if err != nil {
-				logln(1, fmt.Sprintf("Read error: %v", err))
 				if err == io.EOF {
 				if err == io.EOF {
 					// client closed the connection already
 					// client closed the connection already
+					logln(0, fmt.Sprintf("%s: %v", client.address, err))
 					return
 					return
 				}
 				}
 				if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
 				if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
 					// too slow, timeout
 					// too slow, timeout
+					logln(0, fmt.Sprintf("%s: %v", client.address, err))
 					return
 					return
 				}
 				}
+				logln(1, fmt.Sprintf("Read error: %v", err))
 				break
 				break
 			}
 			}
 			input = strings.Trim(input, " \n\r")
 			input = strings.Trim(input, " \n\r")
-			cmd := strings.ToUpper(input)
+			bound := len(input);
+			if bound > 16 {
+				bound = 16;
+			}
+			cmd := strings.ToUpper(input[0:bound])
 			switch {
 			switch {
 			case strings.Index(cmd, "HELO") == 0:
 			case strings.Index(cmd, "HELO") == 0:
 				if len(input) > 5 {
 				if len(input) > 5 {
@@ -298,7 +296,13 @@ func handleClient(client *Client) {
 				if len(input) > 5 {
 				if len(input) > 5 {
 					client.helo = input[5:]
 					client.helo = input[5:]
 				}
 				}
-				responseAdd(client, "250-"+gConfig["GSMTP_HOST_NAME"]+" Hello "+client.helo+"["+client.address+"]"+"\r\n"+"250-SIZE "+gConfig["GSMTP_MAX_SIZE"]+"\r\n"+advertiseTls+"250 HELP")
+				responseAdd(client, "250-"+gConfig["GSMTP_HOST_NAME"]+
+				" Hello "+client.helo+"["+client.address+"]"+"\r\n"+
+				"250-SIZE "+gConfig["GSMTP_MAX_SIZE"]+"\r\n"+
+				"250-PIPELINING \r\n" +
+				advertiseTls+"250 HELP")
+			case strings.Index(cmd, "HELP") == 0:
+				responseAdd(client, "250 Help! I need somebody...")
 			case strings.Index(cmd, "MAIL FROM:") == 0:
 			case strings.Index(cmd, "MAIL FROM:") == 0:
 				if len(input) > 10 {
 				if len(input) > 10 {
 					client.mail_from = input[10:]
 					client.mail_from = input[10:]
@@ -344,17 +348,21 @@ func handleClient(client *Client) {
 			var err error
 			var err error
 			client.data, err = readSmtp(client)
 			client.data, err = readSmtp(client)
 			if err == nil {
 			if err == nil {
-				// to do: timeout when adding to SaveMailChan
-				// place on the channel so that one of the save mail workers can pick it up
-				SaveMailChan <- client
-				// wait for the save to complete
-				status := <-client.savedNotify
-
-				if status == 1 {
-					responseAdd(client, "250 OK : queued as "+client.hash)
+				if _, _, mailErr:= validateEmailData(client); mailErr == nil {
+					// to do: timeout when adding to SaveMailChan
+					// place on the channel so that one of the save mail workers can pick it up
+					SaveMailChan <- client
+					// wait for the save to complete
+					status := <-client.savedNotify
+					if status == 1 {
+						responseAdd(client, "250 OK : queued as "+client.hash)
+					} else {
+						responseAdd(client, "554 Error: transaction failed, blame it on the weather")
+					}
 				} else {
 				} else {
-					responseAdd(client, "554 Error: transaction failed, blame it on the weather")
+					responseAdd(client, "550 Error: " + mailErr.Error())
 				}
 				}
+
 			} else {
 			} else {
 				logln(1, fmt.Sprintf("DATA read error: %v", err))
 				logln(1, fmt.Sprintf("DATA read error: %v", err))
 			}
 			}
@@ -478,18 +486,18 @@ func saveMail() {
 	sql += " values (NOW(), ?, ?, ?, ? , 'UTF-8' , ?, 0, ?, '', ?, 0, ?, ?)"
 	sql += " values (NOW(), ?, ?, ?, ? , 'UTF-8' , ?, 0, ?, '', ?, 0, ?, ?)"
 	ins, sql_err := db.Prepare(sql)
 	ins, sql_err := db.Prepare(sql)
 	if sql_err != nil {
 	if sql_err != nil {
-		logln(2, fmt.Sprintf("Sql statement incorrect: %s", sql_err))
+		logln(2, fmt.Sprintf("Sql statement incorrect: %s\n", sql_err))
 	}
 	}
 	sql = "UPDATE gm2_setting SET `setting_value` = `setting_value`+1 WHERE `setting_name`='received_emails' LIMIT 1"
 	sql = "UPDATE gm2_setting SET `setting_value` = `setting_value`+1 WHERE `setting_name`='received_emails' LIMIT 1"
 	incr, sql_err := db.Prepare(sql)
 	incr, sql_err := db.Prepare(sql)
 	if sql_err != nil {
 	if sql_err != nil {
-		logln(2, fmt.Sprintf("Sql statement incorrect: %s", sql_err))
+		logln(2, fmt.Sprintf("Sql statement incorrect: %s\n", sql_err))
 	}
 	}
 
 
 	//  receives values from the channel repeatedly until it is closed.
 	//  receives values from the channel repeatedly until it is closed.
 	for {
 	for {
 		client := <-SaveMailChan
 		client := <-SaveMailChan
-		if user, _, addr_err := validateEmailData(client); addr_err != nil { // user, host, addr_err
+		if user, _, addr_err := validateEmailData(client); addr_err != nil {
 			logln(1, fmt.Sprintln("mail_from didnt validate: %v", addr_err)+" client.mail_from:"+client.mail_from)
 			logln(1, fmt.Sprintln("mail_from didnt validate: %v", addr_err)+" client.mail_from:"+client.mail_from)
 			// notify client that a save completed, -1 = error
 			// notify client that a save completed, -1 = error
 			client.savedNotify <- -1
 			client.savedNotify <- -1
@@ -512,7 +520,7 @@ func saveMail() {
 		body = "gzencode"
 		body = "gzencode"
 		redis_err = redis.redisConnection()
 		redis_err = redis.redisConnection()
 		if redis_err == nil {
 		if redis_err == nil {
-			_, do_err := redis.conn.Do("SETEX", client.hash, 3600, client.data)
+			_, do_err := redis.conn.Do("SETEX", client.hash, getConfigInt("GSMTP_MAIL_EXPIRE_SECONDS", 7200), client.data)
 			if do_err == nil {
 			if do_err == nil {
 				client.data = ""
 				client.data = ""
 				body = "redis"
 				body = "redis"
@@ -534,13 +542,13 @@ func saveMail() {
 		// save, discard result
 		// save, discard result
 		_, _, err = ins.Exec()
 		_, _, err = ins.Exec()
 		if err != nil {
 		if err != nil {
-			logln(1, fmt.Sprintf("Database error, %v %v", err))
+			logln(1, fmt.Sprintf("Database error, %v ", err))
 			client.savedNotify <- -1
 			client.savedNotify <- -1
 		} else {
 		} else {
-			logln(1, "Email saved "+client.hash+" len:"+strconv.Itoa(length))
+			logln(0, "Email saved "+client.hash+" len:"+strconv.Itoa(length))
 			_, _, err = incr.Exec()
 			_, _, err = incr.Exec()
 			if err != nil {
 			if err != nil {
-				logln(1, fmt.Sprintf("Failed to incr count:", err))
+				logln(1, fmt.Sprintf("Failed to incr count: %v", err))
 			}
 			}
 			client.savedNotify <- 1
 			client.savedNotify <- 1
 		}
 		}
@@ -572,7 +580,7 @@ func validateEmailData(client *Client) (user string, host string, addr_err error
 	}
 	}
 	client.rcpt_to = user + "@" + host
 	client.rcpt_to = user + "@" + host
 	// check if on allowed hosts
 	// check if on allowed hosts
-	if allowed := allowedHosts[host]; !allowed {
+	if allowed := allowedHosts[strings.ToLower(host)]; !allowed {
 		return user, host, errors.New("invalid host:" + host)
 		return user, host, errors.New("invalid host:" + host)
 	}
 	}
 	return user, host, addr_err
 	return user, host, addr_err
@@ -641,13 +649,20 @@ func mailTransportDecode(str string, encoding_type string, charset string) strin
 	} else if encoding_type == "quoted-printable" {
 	} else if encoding_type == "quoted-printable" {
 		str = fromQuotedP(str)
 		str = fromQuotedP(str)
 	}
 	}
+
 	if charset != "UTF-8" {
 	if charset != "UTF-8" {
 		charset = fixCharset(charset)
 		charset = fixCharset(charset)
-		// eg. charset can be "ISO-2022-JP"
-		convstr, err := iconv.Conv(str, "UTF-8", charset)
-		if err == nil {
-			return convstr
+		if cd, err := iconv.Open("UTF-8", charset); err == nil{
+			defer func() {
+				cd.Close()
+				if r := recover(); r != nil {
+					logln(1, fmt.Sprintf("Recovered in %v", r))
+				}
+			}()
+			// eg. charset can be "ISO-2022-JP"
+			return cd.ConvString(str)
 		}
 		}
+
 	}
 	}
 	return str
 	return str
 }
 }