├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── build.sh ├── dependencies.sh ├── email └── email.go ├── generate_keys.sh ├── goguerrilla.go ├── install.sh ├── makefile ├── pgp_encrypt └── pgp_encrypt.go ├── publickey ├── publickey.go └── publickey_test.go ├── smtp.conf.sample └── test_scripts ├── causecrash.rb ├── sendattachment.rb └── sendmail.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.asc 2 | *.key 3 | *.crt 4 | *.conf 5 | *.csr 6 | *.secure 7 | goguerrilla 8 | bin/ 9 | release/ 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.3 (2015-11-1) 4 | 5 | ### Fix release 6 | 7 | - The previous release had a nasty bug preventing the message body from showing 8 | if the attachment was enabled. This has been fixed 9 | 10 | No new features, just a bug fix 11 | 12 | ## v0.2 (2015-11-1) 13 | 14 | ### Initial attachment support 15 | 16 | - Make use of forked Email package for easier sending of attachments 17 | - Email body can be attached for easier reading on mobile devices 18 | - Default configuration should require less modifying 19 | - Key generation script only requires typing the password once 20 | 21 | I will try and get attachment relaying into the next release 22 | 23 | ## v0.1 (2015-10-31) 24 | 25 | ### Initial Release 26 | 27 | - Email relaying 28 | - Email encryption 29 | - Keys downloaded from keyserver 30 | - Error handling of malformed emails 31 | 32 | Doesn't support attachments yet -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Software Licenses 2 | 3 | This software uses code from the following OSS projects and inherits their license terms. 4 | 5 | ## Go-Guerrilla SMTPd 6 | 7 | A minimalist SMTP server written in Go, made for receiving large volumes of mail. 8 | Works either as a stand-alone or in conjunction with Nginx SMTP proxy. 9 | 10 | Copyright (c) 2012 Flashmob, GuerrillaMail.com 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 13 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 14 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 18 | Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 21 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 22 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | Version: 1.1 26 | Author: Flashmob, GuerrillaMail.com 27 | Contact: flashmob@gmail.com 28 | License: MIT 29 | Repository: https://github.com/flashmob/Go-Guerrilla-SMTPd 30 | Site: http://www.guerrillamail.com/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | PGP Email Relay 3 | ==================== 4 | 5 | A simple SMTP relay that will encrypt any emails that it receives, and will relay 6 | the message via a remote SMTP server. 7 | 8 | The intended purpose of this project is to allow an application that sends automated emails to encrypt them before they reach the recipient, without modifying the source code of the application itself. 9 | 10 | Most of the SMTP code is based on [Go Guerrilla](https://github.com/flashmob/go-guerrilla) by Flashmob 11 | 12 | Public keys are downloaded from a keyserver (the keyserver URL is configurable). You can also place your own keys into the key cache folder, these will not be overwritten. 13 | 14 | 15 | Building 16 | =========================== 17 | 18 | To build, you will need do the following; 19 | 20 | 1. Install Golang 21 | 2. Run the build script ```./build.sh``` 22 | 23 | 24 | Before you run the server 25 | =========================== 26 | 27 | 1. Rename smtp.conf.sample to smtp.conf and modify accordingly 28 | 2. Run the key generation script ```./generate_keys.sh``` 29 | 30 | 31 | Configuration 32 | ============================================ 33 | The configuration is in strict JSON format. Here is an annotated configuration. 34 | Copy smtp.conf.sample to smtp.conf 35 | 36 | | Config Option | Purpose | Example | 37 | |---|---|---| 38 | |REMOTE_SMTP_USER|Remote SMTP server username| user@remotehost.com | 39 | |REMOTE_SMTP_PASS|Remote SMTP server password| p@55w0rd | 40 | |REMOTE_SMTP_HOST|Remote SMTP server hostname| smtp.remotehost.com | 41 | |REMOTE_SMTP_PORT|Which port the remote SMTP server is listening on| 25 | 42 | |PGP_KEYSERVER|The PGP keyserver that will be used to cache keys from| pgp.mit.edu | 43 | |PGP_KEYSERVER_QUERY|The URL query that is used to search for keys| /pks/lookup?op=index&exact=on&search= | 44 | |PGP_KEY_FOLDER|Where keys are cached| /tmp/key_cache | 45 | |PGP_ATTACH_BODY|Attach the contents of the encrypted email body. Makes it easier to decrypt on mobile clients.| Y or N | 46 | |GM_ALLOWED_HOSTS|Which domains accept mail| localhost, mail.yourhost.com | 47 | |GM_PRIMARY_MAIL_HOST|Given in the SMTP greeting| mail.yourhost.com | 48 | |GSMTP_HOST_NAME|Given in the SMTP greeting| mail.yourhost.com | 49 | |GSMTP_LOG_FILE"|Not used yet| N/A | 50 | |GSMTP_MAX_SIZE|Max size of DATA command| 15728640 | 51 | |GSMTP_PRV_KEY|Private key for TLS|server.key| 52 | |GSMTP_PUB_KEY|Public key for TLS|server.cert| 53 | |GSMTP_TIMEOUT|TCP connection timeout in seconds|100| 54 | |GSMTP_VERBOSE|Enable Debugging|Y or N| 55 | |GSTMP_LISTEN_INTERFACE|What IP:PORT to listen on|127.0.0.1:25| 56 | |GM_MAX_CLIENTS|Max clients that can be handled|500| 57 | |NGINX_AUTH_ENABLED| Enable Nginx authentication|Y or N| 58 | |NGINX_AUTH|If using Nginx proxy, choose an ip and port to serve Auth requsts for Nginx|127.0.0.1:8025| 59 | |SGID|Group id of the user from /etc/passwd|508| 60 | |GUID|Uid from /etc/passwd|504| 61 | 62 | Using Nginx as a proxy 63 | ========================================================= 64 | Nginx can be used to proxy SMTP traffic for GoGuerrilla SMTPd 65 | 66 | Why proxy SMTP? 67 | 68 | * Terminate TLS connections: Golang is not there yet when it comes to TLS. 69 | At present, only a partial implementation of TLS is provided (as of Nov 2012). 70 | OpenSSL on the other hand, used in Nginx, has a complete implementation of 71 | SSL v2/v3 and TLS protocols. 72 | * Could be used for load balancing and authentication in the future. 73 | 74 | 1. Compile nginx with --with-mail --with-mail_ssl_module 75 | 76 | 2. Configuration: 77 | 78 | 79 | mail { 80 | auth_http 127.0.0.1:8025/; # This is the URL to GoGuerrilla's http service which tells Nginx where to proxy the traffic to 81 | server { 82 | listen 15.29.8.163:25; 83 | protocol smtp; 84 | server_name ak47.example.com; 85 | 86 | smtp_auth none; 87 | timeout 30000; 88 | smtp_capabilities "SIZE 15728640"; 89 | 90 | # ssl default off. Leave off if starttls is on 91 | #ssl on; 92 | ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; 93 | ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; 94 | ssl_session_timeout 5m; 95 | ssl_protocols SSLv2 SSLv3 TLSv1; 96 | ssl_ciphers HIGH:!aNULL:!MD5; 97 | ssl_prefer_server_ciphers on; 98 | # TLS off unless client issues STARTTLS command 99 | starttls on; 100 | proxy on; 101 | } 102 | } 103 | 104 | 105 | Assuming that Guerrilla SMTPd has the following configuration settings: 106 | 107 | "GSMTP_MAX_SIZE" "15728640", 108 | "NGINX_AUTH_ENABLED": "Y", 109 | "NGINX_AUTH": "127.0.0.1:8025", 110 | 111 | 112 | Starting / Command Line usage 113 | ========================================================== 114 | 115 | All command line arguments are optional 116 | 117 | -config="smtp.conf": Path to the configuration file 118 | -if="": Interface and port to listen on, eg. 127.0.0.1:2525 119 | -v="n": Verbose, [y | n] 120 | 121 | Starting from the command line (example) 122 | 123 | /usr/bin/nohup /home/elmundio87/pgp-email-relaay -config=/home/elmundio87/smtp.conf 2>&1 & 124 | 125 | This will place goguerrilla in the background and continue running 126 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | ./dependencies.sh 2 | 3 | rm -rf ./release 4 | 5 | BIN="pgp-smtpd" 6 | TAG=`git describe --tags` 7 | 8 | for GOOS in windows darwin linux; do 9 | for GOARCH in 386 amd64; do 10 | echo "Building $GOOS-$GOARCH" 11 | export GOOS=$GOOS 12 | export GOARCH=$GOARCH 13 | if [ "$GOOS" == "windows" ] 14 | then 15 | go build -o release/$GOOS-$GOARCH/${BIN}.exe 16 | else 17 | go build -o release/$GOOS-$GOARCH/${BIN} 18 | fi 19 | zip -j release/${BIN}-${GOOS}-${GOARCH}-${TAG}.zip release/$GOOS-$GOARCH/* README.md smtp.conf.sample generate_keys.sh 20 | done 21 | done 22 | 23 | -------------------------------------------------------------------------------- /dependencies.sh: -------------------------------------------------------------------------------- 1 | echo "Fetching dependencies..." 2 | go get -v golang.org/x/crypto/openpgp 3 | go get -v github.com/cryptix/go/logging 4 | go get -v github.com/sloonz/go-iconv 5 | go get -v github.com/garyburd/redigo/redis 6 | go get -v github.com/sloonz/go-qprintable 7 | go get -v golang.org/x/net/html 8 | go get -v github.com/elmundio87/email -------------------------------------------------------------------------------- /email/email.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Santiago Corredoira 2 | // Forked from https://github.com/scorredoira/email 3 | // Distributed under a BSD-like license. 4 | package email 5 | 6 | import ( 7 | "bytes" 8 | "encoding/base64" 9 | "fmt" 10 | "io/ioutil" 11 | "net/smtp" 12 | "path/filepath" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | type Attachment struct { 18 | Filename string 19 | Data []byte 20 | Inline bool 21 | } 22 | 23 | type Message struct { 24 | From string 25 | To []string 26 | Cc []string 27 | Bcc []string 28 | ReplyTo string 29 | Subject string 30 | Body string 31 | BodyContentType string 32 | Attachments map[string]*Attachment 33 | } 34 | 35 | func (m *Message) attach(file string, inline bool) error { 36 | data, err := ioutil.ReadFile(file) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | _, filename := filepath.Split(file) 42 | 43 | m.Attachments[filename] = &Attachment{ 44 | Filename: filename, 45 | Data: data, 46 | Inline: inline, 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (m *Message) attachData(filename string, data []byte, inline bool) error { 53 | 54 | m.Attachments[filename] = &Attachment{ 55 | Filename: filename, 56 | Data: data, 57 | Inline: inline, 58 | } 59 | 60 | return nil 61 | } 62 | 63 | //Elmundio87: Attach data without having to write to the disk 64 | func (m *Message) AttachData(filename string, data []byte) error { 65 | return m.attachData(filename, data, false) 66 | } 67 | 68 | func (m *Message) Attach(file string) error { 69 | return m.attach(file, false) 70 | } 71 | 72 | func (m *Message) Inline(file string) error { 73 | return m.attach(file, true) 74 | } 75 | 76 | func newMessage(subject string, body string, bodyContentType string) *Message { 77 | m := &Message{Subject: subject, Body: body, BodyContentType: bodyContentType} 78 | 79 | m.Attachments = make(map[string]*Attachment) 80 | 81 | return m 82 | } 83 | 84 | // NewMessage returns a new Message that can compose an email with attachments 85 | func NewMessage(subject string, body string) *Message { 86 | return newMessage(subject, body, "text/plain") 87 | } 88 | 89 | // NewMessage returns a new Message that can compose an HTML email with attachments 90 | func NewHTMLMessage(subject string, body string) *Message { 91 | return newMessage(subject, body, "text/html") 92 | } 93 | 94 | // ToList returns all the recipients of the email 95 | func (m *Message) Tolist() []string { 96 | tolist := m.To 97 | 98 | for _, cc := range m.Cc { 99 | tolist = append(tolist, cc) 100 | } 101 | 102 | for _, bcc := range m.Bcc { 103 | tolist = append(tolist, bcc) 104 | } 105 | 106 | return tolist 107 | } 108 | 109 | // Bytes returns the mail data 110 | func (m *Message) Bytes() []byte { 111 | buf := bytes.NewBuffer(nil) 112 | 113 | buf.WriteString("From: " + m.From + "\r\n") 114 | 115 | t := time.Now() 116 | buf.WriteString("Date: " + t.Format(time.RFC822) + "\r\n") 117 | 118 | buf.WriteString("To: " + strings.Join(m.To, ",") + "\r\n") 119 | if len(m.Cc) > 0 { 120 | buf.WriteString("Cc: " + strings.Join(m.Cc, ",") + "\r\n") 121 | } 122 | 123 | buf.WriteString("Subject: " + m.Subject + "\r\n") 124 | 125 | if len(m.ReplyTo) > 0 { 126 | buf.WriteString("Reply-To: " + m.ReplyTo + "\r\n") 127 | } 128 | 129 | buf.WriteString("MIME-Version: 1.0\r\n") 130 | 131 | boundary := "f46d043c813270fc6b04c2d223da" 132 | 133 | if len(m.Attachments) > 0 { 134 | buf.WriteString("Content-Type: multipart/mixed; boundary=" + boundary + "\r\n\r\n") 135 | buf.WriteString("--" + boundary + "\r\n") 136 | } 137 | 138 | buf.WriteString(fmt.Sprintf("Content-Type: %s; charset=utf-8\r\n\r\n", m.BodyContentType)) 139 | buf.WriteString(m.Body) 140 | buf.WriteString("\r\n") 141 | 142 | if len(m.Attachments) > 0 { 143 | for _, attachment := range m.Attachments { 144 | buf.WriteString("\r\n\r\n--" + boundary + "\r\n") 145 | 146 | if attachment.Inline { 147 | buf.WriteString("Content-Type: message/rfc822\r\n") 148 | buf.WriteString("Content-Disposition: inline; filename=\"" + attachment.Filename + "\"\r\n\r\n") 149 | 150 | buf.Write(attachment.Data) 151 | } else { 152 | buf.WriteString("Content-Type: application/octet-stream\r\n") 153 | buf.WriteString("Content-Transfer-Encoding: base64\r\n") 154 | buf.WriteString("Content-Disposition: attachment; filename=\"" + attachment.Filename + "\"\r\n\r\n") 155 | 156 | b := make([]byte, base64.StdEncoding.EncodedLen(len(attachment.Data))) 157 | base64.StdEncoding.Encode(b, attachment.Data) 158 | 159 | // write base64 content in lines of up to 76 chars 160 | for i, l := 0, len(b); i < l; i++ { 161 | buf.WriteByte(b[i]) 162 | if (i+1)%76 == 0 { 163 | buf.WriteString("\r\n") 164 | } 165 | } 166 | } 167 | 168 | buf.WriteString("\r\n--" + boundary) 169 | } 170 | 171 | buf.WriteString("--") 172 | } 173 | 174 | return buf.Bytes() 175 | } 176 | 177 | func Send(addr string, auth smtp.Auth, m *Message) error { 178 | return smtp.SendMail(addr, auth, m.From, m.Tolist(), m.Bytes()) 179 | } 180 | -------------------------------------------------------------------------------- /generate_keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Creating server key and certificate" 4 | echo "Enter a password: (Output Suppressed)" 5 | read -s password 6 | 7 | echo ${password} 8 | openssl genrsa -des3 -out server.key -passout pass:${password} 1024 9 | openssl req -new -key server.key -out server.csr -passin pass:${password} 10 | openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt -passin pass:${password} 11 | mv server.key server.key.secure 12 | echo "Decrypting key for the server to use it" 13 | openssl rsa -in server.key.secure -out server.key -passin pass:${password} 14 | 15 | echo "==============================================================================" 16 | echo "You now have a server.key and server.cert. Edit the GSMTP_PRV_KEY and" 17 | echo "GSMTP_PUB_KEY properties in the config file if you want to store these keys" 18 | echo "outside of the installation directory" 19 | echo "==============================================================================" -------------------------------------------------------------------------------- /goguerrilla.go: -------------------------------------------------------------------------------- 1 | /** 2 | Go-Guerrilla SMTPd 3 | A minimalist SMTP server written in Go, made for receiving large volumes of mail. 4 | Works either as a stand-alone or in conjunction with Nginx SMTP proxy. 5 | TO DO: add http server for nginx 6 | 7 | Copyright (c) 2012 Flashmob, GuerrillaMail.com 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 10 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 11 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 15 | Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 18 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | What is Go Guerrilla SMTPd? 23 | It's a small SMTP server written in Go, optimized for receiving email. 24 | Written for GuerrillaMail.com which processes tens of thousands of emails 25 | every hour. 26 | 27 | Benchmarking: 28 | http://www.jrh.org/smtp/index.html 29 | Test 500 clients: 30 | $ time smtp-source -c -l 5000 -t test@spam4.me -s 500 -m 5000 5.9.7.183 31 | 32 | Version: 1.1 33 | Author: Flashmob, GuerrillaMail.com 34 | Contact: flashmob@gmail.com 35 | License: MIT 36 | Repository: https://github.com/flashmob/Go-Guerrilla-SMTPd 37 | Site: http://www.guerrillamail.com/ 38 | 39 | See README for more details 40 | 41 | To build 42 | Install the following 43 | $ go get github.com/ziutek/mymysql/thrsafe 44 | $ go get github.com/ziutek/mymysql/autorc 45 | $ go get github.com/ziutek/mymysql/godrv 46 | $ go get github.com/sloonz/go-iconv 47 | 48 | TODO: after failing tls, 49 | 50 | patch: 51 | rebuild all: go build -a -v new.go 52 | 53 | */ 54 | 55 | package main 56 | 57 | import ( 58 | "bufio" 59 | "bytes" 60 | "compress/zlib" 61 | "crypto/md5" 62 | "crypto/rand" 63 | "crypto/tls" 64 | "encoding/base64" 65 | "encoding/hex" 66 | "encoding/json" 67 | "errors" 68 | "flag" 69 | "fmt" 70 | "github.com/garyburd/redigo/redis" 71 | // "github.com/sloonz/go-iconv" 72 | "github.com/sloonz/go-qprintable" 73 | "io" 74 | "io/ioutil" 75 | "log" 76 | "net" 77 | "net/http" 78 | "os" 79 | "regexp" 80 | "runtime" 81 | "strconv" 82 | "strings" 83 | "time" 84 | ) 85 | 86 | import "github.com/elmundio87/pgp-email-relay/pgp_encrypt" 87 | 88 | type Client struct { 89 | state int 90 | helo string 91 | mail_from string 92 | rcpt_to string 93 | read_buffer string 94 | response string 95 | address string 96 | data string 97 | subject string 98 | hash string 99 | time int64 100 | tls_on bool 101 | conn net.Conn 102 | bufin *bufio.Reader 103 | bufout *bufio.Writer 104 | kill_time int64 105 | errors int 106 | clientId int64 107 | savedNotify chan int 108 | } 109 | 110 | var TLSconfig *tls.Config 111 | var max_size int // max email DATA size 112 | var timeout time.Duration 113 | var allowedHosts = make(map[string]bool, 15) 114 | var sem chan int // currently active clients 115 | 116 | var SaveMailChan chan *Client // workers for saving mail 117 | // defaults. Overwrite any of these in the configure() function which loads them from a json file 118 | var gConfig = map[string]string{ 119 | "PGP_ATTACH_BODY": "Y", 120 | "GSMTP_MAX_SIZE": "131072", 121 | "GSMTP_HOST_NAME": "server.example.com", // This should also be set to reflect your RDNS 122 | "GSMTP_VERBOSE": "Y", 123 | "GSMTP_LOG_FILE": "", // Eg. /var/log/goguerrilla.log or leave blank if no logging 124 | "GSMTP_TIMEOUT": "100", // how many seconds before timeout. 125 | "GSTMP_LISTEN_INTERFACE": "0.0.0.0:25", 126 | "GSMTP_PUB_KEY": "/etc/ssl/certs/ssl-cert-snakeoil.pem", 127 | "GSMTP_PRV_KEY": "/etc/ssl/private/ssl-cert-snakeoil.key", 128 | "GM_ALLOWED_HOSTS": "guerrillamail.de,guerrillamailblock.com", 129 | "GM_PRIMARY_MAIL_HOST": "guerrillamail.com", 130 | "GM_MAX_CLIENTS": "500", 131 | "NGINX_AUTH_ENABLED": "N", // Y or N 132 | "NGINX_AUTH": "127.0.0.1:8025", // If using Nginx proxy, ip and port to serve Auth requsts 133 | "SGID": "1008", // group id 134 | "SUID": "1008", // user id, from /etc/passwd 135 | } 136 | 137 | type redisClient struct { 138 | count int 139 | conn redis.Conn 140 | time int 141 | } 142 | 143 | func logln(level int, s string) { 144 | 145 | if gConfig["GSMTP_VERBOSE"] == "Y" { 146 | fmt.Println(s) 147 | } 148 | if level == 2 { 149 | log.Fatalf(s) 150 | } 151 | if len(gConfig["GSMTP_LOG_FILE"]) > 0 { 152 | log.Println(s) 153 | } 154 | } 155 | 156 | func configure() { 157 | var configFile, verbose, iface string 158 | log.SetOutput(os.Stdout) 159 | // parse command line arguments 160 | flag.StringVar(&configFile, "config", "smtp.conf", "Path to the configuration file") 161 | flag.StringVar(&verbose, "v", "n", "Verbose, [y | n] ") 162 | flag.StringVar(&iface, "if", "", "Interface and port to listen on, eg. 127.0.0.1:2525 ") 163 | flag.Parse() 164 | // load in the config. 165 | b, err := ioutil.ReadFile(configFile) 166 | if err != nil { 167 | log.Fatalln("Could not read config file") 168 | } 169 | var myConfig map[string]string 170 | err = json.Unmarshal(b, &myConfig) 171 | if err != nil { 172 | log.Fatalln("Could not parse config file") 173 | } 174 | for k, v := range myConfig { 175 | gConfig[k] = v 176 | } 177 | gConfig["GSMTP_VERBOSE"] = strings.ToUpper(verbose) 178 | if len(iface) > 0 { 179 | gConfig["GSTMP_LISTEN_INTERFACE"] = iface 180 | } 181 | // map the allow hosts for easy lookup 182 | if arr := strings.Split(gConfig["GM_ALLOWED_HOSTS"], ","); len(arr) > 0 { 183 | for i := 0; i < len(arr); i++ { 184 | allowedHosts[arr[i]] = true 185 | } 186 | } 187 | var n int 188 | var n_err error 189 | // sem is an active clients channel used for counting clients 190 | if n, n_err = strconv.Atoi(gConfig["GM_MAX_CLIENTS"]); n_err != nil { 191 | n = 50 192 | } 193 | // currently active client list 194 | sem = make(chan int, n) 195 | // database writing workers 196 | SaveMailChan = make(chan *Client, 5) 197 | // timeout for reads 198 | if n, n_err = strconv.Atoi(gConfig["GSMTP_TIMEOUT"]); n_err != nil { 199 | timeout = time.Duration(10) 200 | } else { 201 | timeout = time.Duration(n) 202 | } 203 | // max email size 204 | if max_size, n_err = strconv.Atoi(gConfig["GSMTP_MAX_SIZE"]); n_err != nil { 205 | max_size = 131072 206 | } 207 | // custom log file 208 | if len(gConfig["GSMTP_LOG_FILE"]) > 0 { 209 | logfile, err := os.OpenFile(gConfig["GSMTP_LOG_FILE"], os.O_WRONLY|os.O_APPEND|os.O_CREATE|os.O_SYNC, 0600) 210 | if err != nil { 211 | log.Fatal("Unable to open log file ["+gConfig["GSMTP_LOG_FILE"]+"]: ", err) 212 | } 213 | log.SetOutput(logfile) 214 | } 215 | 216 | return 217 | } 218 | 219 | func main() { 220 | configure() 221 | cert, err := tls.LoadX509KeyPair(gConfig["GSMTP_PUB_KEY"], gConfig["GSMTP_PRV_KEY"]) 222 | if err != nil { 223 | logln(2, fmt.Sprintf("There was a problem with loading the certificate: %s", err)) 224 | } 225 | TLSconfig = &tls.Config{Certificates: []tls.Certificate{cert}, ClientAuth: tls.VerifyClientCertIfGiven, ServerName: gConfig["GSMTP_HOST_NAME"]} 226 | TLSconfig.Rand = rand.Reader 227 | // start some savemail workers 228 | for i := 0; i < 3; i++ { 229 | go saveMail() 230 | } 231 | if gConfig["NGINX_AUTH_ENABLED"] == "Y" { 232 | go nginxHTTPAuth() 233 | } 234 | // Start listening for SMTP connections 235 | listener, err := net.Listen("tcp", gConfig["GSTMP_LISTEN_INTERFACE"]) 236 | if err != nil { 237 | logln(2, fmt.Sprintf("Cannot listen on port, %v", err)) 238 | } else { 239 | logln(1, fmt.Sprintf("Listening on tcp %s", gConfig["GSTMP_LISTEN_INTERFACE"])) 240 | } 241 | var clientId int64 242 | clientId = 1 243 | welcomeMessage() 244 | for { 245 | conn, err := listener.Accept() 246 | if err != nil { 247 | logln(1, fmt.Sprintf("Accept error: %s", err)) 248 | continue 249 | } 250 | logln(1, fmt.Sprintf(" There are now "+strconv.Itoa(runtime.NumGoroutine())+" serving goroutines")) 251 | sem <- 1 // Wait for active queue to drain. 252 | go handleClient(&Client{ 253 | conn: conn, 254 | address: conn.RemoteAddr().String(), 255 | time: time.Now().Unix(), 256 | bufin: bufio.NewReader(conn), 257 | bufout: bufio.NewWriter(conn), 258 | clientId: clientId, 259 | savedNotify: make(chan int), 260 | }) 261 | clientId++ 262 | } 263 | } 264 | 265 | func handleClient(client *Client) { 266 | defer closeClient(client) 267 | // defer closeClient(client) 268 | greeting := "220 " + gConfig["GSMTP_HOST_NAME"] + 269 | " SMTP Guerrilla-SMTPd #" + strconv.FormatInt(client.clientId, 10) + " (" + strconv.Itoa(len(sem)) + ") " + time.Now().Format(time.RFC1123Z) 270 | advertiseTls := "250-STARTTLS\r\n" 271 | for i := 0; i < 100; i++ { 272 | switch client.state { 273 | case 0: 274 | responseAdd(client, greeting) 275 | client.state = 1 276 | case 1: 277 | input, err := readSmtp(client) 278 | if err != nil { 279 | logln(1, fmt.Sprintf("Read error: %v", err)) 280 | if err == io.EOF { 281 | // client closed the connection already 282 | return 283 | } 284 | if neterr, ok := err.(net.Error); ok && neterr.Timeout() { 285 | // too slow, timeout 286 | return 287 | } 288 | break 289 | } 290 | input = strings.Trim(input, " \n\r") 291 | cmd := strings.ToUpper(input) 292 | switch { 293 | case strings.Index(cmd, "HELO") == 0: 294 | if len(input) > 5 { 295 | client.helo = input[5:] 296 | } 297 | responseAdd(client, "250 "+gConfig["GSMTP_HOST_NAME"]+" Hello ") 298 | case strings.Index(cmd, "EHLO") == 0: 299 | if len(input) > 5 { 300 | client.helo = input[5:] 301 | } 302 | 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") 303 | case strings.Index(cmd, "MAIL FROM:") == 0: 304 | if len(input) > 10 { 305 | client.mail_from = input[10:] 306 | } 307 | responseAdd(client, "250 Ok") 308 | case strings.Index(cmd, "XCLIENT") == 0: 309 | // Nginx sends this 310 | // XCLIENT ADDR=212.96.64.216 NAME=[UNAVAILABLE] 311 | client.address = input[13:] 312 | client.address = client.address[0:strings.Index(client.address, " ")] 313 | fmt.Println("client address:[" + client.address + "]") 314 | responseAdd(client, "250 OK") 315 | case strings.Index(cmd, "RCPT TO:") == 0: 316 | if len(input) > 8 { 317 | client.rcpt_to = input[8:] 318 | } 319 | responseAdd(client, "250 Accepted") 320 | case strings.Index(cmd, "NOOP") == 0: 321 | responseAdd(client, "250 OK") 322 | case strings.Index(cmd, "RSET") == 0: 323 | client.mail_from = "" 324 | client.rcpt_to = "" 325 | responseAdd(client, "250 OK") 326 | case strings.Index(cmd, "DATA") == 0: 327 | responseAdd(client, "354 Enter message, ending with \".\" on a line by itself") 328 | client.state = 2 329 | case (strings.Index(cmd, "STARTTLS") == 0) && !client.tls_on: 330 | responseAdd(client, "220 Ready to start TLS") 331 | // go to start TLS state 332 | client.state = 3 333 | case strings.Index(cmd, "QUIT") == 0: 334 | responseAdd(client, "221 Bye") 335 | killClient(client) 336 | default: 337 | responseAdd(client, fmt.Sprintf("500 unrecognized command")) 338 | client.errors++ 339 | if client.errors > 3 { 340 | responseAdd(client, fmt.Sprintf("500 Too many unrecognized commands")) 341 | killClient(client) 342 | } 343 | } 344 | case 2: 345 | var err error 346 | client.data, err = readSmtp(client) 347 | if err == nil { 348 | // to do: timeout when adding to SaveMailChan 349 | // place on the channel so that one of the save mail workers can pick it up 350 | SaveMailChan <- client 351 | // wait for the save to complete 352 | status := <-client.savedNotify 353 | 354 | if status == 1 { 355 | responseAdd(client, "250 OK : queued as "+client.hash) 356 | } else { 357 | responseAdd(client, "554 Error: transaction failed, blame it on the weather") 358 | } 359 | } else { 360 | logln(1, fmt.Sprintf("DATA read error: %v", err)) 361 | } 362 | client.state = 1 363 | case 3: 364 | // upgrade to TLS 365 | var tlsConn *tls.Conn 366 | tlsConn = tls.Server(client.conn, TLSconfig) 367 | err := tlsConn.Handshake() // not necessary to call here, but might as well 368 | if err == nil { 369 | client.conn = net.Conn(tlsConn) 370 | client.bufin = bufio.NewReader(client.conn) 371 | client.bufout = bufio.NewWriter(client.conn) 372 | client.tls_on = true 373 | } else { 374 | logln(1, fmt.Sprintf("Could not TLS handshake:%v", err)) 375 | } 376 | advertiseTls = "" 377 | client.state = 1 378 | } 379 | // Send a response back to the client 380 | err := responseWrite(client) 381 | if err != nil { 382 | if err == io.EOF { 383 | // client closed the connection already 384 | return 385 | } 386 | if neterr, ok := err.(net.Error); ok && neterr.Timeout() { 387 | // too slow, timeout 388 | return 389 | } 390 | } 391 | if client.kill_time > 1 { 392 | return 393 | } 394 | } 395 | 396 | } 397 | 398 | func responseAdd(client *Client, line string) { 399 | client.response = line + "\r\n" 400 | } 401 | func closeClient(client *Client) { 402 | client.conn.Close() 403 | <-sem // Done; enable next client to run. 404 | } 405 | func killClient(client *Client) { 406 | client.kill_time = time.Now().Unix() 407 | } 408 | 409 | func readSmtp(client *Client) (input string, err error) { 410 | var reply string 411 | // Command state terminator by default 412 | suffix := "\r\n" 413 | if client.state == 2 { 414 | // DATA state 415 | suffix = "\r\n.\r\n" 416 | } 417 | for err == nil { 418 | client.conn.SetDeadline(time.Now().Add(timeout * time.Second)) 419 | reply, err = client.bufin.ReadString('\n') 420 | if reply != "" { 421 | input = input + reply 422 | if len(input) > max_size { 423 | err = errors.New("Maximum DATA size exceeded (" + strconv.Itoa(max_size) + ")") 424 | return input, err 425 | } 426 | if client.state == 2 { 427 | // Extract the subject while we are at it. 428 | scanSubject(client, reply) 429 | } 430 | } 431 | if err != nil { 432 | break 433 | } 434 | if strings.HasSuffix(input, suffix) { 435 | break 436 | } 437 | } 438 | return input, err 439 | } 440 | 441 | // Scan the data part for a Subject line. Can be a multi-line 442 | func scanSubject(client *Client, reply string) { 443 | if client.subject == "" && (len(reply) > 8) { 444 | test := strings.ToUpper(reply[0:9]) 445 | if i := strings.Index(test, "SUBJECT: "); i == 0 { 446 | // first line with \r\n 447 | client.subject = reply[9:] 448 | } 449 | } else if strings.HasSuffix(client.subject, "\r\n") { 450 | // chop off the \r\n 451 | client.subject = client.subject[0 : len(client.subject)-2] 452 | if (strings.HasPrefix(reply, " ")) || (strings.HasPrefix(reply, "\t")) { 453 | // subject is multi-line 454 | client.subject = client.subject + reply[1:] 455 | } 456 | } 457 | } 458 | 459 | func responseWrite(client *Client) (err error) { 460 | var size int 461 | client.conn.SetDeadline(time.Now().Add(timeout * time.Second)) 462 | size, err = client.bufout.WriteString(client.response) 463 | client.bufout.Flush() 464 | client.response = client.response[size:] 465 | return err 466 | } 467 | 468 | func saveMail() { 469 | 470 | for { 471 | client := <-SaveMailChan 472 | 473 | pgp_encrypt.HandleMail(client.data, client.rcpt_to, gConfig) 474 | 475 | client.savedNotify <- 1 476 | } 477 | 478 | } 479 | 480 | func (c *redisClient) redisConnection() (err error) { 481 | if c.count > 100 { 482 | c.conn.Close() 483 | c.count = 0 484 | } 485 | if c.count == 0 { 486 | c.conn, err = redis.Dial("tcp", ":6379") 487 | if err != nil { 488 | // handle error 489 | return err 490 | } 491 | } 492 | return nil 493 | } 494 | 495 | func validateEmailData(client *Client) (user string, host string, addr_err error) { 496 | if user, host, addr_err = extractEmail(client.mail_from); addr_err != nil { 497 | return user, host, addr_err 498 | } 499 | client.mail_from = user + "@" + host 500 | if user, host, addr_err = extractEmail(client.rcpt_to); addr_err != nil { 501 | return user, host, addr_err 502 | } 503 | client.rcpt_to = user + "@" + host 504 | // check if on allowed hosts 505 | if allowed := allowedHosts[host]; !allowed { 506 | return user, host, errors.New("invalid host:" + host) 507 | } 508 | return user, host, addr_err 509 | } 510 | 511 | func extractEmail(str string) (name string, host string, err error) { 512 | re, _ := regexp.Compile(`<(.+?)@(.+?)>`) // go home regex, you're drunk! 513 | if matched := re.FindStringSubmatch(str); len(matched) > 2 { 514 | host = validHost(matched[2]) 515 | name = matched[1] 516 | } else { 517 | if res := strings.Split(str, "@"); len(res) > 1 { 518 | name = res[0] 519 | host = validHost(res[1]) 520 | } 521 | } 522 | if host == "" || name == "" { 523 | err = errors.New("Invalid address, [" + name + "@" + host + "] address:" + str) 524 | } 525 | return name, host, err 526 | } 527 | 528 | // Decode strings in Mime header format 529 | // eg. =?ISO-2022-JP?B?GyRCIVo9dztSOWJAOCVBJWMbKEI=?= 530 | func mimeHeaderDecode(str string) string { 531 | reg, _ := regexp.Compile(`=\?(.+?)\?([QBqp])\?(.+?)\?=`) 532 | matched := reg.FindAllStringSubmatch(str, -1) 533 | var charset, encoding, payload string 534 | if matched != nil { 535 | for i := 0; i < len(matched); i++ { 536 | if len(matched[i]) > 2 { 537 | charset = matched[i][1] 538 | encoding = strings.ToUpper(matched[i][2]) 539 | payload = matched[i][3] 540 | switch encoding { 541 | case "B": 542 | str = strings.Replace(str, matched[i][0], mailTransportDecode(payload, "base64", charset), 1) 543 | case "Q": 544 | str = strings.Replace(str, matched[i][0], mailTransportDecode(payload, "quoted-printable", charset), 1) 545 | } 546 | } 547 | } 548 | } 549 | return str 550 | } 551 | 552 | func validHost(host string) string { 553 | host = strings.Trim(host, " ") 554 | re, _ := regexp.Compile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`) 555 | if re.MatchString(host) { 556 | return host 557 | } 558 | return "" 559 | } 560 | 561 | // decode from 7bit to 8bit UTF-8 562 | // encoding_type can be "base64" or "quoted-printable" 563 | func mailTransportDecode(str string, encoding_type string, charset string) string { 564 | if charset == "" { 565 | charset = "UTF-8" 566 | } else { 567 | charset = strings.ToUpper(charset) 568 | } 569 | if encoding_type == "base64" { 570 | str = fromBase64(str) 571 | } else if encoding_type == "quoted-printable" { 572 | str = fromQuotedP(str) 573 | } 574 | /*if charset != "UTF-8" { 575 | charset = fixCharset(charset) 576 | // eg. charset can be "ISO-2022-JP" 577 | convstr, err := iconv.Conv(str, "UTF-8", charset) 578 | if err == nil { 579 | return convstr 580 | } 581 | }*/ 582 | return str 583 | } 584 | 585 | func fromBase64(data string) string { 586 | buf := bytes.NewBufferString(data) 587 | decoder := base64.NewDecoder(base64.StdEncoding, buf) 588 | res, _ := ioutil.ReadAll(decoder) 589 | return string(res) 590 | } 591 | 592 | func fromQuotedP(data string) string { 593 | buf := bytes.NewBufferString(data) 594 | decoder := qprintable.NewDecoder(qprintable.BinaryEncoding, buf) 595 | res, _ := ioutil.ReadAll(decoder) 596 | return string(res) 597 | } 598 | 599 | func compress(s string) string { 600 | var b bytes.Buffer 601 | w, _ := zlib.NewWriterLevel(&b, zlib.BestSpeed) // flate.BestCompression 602 | w.Write([]byte(s)) 603 | w.Close() 604 | return b.String() 605 | } 606 | 607 | func fixCharset(charset string) string { 608 | reg, _ := regexp.Compile(`[_:.\/\\]`) 609 | fixed_charset := reg.ReplaceAllString(charset, "-") 610 | // Fix charset 611 | // borrowed from http://squirrelmail.svn.sourceforge.net/viewvc/squirrelmail/trunk/squirrelmail/include/languages.php?revision=13765&view=markup 612 | // OE ks_c_5601_1987 > cp949 613 | fixed_charset = strings.Replace(fixed_charset, "ks-c-5601-1987", "cp949", -1) 614 | // Moz x-euc-tw > euc-tw 615 | fixed_charset = strings.Replace(fixed_charset, "x-euc", "euc", -1) 616 | // Moz x-windows-949 > cp949 617 | fixed_charset = strings.Replace(fixed_charset, "x-windows_", "cp", -1) 618 | // windows-125x and cp125x charsets 619 | fixed_charset = strings.Replace(fixed_charset, "windows-", "cp", -1) 620 | // ibm > cp 621 | fixed_charset = strings.Replace(fixed_charset, "ibm", "cp", -1) 622 | // iso-8859-8-i -> iso-8859-8 623 | fixed_charset = strings.Replace(fixed_charset, "iso-8859-8-i", "iso-8859-8", -1) 624 | if charset != fixed_charset { 625 | return fixed_charset 626 | } 627 | return charset 628 | } 629 | 630 | func md5hex(str string) string { 631 | h := md5.New() 632 | h.Write([]byte(str)) 633 | sum := h.Sum([]byte{}) 634 | return hex.EncodeToString(sum) 635 | } 636 | 637 | // If running Nginx as a proxy, give Nginx the IP address and port for the SMTP server 638 | // Primary use of Nginx is to terminate TLS so that Go doesn't need to deal with it. 639 | // This could perform auth and load balancing too 640 | // See http://wiki.nginx.org/MailCoreModule 641 | func nginxHTTPAuth() { 642 | parts := strings.Split(gConfig["GSTMP_LISTEN_INTERFACE"], ":") 643 | gConfig["HTTP_AUTH_HOST"] = parts[0] 644 | gConfig["HTTP_AUTH_PORT"] = parts[1] 645 | fmt.Println(parts) 646 | http.HandleFunc("/", nginxHTTPAuthHandler) 647 | err := http.ListenAndServe(gConfig["NGINX_AUTH"], nil) 648 | if err != nil { 649 | log.Fatal("ListenAndServe: ", err) 650 | } 651 | 652 | } 653 | 654 | func nginxHTTPAuthHandler(w http.ResponseWriter, r *http.Request) { 655 | w.Header().Add("Auth-Status", "OK") 656 | w.Header().Add("Auth-Server", gConfig["HTTP_AUTH_HOST"]) 657 | w.Header().Add("Auth-Port", gConfig["HTTP_AUTH_PORT"]) 658 | fmt.Fprint(w, "") 659 | } 660 | 661 | func welcomeMessage() { 662 | message := ` 663 | PGP Email Relay has started without any errors. 664 | ` 665 | fmt.Println(message) 666 | } 667 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export bin=goguerrilla 4 | export installdir=/etc/pgp_email_relay 5 | export GOPATH=/tmp/go 6 | 7 | if [[ $EUID -ne 0 ]]; then 8 | echo "You must be a root user" 2>&1 9 | exit 1 10 | fi 11 | 12 | if [ ! -f ${bin} ]; then 13 | echo "Binary missing, running build script..." 14 | ./build.sh 15 | fi 16 | 17 | if [ ! -f ${bin}.conf ]; then 18 | echo "Missing configuration file: ${bin}.conf" 19 | exit 1 20 | fi 21 | 22 | echo "Creating install directory ${installdir}" 23 | mkdir -p $installdir 24 | 25 | if [ ! -f server.key ]; then 26 | echo "server.key missing, generating certificate and key" 27 | ./generate_keys.sh 28 | fi 29 | 30 | echo "Moving files to ${installdir}" 31 | cp $bin server.{key,crt} ${bin}.conf $installdir/ 32 | echo "Done!" 33 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @go test ./... 3 | 4 | test-cov: 5 | @go test ./... -cover 6 | -------------------------------------------------------------------------------- /pgp_encrypt/pgp_encrypt.go: -------------------------------------------------------------------------------- 1 | package pgp_encrypt 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/cryptix/go/logging" 7 | "github.com/elmundio87/pgp-email-relay/email" 8 | "github.com/elmundio87/pgp-email-relay/publickey" 9 | "golang.org/x/crypto/openpgp" 10 | "golang.org/x/crypto/openpgp/armor" 11 | "html/template" 12 | "io" 13 | "io/ioutil" 14 | "net/mail" 15 | "net/smtp" 16 | "os" 17 | "path" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | const encryptionType = "PGP MESSAGE" 23 | 24 | func HandleMail(client_data string, client_rcpt_to string, gConfig map[string]string) { 25 | 26 | var to = client_rcpt_to[1 : len(client_rcpt_to)-1] 27 | 28 | addresses := strings.Split(to, ",") 29 | 30 | for _, address := range addresses { 31 | 32 | emailData := client_data[:len(client_data)-4] 33 | msg, err := mail.ReadMessage(bytes.NewBuffer([]byte(emailData))) 34 | 35 | if err != nil { 36 | sendErrorReport(err, emailData, address, gConfig) 37 | return 38 | } 39 | 40 | headers := make(map[string]string) 41 | for key, value := range msg.Header { 42 | headers[key] = value[0] 43 | } 44 | 45 | body, _ := ioutil.ReadAll(msg.Body) 46 | 47 | encryptedBody := encrypt(string(body), address, gConfig) 48 | 49 | sendEmail(headers, encryptedBody, address, gConfig) 50 | 51 | } 52 | 53 | } 54 | 55 | // http://stackoverflow.com/a/31742265 56 | func sendErrorReport(err error, emailData string, address string, gConfig map[string]string) { 57 | 58 | bodyTemplate := ` 59 | Error: {{.Error}} 60 | Time: {{.Time}} 61 | 62 | Open issues: https://github.com/elmundio87/pgp-email-relay/issues 63 | 64 | Feel free to submit a bug report. 65 | 66 | Below is the message that caused the error (encrypted); 67 | 68 | ` 69 | encryptedDataDump := encrypt(string(emailData), address, gConfig) 70 | data := map[string]interface{}{ 71 | "Error": err.Error(), 72 | "Address": address, 73 | "Time": time.Now(), 74 | } 75 | 76 | headers := make(map[string]string) 77 | headers["Subject"] = "Crash Report" 78 | headers["From"] = gConfig["REMOTE_SMTP_USER"] 79 | 80 | t := template.Must(template.New("email").Parse(bodyTemplate)) 81 | buf := &bytes.Buffer{} 82 | if err := t.Execute(buf, data); err != nil { 83 | panic(err) 84 | } 85 | body := buf.String() 86 | 87 | sendEmail(headers, body+encryptedDataDump, address, gConfig) 88 | 89 | } 90 | 91 | func encrypt(input string, email string, gConfig map[string]string) string { 92 | 93 | os.MkdirAll(gConfig["PGP_KEY_FOLDER"], 0777) 94 | keyfileName := path.Join(gConfig["PGP_KEY_FOLDER"], email+".asc") 95 | keyfileExists, _ := exists(keyfileName) 96 | if !keyfileExists { 97 | 98 | key := publickey.GetKeyFromEmail(email, gConfig["PGP_KEYSERVER"], gConfig["PGP_KEYSERVER_QUERY"]) 99 | if key == "no keys found" { 100 | return key + " on keyserver " + gConfig["PGP_KEYSERVER"] + " from query " + gConfig["PGP_KEYSERVER"] + gConfig["PGP_KEYSERVER_QUERY"] + email 101 | } 102 | 103 | if key == "invalid host" { 104 | return gConfig["PGP_KEYSERVER"] + " is offline and your key has not previously been cached." 105 | } 106 | 107 | f, err := os.Create(keyfileName) 108 | if err != nil { 109 | fmt.Println(err) 110 | } 111 | n, err := io.WriteString(f, key) 112 | if err != nil { 113 | fmt.Println(n, err) 114 | } 115 | f.Close() 116 | } 117 | 118 | to, err := os.Open(keyfileName) 119 | logging.CheckFatal(err) 120 | 121 | defer to.Close() 122 | 123 | entitylist, err := openpgp.ReadArmoredKeyRing(to) 124 | 125 | buf := new(bytes.Buffer) 126 | w, _ := armor.Encode(buf, encryptionType, nil) 127 | plaintext, _ := openpgp.Encrypt(w, entitylist, nil, nil, nil) 128 | 129 | fmt.Fprintf(plaintext, input) 130 | plaintext.Close() 131 | w.Close() 132 | 133 | return buf.String() 134 | 135 | } 136 | 137 | func sendEmail(headers map[string]string, body string, address string, gConfig map[string]string) { 138 | 139 | host := gConfig["REMOTE_SMTP_HOST"] 140 | port := gConfig["REMOTE_SMTP_PORT"] 141 | user := gConfig["REMOTE_SMTP_USER"] 142 | password := gConfig["REMOTE_SMTP_PASS"] 143 | 144 | m := email.NewMessage(headers["Subject"], body) 145 | 146 | fromAddress := "Unknown" 147 | fromHeader, ok := headers["From"] 148 | if ok { 149 | from, _ := mail.ParseAddress(fromHeader) 150 | fromAddress = from.Address 151 | } 152 | 153 | to, _ := mail.ParseAddress(address) 154 | toAddress := to.Address 155 | 156 | m.From = fromAddress 157 | m.To = []string{toAddress} 158 | 159 | if gConfig["PGP_ATTACH_BODY"] == "Y" { 160 | m.AttachData("message.asc", []byte(body)) 161 | } 162 | 163 | err := email.Send(host+":"+port, smtp.PlainAuth("", user, password, host), m) 164 | logging.CheckFatal(err) 165 | } 166 | 167 | func exists(path string) (bool, error) { 168 | _, err := os.Stat(path) 169 | if err == nil { 170 | return true, nil 171 | } 172 | if os.IsNotExist(err) { 173 | return false, nil 174 | } 175 | return true, err 176 | } 177 | -------------------------------------------------------------------------------- /publickey/publickey.go: -------------------------------------------------------------------------------- 1 | package publickey 2 | 3 | import "golang.org/x/net/html" 4 | import "bytes" 5 | import "net/http" 6 | import "io/ioutil" 7 | import "strings" 8 | 9 | type HtmlOutput struct { 10 | body string 11 | code int 12 | err error 13 | } 14 | 15 | func CreateQueryURL(host string, query string, email string) string { 16 | return host + query + email 17 | } 18 | 19 | func GetLinksFromHTML(body string) []string { 20 | x := bytes.NewBufferString(body) 21 | 22 | z := html.NewTokenizer(x) 23 | 24 | links := []string{} 25 | 26 | for { 27 | tt := z.Next() 28 | 29 | switch { 30 | case tt == html.ErrorToken: 31 | // End of the document, we're done 32 | return links 33 | case tt == html.StartTagToken: 34 | t := z.Token() 35 | 36 | isAnchor := t.Data == "a" 37 | if isAnchor { 38 | for _, a := range t.Attr { 39 | if a.Key == "href" { 40 | links = append(links, a.Val) 41 | break 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | func FormatUrl(url string) string { 50 | if strings.HasPrefix(url, "http") { 51 | return url 52 | } else { 53 | return "http://" + url 54 | } 55 | } 56 | 57 | func DownloadFile(url string) HtmlOutput { 58 | resp, err := http.Get(FormatUrl(url)) 59 | 60 | if err != nil { 61 | return HtmlOutput{"", 404, err} 62 | } 63 | 64 | bytes, _ := ioutil.ReadAll(resp.Body) 65 | 66 | resp.Body.Close() 67 | 68 | return HtmlOutput{string(bytes), resp.StatusCode, nil} 69 | } 70 | 71 | func GetKeyFromEmail(email string, host string, query string) string { 72 | keyserverLink := CreateQueryURL(host, query, email) 73 | html := DownloadFile(keyserverLink) 74 | 75 | if html.err != nil { 76 | return "invalid host" 77 | } 78 | 79 | links := GetLinksFromHTML(html.body) 80 | if len(links) == 0 { 81 | return "no keys found" 82 | } 83 | 84 | keyLink := host + string(links[0]) 85 | 86 | return DownloadFile(keyLink).body 87 | } 88 | -------------------------------------------------------------------------------- /publickey/publickey_test.go: -------------------------------------------------------------------------------- 1 | package publickey 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | var keyserver_result = ` 9 | Search results for 'gmail elmundio1987 com' 10 | 11 |

