├── VERSION ├── logo.jpg ├── vendor └── github.com │ └── ivpusic │ └── grpool │ ├── .travis.yml │ ├── LICENSE │ ├── README.md │ └── grpool.go ├── .gitignore ├── .travis.yml ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── main_test.go ├── README.md └── main.go /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.2 -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c1982/bomberman/HEAD/logo.jpg -------------------------------------------------------------------------------- /vendor/github.com/ivpusic/grpool/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - 1.6 6 | - 1.7 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.dll 3 | *.so 4 | *.dylib 5 | *.test 6 | *.out 7 | .glide/ 8 | bomberman* 9 | .vscode/launch.json 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: required 3 | go: 4 | - tip 5 | - "1.10" 6 | 7 | script: go test -v -covermode=atomic -coverprofile=coverage.out 8 | 9 | after_success: 10 | - bash <(curl -s https://codecov.io/bash) 11 | 12 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/ivpusic/grpool" 7 | packages = ["."] 8 | revision = "28957a27c9441f178d644aa598bac0aad4dd7bba" 9 | 10 | [solve-meta] 11 | analyzer-name = "dep" 12 | analyzer-version = 1 13 | inputs-digest = "710524a6a53be8101cbb590d7d39e86b5ae6fa9f08d30204c920cf4e26839d43" 14 | solver-name = "gps-cdcl" 15 | solver-version = 1 16 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/ivpusic/grpool" 31 | 32 | [prune] 33 | go-tests = true 34 | unused-packages = true 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Oğuzhan YILMAZ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/github.com/ivpusic/grpool/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ivan Pusic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/github.com/ivpusic/grpool/README.md: -------------------------------------------------------------------------------- 1 | # grpool 2 | [![Build Status](https://travis-ci.org/ivpusic/grpool.svg?branch=master)](https://travis-ci.org/ivpusic/grpool) 3 | 4 | Lightweight Goroutine pool 5 | 6 | Clients can submit jobs. Dispatcher takes job, and sends it to first available worker. 7 | When worker is done with processing job, will be returned back to worker pool. 8 | 9 | Number of workers and Job queue size is configurable. 10 | 11 | ## Docs 12 | https://godoc.org/github.com/ivpusic/grpool 13 | 14 | ## Installation 15 | ``` 16 | go get github.com/ivpusic/grpool 17 | ``` 18 | 19 | ## Simple example 20 | ```Go 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | "runtime" 26 | "time" 27 | 28 | "github.com/ivpusic/grpool" 29 | ) 30 | 31 | func main() { 32 | // number of workers, and size of job queue 33 | pool := grpool.NewPool(100, 50) 34 | 35 | // release resources used by pool 36 | defer pool.Release() 37 | 38 | // submit one or more jobs to pool 39 | for i := 0; i < 10; i++ { 40 | count := i 41 | 42 | pool.JobQueue <- func() { 43 | fmt.Printf("I am worker! Number %d\n", count) 44 | } 45 | } 46 | 47 | // dummy wait until jobs are finished 48 | time.Sleep(1 * time.Second) 49 | } 50 | ``` 51 | 52 | ## Example with waiting jobs to finish 53 | ```Go 54 | package main 55 | 56 | import ( 57 | "fmt" 58 | "runtime" 59 | 60 | "github.com/ivpusic/grpool" 61 | ) 62 | 63 | func main() { 64 | // number of workers, and size of job queue 65 | pool := grpool.NewPool(100, 50) 66 | defer pool.Release() 67 | 68 | // how many jobs we should wait 69 | pool.WaitCount(10) 70 | 71 | // submit one or more jobs to pool 72 | for i := 0; i < 10; i++ { 73 | count := i 74 | 75 | pool.JobQueue <- func() { 76 | // say that job is done, so we can know how many jobs are finished 77 | defer pool.JobDone() 78 | 79 | fmt.Printf("hello %d\n", count) 80 | } 81 | } 82 | 83 | // wait until we call JobDone for all jobs 84 | pool.WaitAll() 85 | } 86 | ``` 87 | 88 | ## License 89 | *MIT* 90 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func Test_sequental(t *testing.T) { 9 | 10 | assets := []struct { 11 | IPAddr string 12 | Index int 13 | }{ 14 | {"10.0.0.1", 0}, 15 | {"10.0.0.2", 1}, 16 | {"10.0.0.3", 2}, 17 | {"10.0.0.1", 3}, 18 | {"10.0.0.2", 4}, 19 | {"10.0.0.3", 5}, 20 | {"10.0.0.1", 6}, 21 | } 22 | 23 | list := []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"} 24 | 25 | for i := 0; i < len(assets); i++ { 26 | 27 | a := assets[i] 28 | ip := sequental(i, list) 29 | if ip != a.IPAddr { 30 | t.Error("invalid ip:", a.IPAddr) 31 | } 32 | } 33 | } 34 | 35 | func Test_getMetric(t *testing.T) { 36 | 37 | s := []map[string]time.Duration{} 38 | s = append(s, map[string]time.Duration{ 39 | "DATA": time.Second * 1, 40 | }) 41 | 42 | s = append(s, map[string]time.Duration{ 43 | "DATA": time.Second * 2, 44 | }) 45 | 46 | max, min, med := getMetric("DATA", s) 47 | 48 | if max != time.Second*1 { 49 | t.Error("max duration invalid") 50 | } 51 | 52 | if min != time.Second*2 { 53 | t.Error("min duration invalid") 54 | } 55 | 56 | if d, _ := time.ParseDuration("1.5s"); med != d { 57 | t.Error("med duration invalid") 58 | } 59 | } 60 | 61 | func Test_countMetric(t *testing.T) { 62 | 63 | s := []map[string]time.Duration{} 64 | s = append(s, map[string]time.Duration{ 65 | "DATA": time.Second * 1, 66 | }) 67 | 68 | s = append(s, map[string]time.Duration{ 69 | "DATA": time.Second * 2, 70 | }) 71 | 72 | s = append(s, map[string]time.Duration{ 73 | "QUIT": time.Second * 2, 74 | }) 75 | 76 | cnt := countMetric("DATA", s) 77 | 78 | if cnt != 2 { 79 | t.Error("Invalid key count:", cnt) 80 | } 81 | 82 | cnt = countMetric("QUIT", s) 83 | 84 | if cnt != 1 { 85 | t.Error("Invalid key count:", cnt) 86 | } 87 | } 88 | 89 | func Test_metricKeys(t *testing.T) { 90 | 91 | s := []map[string]time.Duration{} 92 | s = append(s, map[string]time.Duration{ 93 | "DATA": time.Second * 1, 94 | }) 95 | s = append(s, map[string]time.Duration{ 96 | "MAIL": time.Second * 1, 97 | }) 98 | s = append(s, map[string]time.Duration{ 99 | "QUIT": time.Second * 1, 100 | }) 101 | 102 | keys := metricKeys(s) 103 | 104 | if len(keys) != 3 { 105 | t.Error("invalid length") 106 | } 107 | } 108 | 109 | func Test_isContain(t *testing.T) { 110 | 111 | list := []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"} 112 | if !isContain("10.0.0.1", list) { 113 | t.Error("Key not found") 114 | } 115 | 116 | if isContain("10.0.0.4", list) { 117 | t.Error("Key found! oha.") 118 | } 119 | 120 | } 121 | 122 | //func Test_createBodyFixedSize(t *testing.T) { 123 | // body := createBodyFixedSize(10) 124 | // fmt.Println(body) 125 | // } 126 | -------------------------------------------------------------------------------- /vendor/github.com/ivpusic/grpool/grpool.go: -------------------------------------------------------------------------------- 1 | package grpool 2 | 3 | import "sync" 4 | 5 | // Gorouting instance which can accept client jobs 6 | type worker struct { 7 | workerPool chan *worker 8 | jobChannel chan Job 9 | stop chan struct{} 10 | } 11 | 12 | func (w *worker) start() { 13 | go func() { 14 | var job Job 15 | for { 16 | // worker free, add it to pool 17 | w.workerPool <- w 18 | 19 | select { 20 | case job = <-w.jobChannel: 21 | job() 22 | case <-w.stop: 23 | w.stop <- struct{}{} 24 | return 25 | } 26 | } 27 | }() 28 | } 29 | 30 | func newWorker(pool chan *worker) *worker { 31 | return &worker{ 32 | workerPool: pool, 33 | jobChannel: make(chan Job), 34 | stop: make(chan struct{}), 35 | } 36 | } 37 | 38 | // Accepts jobs from clients, and waits for first free worker to deliver job 39 | type dispatcher struct { 40 | workerPool chan *worker 41 | jobQueue chan Job 42 | stop chan struct{} 43 | } 44 | 45 | func (d *dispatcher) dispatch() { 46 | for { 47 | select { 48 | case job := <-d.jobQueue: 49 | worker := <-d.workerPool 50 | worker.jobChannel <- job 51 | case <-d.stop: 52 | for i := 0; i < cap(d.workerPool); i++ { 53 | worker := <-d.workerPool 54 | 55 | worker.stop <- struct{}{} 56 | <-worker.stop 57 | } 58 | 59 | d.stop <- struct{}{} 60 | return 61 | } 62 | } 63 | } 64 | 65 | func newDispatcher(workerPool chan *worker, jobQueue chan Job) *dispatcher { 66 | d := &dispatcher{ 67 | workerPool: workerPool, 68 | jobQueue: jobQueue, 69 | stop: make(chan struct{}), 70 | } 71 | 72 | for i := 0; i < cap(d.workerPool); i++ { 73 | worker := newWorker(d.workerPool) 74 | worker.start() 75 | } 76 | 77 | go d.dispatch() 78 | return d 79 | } 80 | 81 | // Represents user request, function which should be executed in some worker. 82 | type Job func() 83 | 84 | type Pool struct { 85 | JobQueue chan Job 86 | dispatcher *dispatcher 87 | wg sync.WaitGroup 88 | } 89 | 90 | // Will make pool of gorouting workers. 91 | // numWorkers - how many workers will be created for this pool 92 | // queueLen - how many jobs can we accept until we block 93 | // 94 | // Returned object contains JobQueue reference, which you can use to send job to pool. 95 | func NewPool(numWorkers int, jobQueueLen int) *Pool { 96 | jobQueue := make(chan Job, jobQueueLen) 97 | workerPool := make(chan *worker, numWorkers) 98 | 99 | pool := &Pool{ 100 | JobQueue: jobQueue, 101 | dispatcher: newDispatcher(workerPool, jobQueue), 102 | } 103 | 104 | return pool 105 | } 106 | 107 | // In case you are using WaitAll fn, you should call this method 108 | // every time your job is done. 109 | // 110 | // If you are not using WaitAll then we assume you have your own way of synchronizing. 111 | func (p *Pool) JobDone() { 112 | p.wg.Done() 113 | } 114 | 115 | // How many jobs we should wait when calling WaitAll. 116 | // It is using WaitGroup Add/Done/Wait 117 | func (p *Pool) WaitCount(count int) { 118 | p.wg.Add(count) 119 | } 120 | 121 | // Will wait for all jobs to finish. 122 | func (p *Pool) WaitAll() { 123 | p.wg.Wait() 124 | } 125 | 126 | // Will release resources used by pool 127 | func (p *Pool) Release() { 128 | p.dispatcher.stop <- struct{}{} 129 | <-p.dispatcher.stop 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bomberman 2 | SMTP Performance Test Tool 3 | 4 | [![Build Status](https://travis-ci.org/c1982/bomberman.svg?branch=master)](https://travis-ci.org/c1982/bomberman) [![Go Report Card](https://goreportcard.com/badge/github.com/c1982/bomberman)](https://goreportcard.com/report/github.com/c1982/bomberman) 5 | 6 | ![Bomberman Logo](https://github.com/c1982/bomberman/blob/master/logo.jpg?raw=true) 7 | 8 | ## Installation 9 | 10 | bomberman requires Go 1.11 or later. 11 | 12 | ``` 13 | $ go get github.com/c1982/bomberman 14 | ``` 15 | 16 | or 17 | 18 | [download](https://github.com/c1982/bomberman/releases) 19 | 20 | ## Flags 21 | 22 | | Flag | Desc | 23 | | ------------- |-------------| 24 | | host | Remote SMTP server with Port. Default: mail.server.com:25 | 25 | | from | From email address | 26 | | to | To email address| 27 | | subject | Email subject text | 28 | | size | Set email size Kilobytes (1024 Kilobyte = 1Mbyte). Default: 5Kb | 29 | | helo | SMTP client helo name. Default: mail.server.com | 30 | | count | Email message count. Default: 10| 31 | | workers | Thread workers for SMTP client. Default: 10 | 32 | | jobs | Job queue lenght in workers. Default: 10 | 33 | | outbound | Outbound IP address for SMTP client | 34 | | showerror | Print SMTP errors | 35 | | balance | Tool is use all IP address for outbound ip with sequental balance. Defalut: false | 36 | 37 | ## Server Configuration Checklist 38 | 39 | * Set SPF value in from email address domain. 40 | * Set PTR record your outbound IP addresses 41 | * Increase ulimit on your server (ulimit -n 10000) 42 | 43 | ## Usage 44 | 45 | Send 50 email to mail.server.com:25 50 workers 46 | 47 | ``` 48 | ./bomberman -host=mail.server.com:25 -from=test@mydomain.com -to=user@remotedomain.com -workers=50 -jobs=50 -count=50 -size=75 -balance 49 | ``` 50 | 51 | ## Output 52 | 53 | ``` 54 | Bomberman - SMTP Performance Test Tool 55 | -------------------------------------- 56 | Message Count : 1022 57 | Message Size : 75K 58 | Error : 168 59 | Start : 2018-10-12 06:42:56.808098931 +0300 EEST m=+0.000932257 60 | End : 2018-10-12 06:43:34.049561955 +0300 EEST m=+37.242392313 61 | Time : 37.241460056s 62 | 63 | Source IP Stats: 64 | 65 | 10.0.5.216 : 256 66 | 10.0.5.222 : 256 67 | 10.0.5.238 : 256 68 | 10.0.5.239 : 256 69 | 70 | Destination IP Stats: 71 | 72 | 5.4.0.248:25 : 856 73 | 74 | SMTP Commands: 75 | 76 | DATA (854) : min. 775.377638ms, max. 20.662139316s, med. 8.870254307s 77 | DIAL (1022) : min. 27.323µs, max. 6.000565014s, med. 1.511920428s 78 | HELO (854) : min. 34.061919ms, max. 3.80865823s, med. 343.306129ms 79 | MAIL (854) : min. 42.455906ms, max. 6.150506182s, med. 943.313477ms 80 | RCPT (854) : min. 34.972014ms, max. 3.151397545s, med. 497.683671ms 81 | SUCCESS (854) : min. 1.480909163s, max. 37.223728269s, med. 15.673002296s 82 | TOUCH (854) : min. 112.109537ms, max. 17.759899662s, med. 3.985871341s 83 | ``` 84 | 85 | ## Features 86 | 87 | * Linux/BSD/Windows supported. 88 | * SMTP RFC 5321 support 89 | * Outbount IP selection 90 | * SMTP Command duration min, max, mean metrics 91 | * Multi-thread support 92 | * Workers and Job Queue support 93 | * Balancing outbound ip address automatically 94 | * Set email body size of Kilobyte 95 | * Count and Report destination IP changes 96 | * Count and Report source IP changes 97 | 98 | ## Built With 99 | 100 | * [grpool](https://github.com/ivpusic/grpool) - Lightweight Goroutine pool 101 | * [net/smtp](https://golang.org/pkg/net/smtp/) - Golang SMTP Package 102 | 103 | ## Author 104 | 105 | * **Oğuzhan** - *MaestroPanel Tech Lead* - [c1982](https://github.com/c1982) 106 | 107 | ## ARO 108 | 109 | Cemil and Rıza and Osman ^_^ 110 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "math/rand" 9 | "net" 10 | "net/smtp" 11 | "os" 12 | "runtime" 13 | "sort" 14 | "strings" 15 | "time" 16 | 17 | "github.com/ivpusic/grpool" 18 | ) 19 | 20 | type stats struct { 21 | Durations []map[string]time.Duration 22 | SrcIPStats []string 23 | DstIPStats []string 24 | ErrorCnt int 25 | TotalCnt int 26 | } 27 | 28 | var ( 29 | metric stats 30 | host, from, to, subject, body, helo string 31 | workers, count, jobs, size, timeout int 32 | balance, showerror bool 33 | outbound string 34 | ) 35 | 36 | const ( 37 | metricTemplate = `` + 38 | `Bomberman - SMTP Performance Test Tool` + "\n" + 39 | `--------------------------------------` + "\n" + 40 | `Count : %d` + "\n" + 41 | `Error : %d` + "\n" + 42 | `Size : %dK` + "\n" + 43 | `Start : %v` + "\n" + 44 | `End : %v` + "\n" + 45 | `Time : %v` + "\n" 46 | 47 | bodyTemplate = `from: <%s>` + "\r\n" + 48 | `to: %s` + "\r\n" + 49 | `Subject: %s` + "\r\n\r\n" + 50 | `%s` 51 | 52 | dialTimeout = time.Second * 6 53 | letterIdxBits = 6 54 | letterIdxMask = 1< 1 { 126 | fmt.Println("") 127 | fmt.Println("Destination IP Stats:") 128 | fmt.Println("") 129 | printSlice(metric.DstIPStats, "%s\t: %d\n") 130 | } 131 | 132 | fmt.Println("") 133 | fmt.Println("SMTP Commands:") 134 | fmt.Println("") 135 | 136 | mkeys := metricKeys(metric.Durations) 137 | 138 | for i := 0; i < len(mkeys); i++ { 139 | m := mkeys[i] 140 | min, max, me := getMetric(m, metric.Durations) 141 | cnt := countMetric(m, metric.Durations) 142 | fmt.Printf("%s (%d)\t: min. %-15v, max. %-15v, med. %-15v\n", m, cnt, min, max, me) 143 | } 144 | fmt.Println("") 145 | } 146 | 147 | func start() { 148 | 149 | pool := grpool.NewPool(workers, jobs) 150 | defer pool.Release() 151 | pool.WaitCount(count) 152 | 153 | iplist, err := ipv4list() 154 | 155 | if err != nil { 156 | log.Fatal("pool not created:", err) 157 | } 158 | 159 | body = createBodyFixedSize(size) 160 | 161 | for i := 0; i < count; i++ { 162 | 163 | if balance { 164 | outbound = sequental(i, iplist) 165 | metric.SrcIPStats = append(metric.SrcIPStats, outbound) 166 | } 167 | 168 | pool.JobQueue <- func() { 169 | 170 | metric.TotalCnt++ 171 | 172 | defer pool.JobDone() 173 | 174 | durs, remoteip, err := sendMail(outbound, 175 | host, 176 | from, 177 | to, 178 | subject, 179 | body, 180 | helo) 181 | 182 | if err != nil { 183 | if showerror { 184 | fmt.Printf("%d: %v\n", metric.TotalCnt, err) 185 | } 186 | metric.ErrorCnt++ 187 | } 188 | 189 | if remoteip != "" { 190 | metric.DstIPStats = append(metric.DstIPStats, remoteip) 191 | } 192 | 193 | metric.Durations = append(metric.Durations, durs) 194 | } 195 | } 196 | 197 | pool.WaitAll() 198 | 199 | } 200 | 201 | func sendMail(outbound, smtpServer, from, to, subject, body, helo string) (metric map[string]time.Duration, remoteip string, err error) { 202 | 203 | var wc io.WriteCloser 204 | var msg string 205 | 206 | startTime := time.Now() 207 | 208 | metric = map[string]time.Duration{} 209 | host, _, _ := net.SplitHostPort(smtpServer) 210 | conn, err := newDialer(outbound, smtpServer, dialTimeout) 211 | 212 | if err != nil { 213 | err = fmt.Errorf("DIAL: %v (out:%s)", err, outbound) 214 | metric["DIAL"] = time.Now().Sub(startTime) 215 | return 216 | } 217 | 218 | remoteip = conn.RemoteAddr().String() //remoteip 219 | metric["DIAL"] = time.Now().Sub(startTime) 220 | 221 | newclientTime := time.Now() 222 | c, err := smtp.NewClient(conn, host) 223 | 224 | if err != nil { 225 | err = fmt.Errorf("TOUCH: %v", err) 226 | metric["TOUCH"] = time.Now().Sub(newclientTime) 227 | return 228 | } 229 | 230 | metric["TOUCH"] = time.Now().Sub(newclientTime) 231 | defer c.Close() 232 | 233 | helloTime := time.Now() 234 | err = c.Hello(helo) 235 | 236 | if err != nil { 237 | err = fmt.Errorf("HELO: %v", err) 238 | metric["HELO"] = time.Now().Sub(helloTime) 239 | 240 | return 241 | } 242 | 243 | metric["HELO"] = time.Now().Sub(helloTime) 244 | 245 | mailTime := time.Now() 246 | err = c.Mail(from) 247 | 248 | if err != nil { 249 | err = fmt.Errorf("MAIL: %v", err) 250 | metric["MAIL"] = time.Now().Sub(mailTime) 251 | 252 | return 253 | } 254 | 255 | metric["MAIL"] = time.Now().Sub(mailTime) 256 | 257 | rcptTime := time.Now() 258 | err = c.Rcpt(to) 259 | 260 | if err != nil { 261 | err = fmt.Errorf("RCPT: %v", err) 262 | metric["RCPT"] = time.Now().Sub(rcptTime) 263 | 264 | return 265 | } 266 | 267 | metric["RCPT"] = time.Now().Sub(rcptTime) 268 | 269 | dataTime := time.Now() 270 | 271 | msg = fmt.Sprintf(bodyTemplate, from, to, subject, body) 272 | wc, err = c.Data() 273 | 274 | if err != nil { 275 | err = fmt.Errorf("DATA: %v", err) 276 | metric["DATA"] = time.Now().Sub(dataTime) 277 | 278 | return 279 | } 280 | 281 | _, err = fmt.Fprintf(wc, msg) 282 | 283 | err = wc.Close() 284 | 285 | if err != nil { 286 | err = fmt.Errorf("DATA: %v", err) 287 | metric["DATA"] = time.Now().Sub(dataTime) 288 | 289 | return 290 | } 291 | 292 | metric["DATA"] = time.Now().Sub(dataTime) 293 | 294 | quitTime := time.Now() 295 | err = c.Quit() 296 | 297 | if err != nil { 298 | err = fmt.Errorf("QUIT: %v", err) 299 | metric["QUIT"] = time.Now().Sub(quitTime) 300 | 301 | return 302 | } 303 | 304 | metric["SUCCESS"] = time.Now().Sub(startTime) 305 | 306 | return 307 | } 308 | 309 | func getMetric(name string, metrics []map[string]time.Duration) (max, min, med time.Duration) { 310 | 311 | totaldur, _ := time.ParseDuration("0ms") 312 | list := []time.Duration{} 313 | 314 | for i := 0; i < len(metrics); i++ { 315 | m := metrics[i] 316 | 317 | if t, ok := m[name]; ok { 318 | totaldur += t 319 | list = append(list, t) 320 | } 321 | } 322 | 323 | sort.Slice(list, func(i, j int) bool { 324 | return list[i] > list[j] 325 | }) 326 | 327 | min = list[0] 328 | max = list[len(list)-1] 329 | med = totaldur / time.Duration(len(list)) 330 | 331 | return 332 | } 333 | 334 | func countMetric(name string, metrics []map[string]time.Duration) (cnt int) { 335 | 336 | for i := 0; i < len(metrics); i++ { 337 | m := metrics[i] 338 | 339 | for mkey := range m { 340 | 341 | if mkey == name { 342 | cnt++ 343 | } 344 | } 345 | } 346 | 347 | return 348 | } 349 | 350 | func metricKeys(metrics []map[string]time.Duration) (keys []string) { 351 | 352 | for i := 0; i < len(metrics); i++ { 353 | m := metrics[i] 354 | 355 | for mkey := range m { 356 | contain := isContain(mkey, keys) 357 | 358 | if !contain { 359 | keys = append(keys, mkey) 360 | } 361 | } 362 | } 363 | 364 | sort.Strings(keys) 365 | 366 | return 367 | } 368 | 369 | func isContain(key string, keys []string) bool { 370 | 371 | exists := false 372 | 373 | for z := 0; z < len(keys); z++ { 374 | if keys[z] == key { 375 | exists = true 376 | break 377 | } 378 | } 379 | 380 | return exists 381 | } 382 | 383 | func ipv4list() (iplist []string, err error) { 384 | 385 | addrs, err := net.InterfaceAddrs() 386 | 387 | if err != nil { 388 | return 389 | } 390 | 391 | for i := 0; i < len(addrs); i++ { 392 | 393 | addr := addrs[i].String() 394 | 395 | if strings.Contains(addr, ":") { 396 | continue 397 | } 398 | 399 | nt, _, err := net.ParseCIDR(addr) 400 | 401 | if err != nil { 402 | continue 403 | } 404 | 405 | if !nt.IsGlobalUnicast() { 406 | continue 407 | } 408 | 409 | iplist = append(iplist, nt.String()) 410 | } 411 | 412 | return 413 | } 414 | 415 | func newDialer(outboundip, remotehost string, timeout time.Duration) (conn net.Conn, err error) { 416 | 417 | if outboundip == "" { 418 | return net.Dial("tcp", remotehost) 419 | } 420 | 421 | dialer := &net.Dialer{Timeout: timeout} 422 | dialer.LocalAddr = &net.TCPAddr{IP: net.ParseIP(outboundip)} 423 | 424 | conn, err = dialer.Dial("tcp", remotehost) 425 | 426 | return 427 | } 428 | 429 | func sequental(index int, list []string) string { 430 | 431 | var ob string 432 | ln := len(list) 433 | 434 | if index < ln { 435 | ob = list[index] 436 | } else { 437 | li := index % ln 438 | ob = list[li] 439 | } 440 | 441 | return ob 442 | } 443 | 444 | func printSlice(list []string, format string) { 445 | 446 | m := map[string]int{} 447 | 448 | for i := 0; i < len(list); i++ { 449 | item := list[i] 450 | 451 | if item == "" { 452 | continue 453 | } 454 | 455 | if _, ok := m[item]; !ok { 456 | m[item] = 1 457 | } else { 458 | m[item] = m[item] + 1 459 | } 460 | } 461 | 462 | for k, v := range m { 463 | fmt.Printf(format, k, v) 464 | } 465 | } 466 | 467 | func createBodyFixedSize(n int) string { 468 | 469 | n = n * 1024 470 | 471 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 472 | 473 | var src = rand.NewSource(time.Now().UnixNano()) 474 | b := make([]byte, n) 475 | 476 | for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; { 477 | if remain == 0 { 478 | cache, remain = src.Int63(), letterIdxMax 479 | } 480 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 481 | b[i] = letterBytes[idx] 482 | i-- 483 | } 484 | cache >>= letterIdxBits 485 | remain-- 486 | } 487 | 488 | return string(b) 489 | } 490 | --------------------------------------------------------------------------------