├── .gitignore ├── README.md ├── examples ├── save.php └── validate.php ├── fcgi.go └── fcgi_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastCGI processor for the [Go-guerrilla](https://github.com/flashmob/go-guerrilla) package. 2 | 3 | FastCGI is an optimized CGI protocol implementation used by web servers to execute scripts or other programs. 4 | 5 | Well, it's NOT only for web servers. Yes, you've read that right ;-) 6 | 7 | 8 | ## About 9 | 10 | This package is a _Processor_ for the Go-Guerrilla default `Backend` interface implementation. Typical use for this 11 | package is if you would like to add the ability to deliver emails to your FastCGI backend, using Go-Guerrilla's 12 | built-in _gateway_ backend. 13 | 14 | Just like a web server would hand over the HTTP request to a FastCGI backend, this plugin 15 | allows you to hand over the processing of an email to your FastCGI backend, such as php-fpm. 16 | 17 | The reason why you would do this is because perhaps your codebase is in a scripting language such as PHP, 18 | so there's no need to learn Go, becoming easier for you to maintain, no need to re-compile to change, use your favourite 19 | framework / library / IDE, etc. 20 | 21 | Also, there's no overhead of a web server - it goes straight to your script. 22 | 23 | 24 | ## Usage 25 | 26 | Import `"github.com/flashmob/fastcgi-processor"` to your Go-guerrilla project. 27 | Assuming you have imported the go-guerrilla package already, and all dependencies. 28 | 29 | Then, when [using go-guerrilla as a package](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package), use something like this 30 | 31 | ```go 32 | 33 | 34 | cfg := &AppConfig{ 35 | LogFile: "stderr", 36 | AllowedHosts: []string{"example.com"}, 37 | BackendConfig: backends.BackendConfig{ 38 | "save_process" : "HeadersParser|Debugger|FastCGI", 39 | "validate_process" : "FastCGI", 40 | "fcgi_script_filename_save" : "/home/path/to/save.php", 41 | "fcgi_script_filename_validate" : "/home/path/to/validate.php", 42 | "fcgi_connection_type" : "unix", 43 | "fcgi_connection_address" : "/tmp/php-fpm.sock" 44 | }, 45 | } 46 | d := Daemon{Config: cfg} 47 | d.AddProcessor("FastCGI", fastcgi_processor.Processor) 48 | 49 | d.Start() 50 | 51 | // .. keep the server busy.. 52 | 53 | ``` 54 | 55 | 56 | This will let Go-Guerrilla know about your FastCGI processor. Note that here we've 57 | added `FastCGI` to the end of the `save_process` config option, then used the `d.AddProcessor` api 58 | call to register it. Then configured other settings. 59 | 60 | See the configuration section for how to configure. 61 | 62 | ## Configuration 63 | 64 | The following values are required in your `backend_config` section 65 | 66 | ```json 67 | { 68 | "fcgi_script_filename_save" : "/home/path/to/save.php", 69 | "fcgi_script_filename_validate" : "/home/path/to/validate.php", 70 | "fcgi_connection_type" : "unix", 71 | "fcgi_connection_address" : "/tmp/php-fpm.sock" 72 | } 73 | 74 | 75 | ``` 76 | 77 | `fcgi_connection_type` type can be `unix` or `tcp`. 78 | `fcgi_connection_address` is a path to a unix socket descriptor, or IP address with tcp port eg. "127.0.0.1:8000" 79 | 80 | If `fcgi_connection_address` using the unix socket descriptor, make sure your program has 81 | permissions for writing to it. The permissions will be tested during initialization. 82 | 83 | Don't forget to add `FastCGI` to the end of your `save_process` config option, eg: 84 | 85 | `"save_process": "HeadersParser|Debugger|Hasher|Header|FastCGI",` 86 | 87 | also, add `FastCGI` to the end of your `validate_process` config option if you want to use the validate script, eg: 88 | 89 | `"validate_process": "FastCGI",` 90 | 91 | # Scripting 92 | 93 | ## Validate Recipient Email 94 | 95 | A single parameter comes to to your recipient validating script via HTTP GET. 96 | 97 | * `rcpt_to` - the email address that we want to validate 98 | 99 | Output: 100 | 101 | Please echo the string *PASS* and nothing else if validation passed. 102 | Otherwise return anything you wish. 103 | 104 | ## Save Mail 105 | 106 | The parameters comes to to your saving script via a HTTP POST. 107 | 108 | The following parameters will be sent: 109 | 110 | - `remote_ip` - remote ip address of the client that we got the email from (not the sender) 111 | - `subject` - the subject of the email (if available) 112 | - `tls_on` - boolean, represented as string "true" or "false", was the connection a TLS connection? 113 | - `helo` - hello sent by the client when connecting to us 114 | - `mail_from` - string of the From email address, could be blank to indicate a bounce 115 | - `body` - the raw email body, along with the headers. Please make sure your Fast CGI gateway can handle large enough POST 116 | 117 | Output: 118 | 119 | Please echo the string `SAVED` if successful. 120 | 121 | ## Example 122 | 123 | See MailDiranasaurus - it uses this package as an example, https://github.com/flashmob/MailDiranasaurus 124 | 125 | ## Credits 126 | 127 | This package depends on Shen Sheng's [Go Fastcgi client](https://github.com/tomasen/fcgi_client) package. 128 | 129 | `go get github.com/sloonz/go-maildir` 130 | 131 | ## Tips 132 | 133 | Your FastCGI script should timeout well before 30 seconds, preferably finish under 1 second. 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /examples/save.php: -------------------------------------------------------------------------------- 1 | 0 { 181 | v := url.Values{} 182 | v.Set("rcpt_to", e.RcptTo[len(e.RcptTo)-1].String()) 183 | result, err := p.get(p.config.ScriptFileNameNameValidate, v) 184 | if err != nil { 185 | backends.Log().Debug("FastCgi error", err) 186 | return backends.NewResult( 187 | response.Canned.FailNoSenderDataCmd), 188 | backends.StorageNotAvailable 189 | } 190 | if string(result[0:6]) == "PASSED" { 191 | // validation passed 192 | return c.Process(e, task) 193 | } else { 194 | // validation failed 195 | backends.Log().Debug("FastCgi Read Body failed", err) 196 | return backends.NewResult( 197 | response.Canned.FailNoSenderDataCmd), 198 | backends.StorageNotAvailable 199 | } 200 | 201 | return c.Process(e, task) 202 | 203 | } 204 | return c.Process(e, task) 205 | } else if task == backends.TaskSaveMail { 206 | for i := range e.RcptTo { 207 | // POST to FCGI 208 | resp, err := p.postSave(e) 209 | if err != nil { 210 | 211 | } else if strings.Index(string(resp), "SAVED") == 0 { 212 | return c.Process(e, task) 213 | } else { 214 | backends.Log().WithError(err).Error("Could not save email") 215 | return backends.NewResult(fmt.Sprintf("554 Error: could not save email for [%s]", e.RcptTo[i])), err 216 | } 217 | } 218 | // continue to the next Processor in the decorator chain 219 | return c.Process(e, task) 220 | } else { 221 | return c.Process(e, task) 222 | } 223 | 224 | }) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /fcgi_test.go: -------------------------------------------------------------------------------- 1 | package fcgi_processor 2 | 3 | import ( 4 | "github.com/flashmob/go-guerrilla/mail" 5 | "net/url" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | // change baseDir to location of your save.php and validate.php scripts 11 | var baseDir string = "/vagrant/projects/golang/src/github.com/flashmob/fastcgi-processor/examples/" 12 | 13 | // fcgiType is "unix" or "tcp" 14 | var fcgiType string = "unix" 15 | 16 | // fcgiAddr is a path to a unix socket descriptor, or IP address with tcp port eg. "127.0.0.1:8000" 17 | var fcgiAddr string = "/var/run/php/php7.0-fpm.sock" 18 | 19 | // Test GET method to validate recipient 20 | // This test requires a functioning php-fpm daemon 21 | func TestGet(t *testing.T) { 22 | 23 | c := &fcgiConfig{ 24 | ScriptFileNameNameSave: baseDir + "/save.php", 25 | ScriptFileNameNameValidate: baseDir + "validate.php", 26 | ConnectionType: fcgiType, 27 | ConnectionAddress: fcgiAddr, 28 | } 29 | 30 | f, err := newFastCGIProcessor(c) 31 | if err != nil { 32 | t.Error("could not newFastCGIPorcessor", err) 33 | } 34 | 35 | q := url.Values{} 36 | q.Add("rcpt_to", "test@moo.com") 37 | result, err := f.get(c.ScriptFileNameNameValidate, q) 38 | 39 | if strings.Index(string(result), "PASSED") != 0 { 40 | t.Error("save did not return PASSED, it got:", string(result)) 41 | } 42 | } 43 | 44 | // Test email saving using POST 45 | // This test requires a functioning php-fpm daemon 46 | func TestPost(t *testing.T) { 47 | c := &fcgiConfig{ 48 | ScriptFileNameNameSave: baseDir + "save.php", 49 | ScriptFileNameNameValidate: baseDir + "validate.php", 50 | ConnectionType: fcgiType, 51 | ConnectionAddress: fcgiAddr, 52 | } 53 | 54 | f, err := newFastCGIProcessor(c) 55 | 56 | if err != nil { 57 | t.Error("could not newFastCGIProcessor", err) 58 | } 59 | 60 | q := url.Values{} 61 | q.Add("rcpt_to", "test@moo.com") 62 | 63 | e := &mail.Envelope{ 64 | RemoteIP: "127.0.0.1", 65 | QueuedId: "abc12345", 66 | Helo: "helo.example.com", 67 | MailFrom: mail.Address{"test", "example.com"}, 68 | TLS: true, 69 | } 70 | e.PushRcpt(mail.Address{"test", "example.com"}) 71 | e.Data.WriteString("Subject:Test\n\nThis is a test.") 72 | 73 | result, err := f.postSave(e) 74 | 75 | if err != nil { 76 | t.Error("postSave error", err) 77 | } 78 | 79 | if strings.Index(string(result), "SAVED") != 0 { 80 | t.Error("save did not return SAVED, it got:", string(result)) 81 | } 82 | } 83 | --------------------------------------------------------------------------------