Search results for 'gmail elmundio1987 com'

Type bits/keyID     Date       User ID
 17 | 

pub  4096R/0F0E5CA5 2014-11-16 Edmund Dipple <elmundio1987@gmail.com>
 18 | 
19 | ` 20 | 21 | var publicKey = ` 22 | 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 73 | dHQThJxttwBCU9N4xlwWMErinw== 74 | =S+zB` 75 | 76 | var keyserversTests = []struct { 77 | host string 78 | query string 79 | }{ 80 | {"pgp.mit.edu", "/pks/lookup?op=index&exact=on&search="}, 81 | {"sks-keyservers.net", "/pks/lookup?op=vindex&exact=on&search="}, 82 | } 83 | 84 | func TestCreateQueryURLAppendsParametersCorrectly(t *testing.T) { 85 | assert.Equal(t, CreateQueryURL("http://keys.pgp.net", "/get.pgp?search=", "elmundio1987@gmail.com"), "http://keys.pgp.net/get.pgp?search=elmundio1987@gmail.com", "") 86 | } 87 | 88 | func TestGetFirstLinkFromHTML(t *testing.T) { 89 | assert.Equal(t, GetLinksFromHTML(keyserver_result)[0], "/pks/lookup?op=get&search=0xC63AB6290F0E5CA5", "") 90 | } 91 | 92 | func TestDownloadKeyfile(t *testing.T) { 93 | 94 | assert.NotEqual(t, DownloadFile("https://pgp.mit.edu/pks/lookup?op=index&exact=on&search=elmundio1987@gmail.com"), "", "") 95 | } 96 | 97 | func TestGetKeyFromEmail(t *testing.T) { 98 | 99 | for _, tt := range keyserversTests { 100 | assert.Contains(t, GetKeyFromEmail("elmundio1987@gmail.com", tt.host, tt.query), publicKey, "") 101 | } 102 | 103 | } 104 | 105 | func TestGetKeyWhenNoProtocolProvided(t *testing.T) { 106 | assert.Contains(t, GetKeyFromEmail("elmundio1987@gmail.com", "pgp.mit.edu", "/pks/lookup?op=index&exact=on&search="), publicKey, "") 107 | } 108 | 109 | func TestGetKeyFromWrongEmail(t *testing.T) { 110 | assert.Equal(t, GetKeyFromEmail("elmundio1988@gmail.com", "https://pgp.mit.edu", "/pks/lookup?op=index&exact=on&search="), "no keys found", "") 111 | } 112 | 113 | func TestGetKeyWhenHostDown(t *testing.T) { 114 | assert.Equal(t, GetKeyFromEmail("elmundio1988@gmail.com", "https://pgp.mit.edu2", "/pks/lookup?op=index&exact=on&search="), "invalid host", "") 115 | } 116 | -------------------------------------------------------------------------------- /smtp.conf.sample: -------------------------------------------------------------------------------- 1 | { 2 | "REMOTE_SMTP_USER":"user@remotehost.com", 3 | "REMOTE_SMTP_PASS":"password", 4 | "REMOTE_SMTP_HOST":"smtp.remotehost.com", 5 | "REMOTE_SMTP_PORT":"25", 6 | "PGP_KEYSERVER":"pgp.mit.edu", 7 | "PGP_KEYSERVER_QUERY":"/pks/lookup?op=index&exact=on&search=", 8 | "PGP_KEY_FOLDER":"key_cache", 9 | "GM_ALLOWED_HOSTS":"localhost", 10 | "GM_PRIMARY_MAIL_HOST":"mail.example.com", 11 | "GSMTP_HOST_NAME":"mail.example.com", 12 | "GSMTP_LOG_FILE":"", 13 | "GSMTP_MAX_SIZE":"15728640", 14 | "GSMTP_PRV_KEY":"server.key", 15 | "GSMTP_PUB_KEY":"server.crt", 16 | "GSMTP_TIMEOUT":"100", 17 | "GSMTP_VERBOSE":"N", 18 | "GSTMP_LISTEN_INTERFACE":"127.0.0.1:25", 19 | "MAX_SMTP_CLIENTS":"10000", 20 | "GM_MAX_CLIENTS":"500", 21 | "NGINX_AUTH_ENABLED":"N", 22 | "NGINX_AUTH":"127.0.0.1:8025", 23 | "SGID":"508", 24 | "GUID":"504" 25 | } 26 | -------------------------------------------------------------------------------- /test_scripts/causecrash.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'net/smtp' 4 | 5 | filename = "smtp.conf.sample" 6 | # Read a file and encode it into base64 format 7 | filecontent = File.read(filename) 8 | encodedcontent = [filecontent].pack("m") # base64 9 | 10 | marker = "AUNIQUEMARKER" 11 | 12 | body =< 19 | To: A Test User 20 | Subject: Sending Attachement 21 | MIME-Version: 1.0 22 | Content-Type: multipart/mixed; boundary=#{marker} 23 | --#{marker} 24 | EOF 25 | 26 | # Define the message action 27 | part2 =< e 54 | print "Exception occured: " + e 55 | end -------------------------------------------------------------------------------- /test_scripts/sendattachment.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'net/smtp' 4 | 5 | filename = "smtp.conf.sample" 6 | # Read a file and encode it into base64 format 7 | filecontent = File.read(filename) 8 | encodedcontent = [filecontent].pack("m") # base64 9 | 10 | marker = "AUNIQUEMARKER" 11 | 12 | body =< 19 | To: A Test User 20 | Subject: Sending Attachement 21 | MIME-Version: 1.0 22 | Content-Type: multipart/mixed; boundary=#{marker} 23 | 24 | --#{marker} 25 | EOF 26 | 27 | # Define the message action 28 | part2 =< e 55 | print "Exception occured: " + e 56 | end -------------------------------------------------------------------------------- /test_scripts/sendmail.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'net/smtp' 4 | 5 | SMTPHOST = 'localhost' 6 | FROM = '"Your Email" ' 7 | 8 | def send(to, subject, message) 9 | body = <