├── AUTHORS ├── postmanq.zargo ├── recipient ├── status.go ├── code.go ├── service.go ├── recipient.go └── state.go ├── pubkey.pem ├── .gitignore ├── cmd ├── postmanq.go ├── server.go ├── pmq-report.go ├── lookup.go ├── pmq-grep.go ├── pmq-publish.go ├── client.php ├── client.go └── push_mails.go ├── consumer ├── waiter.go ├── assistant.go ├── sign.go ├── binding.go ├── service.go └── consumer.go ├── install.sh ├── limiter ├── cleaner.go ├── limit.go ├── limiter.go └── service.go ├── privkey.pem ├── application ├── report.go ├── grep.go ├── publish.go ├── post.go └── abstract.go ├── logger ├── writer.go ├── message.go └── service.go ├── analyser ├── aggregate.go ├── rows.go ├── detail.go ├── writer.go └── service.go ├── common ├── iterator.go ├── service.go ├── client.go ├── application.go ├── event.go ├── queue.go └── post.go ├── guardian ├── guardian.go └── service.go ├── LICENSE ├── connector ├── server.go ├── preparer.go ├── seeker.go ├── service.go └── connector.go ├── cert.pem ├── mailer ├── service.go └── mailer.go ├── config.yaml ├── grep └── service.go └── README.md /AUTHORS: -------------------------------------------------------------------------------- 1 | Alexey Solomonov 2 | -------------------------------------------------------------------------------- /postmanq.zargo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actionpay/postmanq/HEAD/postmanq.zargo -------------------------------------------------------------------------------- /recipient/status.go: -------------------------------------------------------------------------------- 1 | package recipient 2 | 3 | type StateStatuses chan StateStatus 4 | 5 | func (s StateStatuses) Add(status StateStatus) { 6 | go func() { 7 | s <- status 8 | }() 9 | } 10 | -------------------------------------------------------------------------------- /pubkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDL9stgc5Xe9QwR2ftWh/4yIknY 3 | Eyj6Ag4Ddhyr6rPTgKYXOkY9rHU2xor7vlFVIcBBqmPTLt0KuV1uDZK9x/Gi9gfH 4 | rmzCpTb2Cc4X27aQSnG7MIimbZwVsiFf2Y4njuElYRZhZjzTTA1gAJTOlkgMcYGE 5 | 9PQgbkTevem37X3EEwIDAQAB 6 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | 28 | test.go 29 | test/ 30 | 31 | -------------------------------------------------------------------------------- /cmd/postmanq.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/actionpay/postmanq/application" 7 | "github.com/actionpay/postmanq/common" 8 | ) 9 | 10 | func main() { 11 | var file string 12 | flag.StringVar(&file, "f", common.ExampleConfigYaml, "configuration yaml file") 13 | flag.Parse() 14 | 15 | app := application.NewPost() 16 | if app.IsValidConfigFilename(file) { 17 | app.SetConfigFilename(file) 18 | app.Run() 19 | } else { 20 | fmt.Printf("Usage: postmanq -f %s\n", common.ExampleConfigYaml) 21 | flag.VisitAll(common.PrintUsage) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "github.com/actionpay/postmanq/logger" 6 | "github.com/actionpay/postmanq/recipient" 7 | "runtime" 8 | ) 9 | 10 | func main() { 11 | common.DefaultWorkersCount = runtime.NumCPU() 12 | logger.Inst() 13 | 14 | conf := &recipient.Config{ 15 | ListenerCount: 10, 16 | MxHostnames: []string{"localhost"}, 17 | } 18 | service := recipient.Inst() 19 | service.(*recipient.Service).Configs = map[string]*recipient.Config{ 20 | "localhost": conf, 21 | } 22 | service.OnRun() 23 | 24 | ch := make(chan bool) 25 | <-ch 26 | } 27 | -------------------------------------------------------------------------------- /cmd/pmq-report.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/actionpay/postmanq/application" 7 | "github.com/actionpay/postmanq/common" 8 | ) 9 | 10 | func main() { 11 | var file string 12 | flag.StringVar(&file, "f", common.ExampleConfigYaml, "configuration yaml file") 13 | flag.Parse() 14 | 15 | app := application.NewReport() 16 | if app.IsValidConfigFilename(file) { 17 | app.SetConfigFilename(file) 18 | app.Run() 19 | } else { 20 | fmt.Println("Usage: pmq-report -f") 21 | flag.VisitAll(common.PrintUsage) 22 | fmt.Println("Example:") 23 | fmt.Printf(" pmq-report -f %s\n", common.ExampleConfigYaml) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /consumer/waiter.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // ожидающий 9 | type Waiter struct { 10 | *time.Ticker 11 | } 12 | 13 | // создает нового ожидающего 14 | func newWaiter() *Waiter { 15 | waiter := &Waiter{time.NewTicker(time.Millisecond * 250)} 16 | go waiter.run() 17 | return waiter 18 | } 19 | 20 | // запускает нового ожидающего 21 | func (w *Waiter) run() { 22 | commas := []string{ 23 | ". ", 24 | " . ", 25 | " .", 26 | } 27 | i := 0 28 | for { 29 | <-w.C 30 | fmt.Printf("\rgetting failure messages, please wait%s", commas[i]) 31 | if i == 2 { 32 | i = 0 33 | } else { 34 | i++ 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cmd/lookup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | func main() { 10 | //mxes, _ := net.LookupMX("gmail.com") 11 | mxes, _ := net.LookupMX("leeching.net") 12 | //for _, mx := range mxes { 13 | fmt.Println("-->", mxes[0].Host) 14 | serverName := seekRealServerName(mxes[0].Host) 15 | fmt.Println("<--", serverName) 16 | fmt.Println("---") 17 | //} 18 | } 19 | 20 | func seekRealServerName(hostname string) string { 21 | parts := strings.Split(hostname, ".") 22 | partsLen := len(parts) 23 | lookupHostname := strings.Join(parts[partsLen-3:partsLen-1], ".") 24 | mxes, err := net.LookupMX(lookupHostname) 25 | if err == nil { 26 | if strings.Contains(mxes[0].Host, lookupHostname) { 27 | return hostname 28 | } else { 29 | return seekRealServerName(mxes[0].Host) 30 | } 31 | } else { 32 | return hostname 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$(whoami)" != "root" ]] 4 | then 5 | echo "sorry, you are not root" 6 | exit 1 7 | fi 8 | 9 | if [[ -z "$(which git)" ]] 10 | then 11 | echo "git are not installed!" 12 | exit 2 13 | fi 14 | 15 | if [[ -z "$(which go)" ]] 16 | then 17 | echo "go are not installed!" 18 | exit 3 19 | fi 20 | 21 | BASE_PATH=`pwd` 22 | VERSION="v.3.1" 23 | export GOPATH="$BASE_PATH" 24 | export GOBIN="$BASE_PATH/bin/" 25 | go get -d github.com/actionpay/postmanq/cmd 26 | cd "$BASE_PATH/src/github.com/actionpay/postmanq" 27 | git checkout "$VERSION" 28 | go install cmd/postmanq.go 29 | go install cmd/pmq-grep.go 30 | go install cmd/pmq-publish.go 31 | go install cmd/pmq-report.go 32 | ln -s "$BASE_PATH/bin/postmanq" /usr/bin/ 33 | ln -s "$BASE_PATH/bin/pmq-grep" /usr/bin/ 34 | ln -s "$BASE_PATH/bin/pmq-publish" /usr/bin/ 35 | ln -s "$BASE_PATH/bin/pmq-report" /usr/bin/ -------------------------------------------------------------------------------- /limiter/cleaner.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | ) 7 | 8 | // чистильщик, проверяет значения ограничений и обнуляет значения ограничений 9 | type Cleaner struct{} 10 | 11 | // создает нового чистильщика 12 | func newCleaner() { 13 | new(Cleaner).clean() 14 | } 15 | 16 | // проверяет значения ограничений и обнуляет значения ограничений 17 | func (c *Cleaner) clean() { 18 | for now := range ticker.C { 19 | // смотрим все ограничения 20 | for _, conf := range service.Configs { 21 | for _, limit := range conf.Limits { 22 | // проверяем дату последнего изменения ограничения 23 | if !limit.isValidDuration(now) { 24 | // если дата последнего изменения выходит за промежуток времени для проверки 25 | // обнулям текущее количество отправленных писем 26 | atomic.StoreInt32(&limit.currentValue, 0) 27 | limit.modifyDate = time.Now() 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQDL9stgc5Xe9QwR2ftWh/4yIknYEyj6Ag4Ddhyr6rPTgKYXOkY9 3 | rHU2xor7vlFVIcBBqmPTLt0KuV1uDZK9x/Gi9gfHrmzCpTb2Cc4X27aQSnG7MIim 4 | bZwVsiFf2Y4njuElYRZhZjzTTA1gAJTOlkgMcYGE9PQgbkTevem37X3EEwIDAQAB 5 | AoGAfkPS1WvYnMTAaxCxyni3wVt+sVfzJwo8mKH2z+qk1ksvBvQZZbasfXNduMix 6 | 2uzg8wXDdInvZuMn1qhqbjgn+mokpgbSR9t/c73siJmp725CDxKoo8XzjIU2lF7E 7 | 3s4utLRqEBmHdpXLnJ9TBOBnSjOXR5atmwhCv0t83tYNtHECQQDqyMFxpW9fjnyS 8 | cX77kq5x1aLPCDiqosrBrVhuIQGv95Ol+4q0bR0zYLNbp3FBeSl38F8tGkJl+hEq 9 | a68jeJl7AkEA3mUUFbFoGKQhuXAA0VVtWd8uJuJgAJNYuluMkcoIYLknZsykjviB 10 | F2RxKTXz48cDqtdgPHA1nXD5feLW9C4ASQJBAOTkG1NESbQLIX9TjsyMT+1CrZrR 11 | FQ2l762p5ZBYNWDsKNGkzbDgv5sbJ0CvmUaPbNI1UVSTSTBJ/vowSWp3ZIkCQD84 12 | MJtyEQtgLQ4P3sujIzC3FuGK3IuNV12yhKU072i/eYnphqX10oyZyulSIwCPJGW+ 13 | T+ceZr9YzDOS9lP3d7ECQBaF274cmklcgdk4aB986xiPkgFVcQFhoMPLQRi0bSSF 14 | xd2xJrqnQOj8cre8SId4YpQeeIG1wWaPX4RMSo1AD7o= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /application/report.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/analyser" 5 | "github.com/actionpay/postmanq/common" 6 | "github.com/actionpay/postmanq/consumer" 7 | ) 8 | 9 | // приложение, анализирующее неотправленные сообщения 10 | type Report struct { 11 | Abstract 12 | } 13 | 14 | // создает новое приложение 15 | func NewReport() common.Application { 16 | return new(Report) 17 | } 18 | 19 | // запускает приложение 20 | func (r *Report) Run() { 21 | common.App = r 22 | common.Services = []interface{}{ 23 | analyser.Inst(), 24 | } 25 | r.services = []interface{}{ 26 | consumer.Inst(), 27 | analyser.Inst(), 28 | } 29 | r.run(r, common.NewApplicationEvent(common.InitApplicationEventKind)) 30 | } 31 | 32 | // запускает сервисы приложения 33 | func (r *Report) FireRun(event *common.ApplicationEvent, abstractService interface{}) { 34 | service := abstractService.(common.ReportService) 35 | go service.OnShowReport() 36 | } 37 | -------------------------------------------------------------------------------- /application/grep.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "github.com/actionpay/postmanq/grep" 6 | ) 7 | 8 | // приложение, ищущее логи по адресату или получателю 9 | type Grep struct { 10 | Abstract 11 | } 12 | 13 | // создает новое приложение 14 | func NewGrep() common.Application { 15 | return new(Grep) 16 | } 17 | 18 | // запускает приложение с аргументами 19 | func (g *Grep) RunWithArgs(args ...interface{}) { 20 | common.App = g 21 | g.services = []interface{}{ 22 | grep.Inst(), 23 | } 24 | 25 | event := common.NewApplicationEvent(common.InitApplicationEventKind) 26 | event.Args = make(map[string]interface{}) 27 | event.Args["envelope"] = args[0] 28 | event.Args["recipient"] = args[1] 29 | event.Args["numberLines"] = args[2] 30 | 31 | g.run(g, event) 32 | } 33 | 34 | // запускает сервисы приложения 35 | func (g *Grep) FireRun(event *common.ApplicationEvent, abstractService interface{}) { 36 | service := abstractService.(common.GrepService) 37 | go service.OnGrep(event) 38 | } 39 | -------------------------------------------------------------------------------- /logger/writer.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // автор логов 8 | type Writer interface { 9 | writeString(string) 10 | getLevel() Level 11 | } 12 | 13 | // автор логов пишущий в стандартный вывод 14 | type StdoutWriter struct{ 15 | // уровень логов, ниже этого уровня логи писаться не будут 16 | level Level 17 | } 18 | 19 | // пишет логи в стандартный вывод 20 | func (s *StdoutWriter) writeString(str string) { 21 | os.Stdout.WriteString(str) 22 | } 23 | 24 | func (s *StdoutWriter) getLevel() Level { 25 | return s.level 26 | } 27 | 28 | // автор логов пишущий в файл 29 | type FileWriter struct { 30 | filename string 31 | // уровень логов, ниже этого уровня логи писаться не будут 32 | level Level 33 | } 34 | 35 | // пишет логи в файл 36 | func (f *FileWriter) writeString(str string) { 37 | file, err := os.OpenFile(f.filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, os.ModePerm) 38 | if err == nil { 39 | _, err = file.WriteString(str) 40 | file.Close() 41 | } 42 | } 43 | 44 | func (f *FileWriter) getLevel() Level { 45 | return f.level 46 | } 47 | -------------------------------------------------------------------------------- /analyser/aggregate.go: -------------------------------------------------------------------------------- 1 | package analyser 2 | 3 | // автор таблиц, агрегирующий отчеты по ключу, например по коду ошибки 4 | type KeyAggregateTableWriter struct { 5 | *AbstractTableWriter 6 | } 7 | 8 | // создает нового автора таблицы, агрегирующего отчеты по ключу 9 | func newKeyAggregateTableWriter(fields []interface{}) TableWriter { 10 | return &KeyAggregateTableWriter{ 11 | newAbstractTableWriter(fields), 12 | } 13 | } 14 | 15 | // записывает данные в таблицу 16 | func (t *KeyAggregateTableWriter) Show() { 17 | t.Clean() 18 | for key, ids := range t.ids { 19 | t.AddRow(key, len(ids)) 20 | } 21 | t.Print() 22 | } 23 | 24 | // автор таблиц, агрегирующий данные 25 | type AggregateTableWriter struct { 26 | *AbstractTableWriter 27 | } 28 | 29 | // создает нового автора таблицы, агрегирующего данные 30 | func newAggregateTableWriter(fields []interface{}) TableWriter { 31 | return &AggregateTableWriter{ 32 | newAbstractTableWriter(fields), 33 | } 34 | } 35 | 36 | // записывает данные в таблицу 37 | func (a *AggregateTableWriter) Show() { 38 | a.Clean() 39 | for _, row := range a.rows { 40 | row.Write(a.Table, nil) 41 | } 42 | a.Print() 43 | } 44 | -------------------------------------------------------------------------------- /analyser/rows.go: -------------------------------------------------------------------------------- 1 | package analyser 2 | 3 | import ( 4 | "github.com/byorty/clitable" 5 | "regexp" 6 | "time" 7 | ) 8 | 9 | // отчет об ошибке 10 | type Report struct { 11 | // идентификатор 12 | Id int 13 | 14 | // отправитель 15 | Envelope string 16 | 17 | // получатель 18 | Recipient string 19 | 20 | // код ошибки 21 | Code int 22 | 23 | // сообщение об ошибке 24 | Message string 25 | 26 | // даты отправок 27 | CreatedDates []time.Time 28 | } 29 | 30 | // записывает отчет в таблицу 31 | func (r Report) Write(table *clitable.Table, valueRegex *regexp.Regexp) { 32 | if valueRegex == nil || 33 | (valueRegex != nil && 34 | (valueRegex.MatchString(r.Envelope) || 35 | valueRegex.MatchString(r.Recipient) || 36 | valueRegex.MatchString(r.Message))) { 37 | table.AddRow( 38 | r.Envelope, 39 | r.Recipient, 40 | r.Code, 41 | r.Message, 42 | len(r.CreatedDates), 43 | ) 44 | } 45 | } 46 | 47 | // агрегированная строка 48 | type AggregateRow []int 49 | 50 | // записывает строку в таблицу 51 | func (a AggregateRow) Write(table *clitable.Table, valueRegex *regexp.Regexp) { 52 | table.AddRow(a[0], a[1], a[2], a[3]) 53 | } 54 | -------------------------------------------------------------------------------- /cmd/pmq-grep.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/actionpay/postmanq/application" 7 | "github.com/actionpay/postmanq/common" 8 | ) 9 | 10 | func main() { 11 | var file, envelope, recipient string 12 | var numberLines int 13 | flag.StringVar(&file, "f", common.ExampleConfigYaml, "configuration yaml file") 14 | flag.StringVar(&envelope, "e", common.InvalidInputString, "necessary envelope") 15 | flag.StringVar(&recipient, "r", common.InvalidInputString, "necessary recipient") 16 | flag.Parse() 17 | 18 | app := application.NewGrep() 19 | if app.IsValidConfigFilename(file) && recipient != common.InvalidInputString { 20 | app.SetConfigFilename(file) 21 | app.RunWithArgs(envelope, recipient, numberLines) 22 | } else { 23 | fmt.Println("Usage: pmq-grep -f -r [-e]") 24 | flag.VisitAll(common.PrintUsage) 25 | fmt.Println("Example:") 26 | fmt.Printf(" pmq-grep -f %s -r mail@example.com\n", common.ExampleConfigYaml) 27 | fmt.Printf(" pmq-grep -f %s -r mail@example.com -n 1000\n", common.ExampleConfigYaml) 28 | fmt.Printf(" pmq-grep -f %s -r mail@example.com -e sender@mail.com\n", common.ExampleConfigYaml) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /application/publish.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "github.com/actionpay/postmanq/consumer" 6 | ) 7 | 8 | // приложение, перекладывающее письма из очереди в очередь 9 | type Publish struct { 10 | Abstract 11 | } 12 | 13 | // создает новое приложение 14 | func NewPublish() common.Application { 15 | return new(Publish) 16 | } 17 | 18 | // запускает приложение с аргументами 19 | func (p *Publish) RunWithArgs(args ...interface{}) { 20 | common.App = p 21 | p.services = []interface{}{ 22 | consumer.Inst(), 23 | } 24 | event := common.NewApplicationEvent(common.InitApplicationEventKind) 25 | event.Args = make(map[string]interface{}) 26 | event.Args["srcQueue"] = args[0] 27 | event.Args["destQueue"] = args[1] 28 | event.Args["host"] = args[2] 29 | event.Args["code"] = args[3] 30 | event.Args["envelope"] = args[4] 31 | event.Args["recipient"] = args[5] 32 | p.run(p, event) 33 | } 34 | 35 | // запускает сервисы приложения 36 | func (p *Publish) FireRun(event *common.ApplicationEvent, abstractService interface{}) { 37 | service := abstractService.(common.PublishService) 38 | go service.OnPublish(event) 39 | } 40 | -------------------------------------------------------------------------------- /common/iterator.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // итератор, используется для слабой связи между сервисами приложения 4 | type Iterator struct { 5 | // элементы 6 | items []interface{} 7 | 8 | // указатель на текущий элемент 9 | current int 10 | } 11 | 12 | // создает итератор 13 | func NewIterator(items []interface{}) *Iterator { 14 | return &Iterator{items: items, current: -1} 15 | } 16 | 17 | // отдает первый элемент 18 | func (i Iterator) First() interface{} { 19 | return i.items[0] 20 | } 21 | 22 | // отдает следующий элемент 23 | func (i *Iterator) Next() interface{} { 24 | var item interface{} 25 | i.current++ 26 | if i.isValidCurrent() { 27 | item = i.items[i.current] 28 | } 29 | return item 30 | } 31 | 32 | // проверяет, что указатель на элемент не превысил количества элементов 33 | func (i *Iterator) isValidCurrent() bool { 34 | return i.current < len(i.items) 35 | } 36 | 37 | // отдает текущий элемент 38 | func (i Iterator) Current() interface{} { 39 | var item interface{} 40 | if i.isValidCurrent() { 41 | item = i.items[i.current] 42 | } 43 | return item 44 | } 45 | 46 | // сигнализирует об окончании итерации 47 | func (i Iterator) IsDone() bool { 48 | return i.current >= len(i.items) 49 | } 50 | -------------------------------------------------------------------------------- /guardian/guardian.go: -------------------------------------------------------------------------------- 1 | package guardian 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "github.com/actionpay/postmanq/logger" 6 | ) 7 | 8 | // защитник, блокирует отправку на указанные почтовые сервисы 9 | type Guardian struct { 10 | // идентификатор для логов 11 | id int 12 | } 13 | 14 | // создает нового защитника 15 | func newGuardian(id int) { 16 | guardian := &Guardian{id} 17 | guardian.run() 18 | } 19 | 20 | // запускает прослушивание событий отправки писем 21 | func (g *Guardian) run() { 22 | for event := range events { 23 | g.guard(event) 24 | } 25 | } 26 | 27 | // блокирует отправку на указанные почтовые сервисы 28 | func (g *Guardian) guard(event *common.SendEvent) { 29 | logger.By(event.Message.HostnameFrom).Info("guardian#%d-%d check mail", g.id, event.Message.Id) 30 | 31 | excludes := service.getExcludes(event.Message.HostnameFrom) 32 | isExclude := false 33 | for _, exclude := range excludes { 34 | if exclude == event.Message.HostnameTo { 35 | isExclude = true 36 | break 37 | } 38 | } 39 | 40 | if isExclude { 41 | logger.By(event.Message.HostnameFrom).Debug("guardian#%d-%d detect postal worker - %s, revoke sending mail", g.id, event.Message.Id, event.Message.HostnameTo) 42 | event.Result <- common.RevokeSendEventResult 43 | } else { 44 | logger.By(event.Message.HostnameFrom).Debug("guardian#%d-%d continue sending mail", g.id, event.Message.Id) 45 | event.Iterator.Next().(common.SendingService).Events() <- event 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /analyser/detail.go: -------------------------------------------------------------------------------- 1 | package analyser 2 | 3 | import ( 4 | "fmt" 5 | "github.com/actionpay/postmanq/common" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | // автор таблиц, выводящий детализированные отчеты об ошибке 11 | type DetailTableWriter struct { 12 | *AbstractTableWriter 13 | } 14 | 15 | // создает нового автора таблицы, выводящего детализированные отчеты 16 | func newDetailTableWriter(fields []interface{}) TableWriter { 17 | return &DetailTableWriter{ 18 | newAbstractTableWriter(fields), 19 | } 20 | } 21 | 22 | // записывает данные в таблицу 23 | func (d *DetailTableWriter) Show() { 24 | d.Clean() 25 | keyRegex, _ := regexp.Compile(d.keyPattern) 26 | valueRegex := regexp.MustCompile(d.valuePattern) 27 | addresses := make([]string, 0) 28 | rows := 0 29 | for key, ids := range d.ids { 30 | if d.keyPattern == "*" || (keyRegex != nil && keyRegex.MatchString(key)) { 31 | for _, id := range ids { 32 | if d.offset == common.InvalidInputInt { 33 | if d.limit == common.InvalidInputInt || (d.limit > common.InvalidInputInt && rows < d.limit) { 34 | row := d.rows[id] 35 | row.Write(d.Table, valueRegex) 36 | if d.necessaryExport { 37 | addresses = append(addresses, row.(Report).Recipient) 38 | } 39 | rows++ 40 | } 41 | } else { 42 | d.offset-- 43 | } 44 | } 45 | } 46 | } 47 | d.Print() 48 | if d.necessaryExport { 49 | fmt.Println() 50 | fmt.Println("Addresses:") 51 | fmt.Println(strings.Join(addresses, ", ")) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /common/service.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // программа отправки почты получилась довольно сложной, т.к. она выполняет обработку и отправку писем, 4 | // работает с диском и с сетью, ведет логирование и проверяет ограничения перед отправкой 5 | // из - за такого насыщенного функционала, было принято решение разбить программу на логические части - сервисы 6 | // сервис - это модуль программы, отвечающий за выполнение одной конкретной задачи, например логирование 7 | // сервис может сам выполнять эту задачу, либо передавать выполнение задачи внутренним обработчикам 8 | 9 | // сервис требующий инициализиции 10 | // данные для инициализиции берутся из файла настроек 11 | type Service interface { 12 | OnInit(*ApplicationEvent) 13 | } 14 | 15 | // сервис получающий событие отправки письма 16 | // используется сервисами для передачи события друг другу 17 | type EventService interface { 18 | Events() chan *SendEvent 19 | } 20 | 21 | // сервис принимающий участие в отправке письма 22 | type SendingService interface { 23 | Service 24 | EventService 25 | OnRun() 26 | OnFinish() 27 | } 28 | 29 | // сервис принимающий участие в агрегации и выводе в консоль писем с ошибками 30 | type ReportService interface { 31 | Service 32 | EventService 33 | OnShowReport() 34 | } 35 | 36 | // сервис перекладывающий письма из очереди в очередь 37 | type PublishService interface { 38 | Service 39 | OnPublish(*ApplicationEvent) 40 | } 41 | 42 | // сервис ищущий записи в логе по письму 43 | type GrepService interface { 44 | Service 45 | OnGrep(*ApplicationEvent) 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Actionpay 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the Actionpay nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /cmd/pmq-publish.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/actionpay/postmanq/application" 7 | "github.com/actionpay/postmanq/common" 8 | ) 9 | 10 | func main() { 11 | var file, srcQueue, destQueue, host, envelope, recipient string 12 | var code int 13 | flag.StringVar(&file, "f", common.ExampleConfigYaml, "configuration yaml file") 14 | flag.StringVar(&srcQueue, "s", common.InvalidInputString, "source queue") 15 | flag.StringVar(&destQueue, "d", common.InvalidInputString, "destination queue") 16 | flag.StringVar(&host, "h", common.InvalidInputString, "amqp server hostname") 17 | flag.IntVar(&code, "c", common.InvalidInputInt, "error code") 18 | flag.StringVar(&envelope, "e", common.InvalidInputString, "necessary envelope") 19 | flag.StringVar(&recipient, "r", common.InvalidInputString, "necessary recipient") 20 | flag.Parse() 21 | 22 | app := application.NewPublish() 23 | if app.IsValidConfigFilename(file) && 24 | srcQueue != common.InvalidInputString && 25 | destQueue != common.InvalidInputString { 26 | app.SetConfigFilename(file) 27 | app.RunWithArgs(srcQueue, destQueue, host, code, envelope, recipient) 28 | } else { 29 | fmt.Println("Usage: pmq-publish -f -s -d [-h] [-c] [-e] [-r]") 30 | flag.VisitAll(common.PrintUsage) 31 | fmt.Println("Example:") 32 | fmt.Printf(" pmq-publish -f %s -s outbox.fail -d outbox\n", common.ExampleConfigYaml) 33 | fmt.Printf(" pmq-publish -f %s -s outbox.fail -d outbox -h example.com\n", common.ExampleConfigYaml) 34 | fmt.Printf(" pmq-publish -f %s -s outbox.fail -d outbox -c 554\n", common.ExampleConfigYaml) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /common/client.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net" 5 | "net/smtp" 6 | "time" 7 | ) 8 | 9 | // статус клиента почтового сервера 10 | type SmtpClientStatus int 11 | 12 | const ( 13 | // отсылает письмо 14 | WorkingSmtpClientStatus SmtpClientStatus = iota 15 | 16 | // ожидает письма 17 | WaitingSmtpClientStatus 18 | 19 | // отсоединен 20 | DisconnectedSmtpClientStatus 21 | ) 22 | 23 | // клиент почтового сервера 24 | type SmtpClient struct { 25 | // идертификатор клиента для удобства в логах 26 | Id int 27 | 28 | // соединение к почтовому серверу 29 | Conn net.Conn 30 | 31 | // реальный smtp клиент 32 | Worker *smtp.Client 33 | 34 | // дата создания или изменения статуса клиента 35 | ModifyDate time.Time 36 | 37 | // статус 38 | Status SmtpClientStatus 39 | 40 | // таймер, по истечении которого, соединение к почтовому сервису будет разорвано 41 | timer *time.Timer 42 | } 43 | 44 | // сстанавливайт таймаут на чтение и запись соединения 45 | func (s *SmtpClient) SetTimeout(timeout time.Duration) { 46 | s.Conn.SetDeadline(time.Now().Add(timeout)) 47 | } 48 | 49 | // переводит клиента в ожидание 50 | // после окончания ожидания соединение разрывается, а статус меняется на отсоединенный 51 | func (s *SmtpClient) Wait() { 52 | s.Status = WaitingSmtpClientStatus 53 | s.timer = time.AfterFunc(App.Timeout().Waiting, func() { 54 | s.Status = DisconnectedSmtpClientStatus 55 | s.Worker.Quit() 56 | s.timer = nil 57 | }) 58 | } 59 | 60 | // переводит клиента в рабочее состояние 61 | // если клиент был в ожидании, ожидание прерывается 62 | func (s *SmtpClient) Wakeup() { 63 | s.Status = WorkingSmtpClientStatus 64 | if s.timer != nil { 65 | s.timer.Stop() 66 | s.timer = nil 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /connector/server.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "net" 6 | ) 7 | 8 | // статус почтового сервис 9 | type MailServerStatus int 10 | 11 | const ( 12 | // по сервису ведется поиск информации 13 | LookupMailServerStatus MailServerStatus = iota 14 | 15 | // по сервису успешно собрана информация 16 | SuccessMailServerStatus 17 | 18 | // по сервису не удалось собрать информацию 19 | ErrorMailServerStatus 20 | ) 21 | 22 | // почтовый сервис 23 | type MailServer struct { 24 | // серверы почтового сервиса 25 | mxServers []*MxServer 26 | 27 | // номер потока, собирающего информацию о почтовом сервисе 28 | connectorId int 29 | 30 | // статус, говорящий о том, собранали ли информация о почтовом сервисе 31 | status MailServerStatus 32 | } 33 | 34 | // почтовый сервер 35 | type MxServer struct { 36 | // доменное имя почтового сервера 37 | hostname string 38 | 39 | // ip сервера 40 | ips []net.IP 41 | 42 | // клиенты сервера 43 | clients []*common.SmtpClient 44 | 45 | // А запись сервера 46 | realServerName string 47 | 48 | // использоватение TLS 49 | useTLS bool 50 | 51 | // очередь клиентов 52 | queues map[string]*common.LimitedQueue 53 | } 54 | 55 | // создает новый почтовый сервер 56 | func newMxServer(hostname, hostnameFrom string) *MxServer { 57 | queues := make(map[string]*common.LimitedQueue) 58 | for _, address := range service.getAddresses(hostnameFrom) { 59 | queues[address] = common.NewLimitQueue() 60 | } 61 | 62 | return &MxServer{ 63 | hostname: hostname, 64 | ips: make([]net.IP, 0), 65 | useTLS: true, 66 | queues: queues, 67 | } 68 | } 69 | 70 | // запрещает использовать TLS соединения 71 | func (m *MxServer) dontUseTLS() { 72 | m.useTLS = false 73 | } 74 | -------------------------------------------------------------------------------- /guardian/service.go: -------------------------------------------------------------------------------- 1 | package guardian 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "github.com/actionpay/postmanq/logger" 6 | yaml "gopkg.in/yaml.v2" 7 | ) 8 | 9 | var ( 10 | // сервис блокирующий отправку писем 11 | service *Service 12 | 13 | // канал для приема событий отправки писем 14 | events = make(chan *common.SendEvent) 15 | ) 16 | 17 | // сервис блокирующий отправку писем 18 | type Service struct { 19 | // количество горутин блокирующий отправку писем к почтовым сервисам 20 | GuardiansCount int `yaml:"workers"` 21 | 22 | Configs map[string]*Config `yaml:"postmans"` 23 | } 24 | 25 | // создает новый сервис блокировок 26 | func Inst() common.SendingService { 27 | if service == nil { 28 | service = new(Service) 29 | } 30 | return service 31 | } 32 | 33 | // инициализирует сервис блокировок 34 | func (s *Service) OnInit(event *common.ApplicationEvent) { 35 | logger.All().Debug("init guardians...") 36 | err := yaml.Unmarshal(event.Data, s) 37 | if err == nil { 38 | if s.GuardiansCount == 0 { 39 | s.GuardiansCount = common.DefaultWorkersCount 40 | } 41 | } else { 42 | logger.All().FailExitWithErr(err) 43 | } 44 | } 45 | 46 | // запускает горутины 47 | func (s *Service) OnRun() { 48 | for i := 0; i < s.GuardiansCount; i++ { 49 | go newGuardian(i + 1) 50 | } 51 | } 52 | 53 | // канал для приема событий отправки писем 54 | func (s *Service) Events() chan *common.SendEvent { 55 | return events 56 | } 57 | 58 | // завершает работу сервиса соединений 59 | func (s *Service) OnFinish() { 60 | close(events) 61 | } 62 | 63 | func (s Service) getExcludes(hostname string) []string { 64 | if conf, ok := s.Configs[hostname]; ok { 65 | return conf.Excludes 66 | } else { 67 | return common.EmptyStrSlice 68 | } 69 | } 70 | 71 | type Config struct { 72 | // хосты, на которую блокируется отправка писем 73 | Excludes []string `yaml:"exclude"` 74 | } 75 | -------------------------------------------------------------------------------- /connector/preparer.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/actionpay/postmanq/common" 7 | "github.com/actionpay/postmanq/logger" 8 | "time" 9 | ) 10 | 11 | // заготовщик, подготавливает событие соединения 12 | type Preparer struct { 13 | // Идентификатор для логов 14 | id int 15 | } 16 | 17 | // создает и запускает нового заготовщика 18 | func newPreparer(id int) { 19 | preparer := &Preparer{id} 20 | preparer.run() 21 | } 22 | 23 | // запускает прослушивание событий отправки писем 24 | func (p *Preparer) run() { 25 | for event := range events { 26 | p.prepare(event) 27 | } 28 | } 29 | 30 | // подготавливает и запускает событие создание соединения 31 | func (p *Preparer) prepare(event *common.SendEvent) { 32 | logger.By(event.Message.HostnameFrom).Info("preparer#%d-%d try create connection", p.id, event.Message.Id) 33 | 34 | connectionEvent := &ConnectionEvent{ 35 | SendEvent: event, 36 | servers: make(chan *MailServer, 1), 37 | connectorId: p.id, 38 | address: service.getAddress(event.Message.HostnameFrom, p.id), 39 | } 40 | goto connectToMailServer 41 | 42 | connectToMailServer: 43 | // отправляем событие сбора информации о сервере 44 | seekerEvents <- connectionEvent 45 | server := <-connectionEvent.servers 46 | switch server.status { 47 | case LookupMailServerStatus: 48 | goto waitLookup 49 | case SuccessMailServerStatus: 50 | connectionEvent.server = server 51 | connectorEvents <- connectionEvent 52 | case ErrorMailServerStatus: 53 | common.ReturnMail( 54 | event, 55 | errors.New(fmt.Sprintf("511 preparer#%d-%d can't lookup %s", p.id, event.Message.Id, event.Message.HostnameTo)), 56 | ) 57 | } 58 | return 59 | 60 | waitLookup: 61 | logger.By(event.Message.HostnameFrom).Debug("preparer#%d-%d wait ending look up mail server %s...", p.id, event.Message.Id, event.Message.HostnameTo) 62 | time.Sleep(common.App.Timeout().Sleep) 63 | goto connectToMailServer 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /limiter/limit.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "time" 6 | ) 7 | 8 | // тип ограничения 9 | type Kind string 10 | 11 | const ( 12 | SecondKind Kind = "second" 13 | MinuteKind = "minute" 14 | HourKind = "hour" 15 | DayKind = "day" 16 | ) 17 | 18 | var ( 19 | // возможные промежутки времени для каждого ограничения 20 | limitDurations = map[Kind]time.Duration{ 21 | SecondKind: time.Second, 22 | MinuteKind: time.Minute, 23 | HourKind: time.Hour, 24 | DayKind: time.Hour * 24, 25 | } 26 | // очереди для каждого ограничения 27 | limitBindingTypes = map[Kind]common.DelayedBindingType{ 28 | SecondKind: common.SecondDelayedBinding, 29 | MinuteKind: common.MinuteDelayedBinding, 30 | HourKind: common.HourDelayedBinding, 31 | DayKind: common.DayDelayedBinding, 32 | } 33 | ) 34 | 35 | // ограничение 36 | type Limit struct { 37 | // максимально допустимое количество писем 38 | Value int32 `json:"value"` 39 | 40 | // тип ограничения 41 | Kind Kind `json:"type"` 42 | 43 | // текущее количество писем 44 | currentValue int32 45 | 46 | // промежуток времени, за который проверяется количество отправленных писем 47 | duration time.Duration 48 | 49 | // дата последнего обнуления количества отправленных писем 50 | modifyDate time.Time 51 | 52 | // тип очереди, в которую необходимо положить письмо, если превышено количество отправленных писем 53 | bindingType common.DelayedBindingType 54 | } 55 | 56 | // инициализирует значения по умолчанию 57 | func (l *Limit) init() { 58 | if duration, ok := limitDurations[l.Kind]; ok { 59 | l.duration = duration 60 | } 61 | if bindingType, ok := limitBindingTypes[l.Kind]; ok { 62 | l.bindingType = bindingType 63 | } 64 | } 65 | 66 | // сигнализирует о том, что надо ли обнулять текущее количество отправленных писем 67 | // если вернулось true, текущее количество отправленных писем не обнуляется 68 | // если вернулось false, текущее количество отправленных писем обнуляется 69 | func (l *Limit) isValidDuration(now time.Time) bool { 70 | return now.Sub(l.modifyDate) <= l.duration 71 | } 72 | -------------------------------------------------------------------------------- /application/post.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "github.com/actionpay/postmanq/connector" 6 | "github.com/actionpay/postmanq/consumer" 7 | "github.com/actionpay/postmanq/guardian" 8 | "github.com/actionpay/postmanq/limiter" 9 | "github.com/actionpay/postmanq/logger" 10 | "github.com/actionpay/postmanq/mailer" 11 | "github.com/actionpay/postmanq/recipient" 12 | yaml "gopkg.in/yaml.v2" 13 | "runtime" 14 | ) 15 | 16 | // приложение, рассылающее письма 17 | type Post struct { 18 | Abstract 19 | 20 | // количество отправителей 21 | Workers int `yaml:"workers"` 22 | } 23 | 24 | // создает новое приложение 25 | func NewPost() common.Application { 26 | return new(Post) 27 | } 28 | 29 | // запускает приложение 30 | func (p *Post) Run() { 31 | common.App = p 32 | common.Services = []interface{}{ 33 | guardian.Inst(), 34 | limiter.Inst(), 35 | connector.Inst(), 36 | mailer.Inst(), 37 | } 38 | p.services = []interface{}{ 39 | logger.Inst(), 40 | consumer.Inst(), 41 | guardian.Inst(), 42 | limiter.Inst(), 43 | connector.Inst(), 44 | mailer.Inst(), 45 | recipient.Inst(), 46 | } 47 | p.run(p, common.NewApplicationEvent(common.InitApplicationEventKind)) 48 | } 49 | 50 | // инициализирует приложение 51 | func (p *Post) Init(event *common.ApplicationEvent) { 52 | // получаем настройки 53 | err := yaml.Unmarshal(event.Data, p) 54 | if err == nil { 55 | p.CommonTimeout.Init() 56 | common.DefaultWorkersCount = p.Workers 57 | runtime.GOMAXPROCS(common.DefaultWorkersCount * 2) 58 | logger.All().Debug("app workers count %d", p.Workers) 59 | } else { 60 | logger.All().FailExitWithErr(err) 61 | } 62 | } 63 | 64 | // запускает сервисы приложения 65 | func (p *Post) FireRun(event *common.ApplicationEvent, abstractService interface{}) { 66 | service := abstractService.(common.SendingService) 67 | go service.OnRun() 68 | } 69 | 70 | // останавливает сервисы приложения 71 | func (p *Post) FireFinish(event *common.ApplicationEvent, abstractService interface{}) { 72 | service := abstractService.(common.SendingService) 73 | go service.OnFinish() 74 | } 75 | -------------------------------------------------------------------------------- /logger/message.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "runtime/debug" 6 | ) 7 | 8 | // запись логирования 9 | type Message struct { 10 | Hostname string 11 | // сообщение для лога, может содержать параметры 12 | Message string 13 | 14 | // уровень логирования записи, необходим для отсечения лишних записей 15 | Level Level 16 | 17 | // аргументы для параметров сообщения 18 | Args []interface{} 19 | 20 | Stack []byte 21 | } 22 | 23 | // созадние новой записи логирования 24 | func NewMessage(level Level, message string, args ...interface{}) *Message { 25 | logMessage := new(Message) 26 | logMessage.Level = level 27 | logMessage.Message = message 28 | logMessage.Args = args 29 | return logMessage 30 | } 31 | 32 | func All() *Message { 33 | return By(common.AllDomains) 34 | } 35 | 36 | func By(hostname string) *Message { 37 | return &Message{ 38 | Hostname: hostname, 39 | } 40 | } 41 | 42 | func (m *Message) log(message string, necessaryLevel Level, args ...interface{}) { 43 | m.Message = message 44 | m.Level = necessaryLevel 45 | m.Args = args 46 | 47 | if necessaryLevel > InfoLevel || necessaryLevel == DebugLevel { 48 | m.Stack = debug.Stack() 49 | } 50 | 51 | messages <- m 52 | } 53 | 54 | // пишет ошибку в лог 55 | func (m *Message) Err(message string, args ...interface{}) { 56 | go m.log(message, ErrorLevel, args...) 57 | } 58 | 59 | // пишет произвольную ошибку в лог и завершает программу 60 | func (m *Message) FailExit(message string, args ...interface{}) { 61 | m.Err(message, args...) 62 | common.App.Events() <- common.NewApplicationEvent(common.FinishApplicationEventKind) 63 | } 64 | 65 | // пишет системную ошибку в лог и завершает программу 66 | func (m *Message) FailExitWithErr(err error) { 67 | m.FailExit("%v", err) 68 | } 69 | 70 | // пишет произвольное предупреждение 71 | func (m *Message) Warn(message string, args ...interface{}) { 72 | go m.log(message, WarningLevel, args...) 73 | } 74 | 75 | // пишет системное предупреждение 76 | func (m *Message) WarnWithErr(err error) { 77 | m.Warn("%v", err) 78 | } 79 | 80 | // пишет информационное сообщение 81 | func (m *Message) Info(message string, args ...interface{}) { 82 | go m.log(message, InfoLevel, args...) 83 | } 84 | 85 | // пишет сообщение для отладки 86 | func (m *Message) Debug(message string, args ...interface{}) { 87 | go m.log(message, DebugLevel, args...) 88 | } 89 | -------------------------------------------------------------------------------- /common/application.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "regexp" 7 | "runtime" 8 | ) 9 | 10 | const ( 11 | // используется в примерах использования 12 | ExampleConfigYaml = "/path/to/config/file.yaml" 13 | 14 | // невалидная строка, введенная пользователем 15 | InvalidInputString = "" 16 | 17 | // невалидное число, введенное пользователем 18 | InvalidInputInt = 0 19 | ) 20 | 21 | var ( 22 | // объект текущего приложения, иногда необходим сервисам, для отправки событий приложению 23 | App Application 24 | 25 | // сервисы, используются для создания итератора 26 | Services []interface{} 27 | 28 | // количество goroutine, может измениться для инициализации приложения 29 | DefaultWorkersCount = runtime.NumCPU() 30 | 31 | // используется в нескольких пакетах, поэтому вынес сюда 32 | FilenameRegex = regexp.MustCompile(`[^\\/]+\.[^\\/]+`) 33 | 34 | // печает аргументы, используемые приложением 35 | PrintUsage = func(f *flag.Flag) { 36 | format := " -%s %s\n" 37 | fmt.Printf(format, f.Name, f.Usage) 38 | } 39 | ) 40 | 41 | // проект содержит несколько приложений: pmq-grep, pmq-publish, pmq-report, postmanq и т.д. 42 | // чтобы упростить и стандартизировать приложения, разработан этот интерфейс 43 | type Application interface { 44 | GetConfigFilename() string 45 | // устанавливает путь к файлу с настройками 46 | SetConfigFilename(string) 47 | 48 | // проверяет валидность пути к файлу с настройками 49 | IsValidConfigFilename(string) bool 50 | 51 | // устанавливает канал событий приложения 52 | SetEvents(chan *ApplicationEvent) 53 | 54 | // возвращает канал событий приложения 55 | Events() chan *ApplicationEvent 56 | 57 | // устанавливает канал завершения приложения 58 | SetDone(chan bool) 59 | 60 | // возвращает канал завершения приложения 61 | Done() chan bool 62 | 63 | // возвращает сервисы, используемые приложением 64 | Services() []interface{} 65 | 66 | // инициализирует сервисы 67 | FireInit(*ApplicationEvent, interface{}) 68 | 69 | // запускает сервисы приложения 70 | FireRun(*ApplicationEvent, interface{}) 71 | 72 | // останавливает сервисы приложения 73 | FireFinish(*ApplicationEvent, interface{}) 74 | 75 | // инициализирует приложение 76 | Init(*ApplicationEvent) 77 | 78 | // запускает приложение 79 | Run() 80 | 81 | // запускает приложение с аргументами 82 | RunWithArgs(...interface{}) 83 | 84 | // возвращает таймауты приложения 85 | Timeout() Timeout 86 | } 87 | -------------------------------------------------------------------------------- /limiter/limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "github.com/actionpay/postmanq/logger" 6 | "sync/atomic" 7 | ) 8 | 9 | // ограничитель, проверяет количество отправленных писем почтовому сервису 10 | type Limiter struct { 11 | // идентификатор для логов 12 | id int 13 | } 14 | 15 | // создает нового ограничителя 16 | func newLimiter(id int) { 17 | limiter := &Limiter{id} 18 | limiter.run() 19 | } 20 | 21 | // запускает ограничителя 22 | func (l *Limiter) run() { 23 | for event := range events { 24 | l.check(event) 25 | } 26 | } 27 | 28 | // проверяет количество отправленных писем почтовому сервису 29 | // если количество превышено, отправляет письмо в отложенную очередь 30 | func (l *Limiter) check(event *common.SendEvent) { 31 | logger.By(event.Message.HostnameFrom).Info("limiter#%d-%d check limit for mail", l.id, event.Message.Id) 32 | limit := service.getLimit(event.Message.HostnameFrom, event.Message.HostnameTo) 33 | // пытаемся найти ограничения для почтового сервиса 34 | if limit == nil { 35 | logger.By(event.Message.HostnameFrom).Debug("limiter#%d-%d not found limit for %s", l.id, event.Message.Id, event.Message.HostnameTo) 36 | } else { 37 | logger.By(event.Message.HostnameFrom).Debug("limiter#%d-%d found limit for %s", l.id, event.Message.Id, event.Message.HostnameTo) 38 | // если оно нашлось, проверяем, что отправка нового письма происходит в тот промежуток времени, 39 | // в который нам необходимо следить за ограничениями 40 | if limit.isValidDuration(event.Message.CreatedDate) { 41 | atomic.AddInt32(&limit.currentValue, 1) 42 | currentValue := atomic.LoadInt32(&limit.currentValue) 43 | logger.By(event.Message.HostnameFrom).Debug("limiter#%d-%d detect current value %d, const value %d", l.id, event.Message.Id, currentValue, limit.Value) 44 | // если ограничение превышено 45 | if currentValue > limit.Value { 46 | logger.By(event.Message.HostnameFrom).Debug("limiter#%d-%d current value is exceeded for %s", l.id, event.Message.Id, event.Message.HostnameTo) 47 | // определяем очередь, в которое переложем письмо 48 | event.Message.BindingType = limit.bindingType 49 | // говорим получателю, что у нас превышение ограничения, 50 | // разблокируем поток получателя 51 | event.Result <- common.OverlimitSendEventResult 52 | return 53 | } 54 | } else { 55 | logger.By(event.Message.HostnameFrom).Debug("limiter#%d-%d duration great then %v", l.id, event.Message.Id, limit.duration) 56 | } 57 | } 58 | event.Iterator.Next().(common.SendingService).Events() <- event 59 | } 60 | -------------------------------------------------------------------------------- /limiter/service.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "github.com/actionpay/postmanq/logger" 6 | yaml "gopkg.in/yaml.v2" 7 | "time" 8 | ) 9 | 10 | var ( 11 | // сервис ограничений 12 | service *Service 13 | 14 | // таймер, работает каждую секунду 15 | ticker *time.Ticker 16 | 17 | // канал для приема событий отправки писем 18 | events = make(chan *common.SendEvent) 19 | ) 20 | 21 | // сервис ограничений, следит за тем, чтобы почтовым сервисам не отправилось больше писем, чем нужно 22 | type Service struct { 23 | // количество горутин проверяющих количество отправленных писем 24 | LimitersCount int `yaml:"workers"` 25 | 26 | Configs map[string]*Config `yaml:"postmans"` 27 | } 28 | 29 | // создает сервис ограничений 30 | func Inst() common.SendingService { 31 | if service == nil { 32 | service = new(Service) 33 | ticker = time.NewTicker(time.Second) 34 | } 35 | return service 36 | } 37 | 38 | // инициализирует сервис 39 | func (s *Service) OnInit(event *common.ApplicationEvent) { 40 | logger.All().Debug("init limits...") 41 | err := yaml.Unmarshal(event.Data, s) 42 | if err == nil { 43 | for name, config := range s.Configs { 44 | s.init(config, name) 45 | } 46 | if s.LimitersCount == 0 { 47 | s.LimitersCount = common.DefaultWorkersCount 48 | } 49 | } else { 50 | logger.All().FailExitWithErr(err) 51 | } 52 | } 53 | 54 | func (s *Service) init(conf *Config, hostname string) { 55 | // инициализируем ограничения 56 | for host, limit := range conf.Limits { 57 | limit.init() 58 | logger.By(hostname).Debug("create limit for %s with type %v and duration %v", host, limit.bindingType, limit.duration) 59 | } 60 | } 61 | 62 | // запускает проверку ограничений и очистку значений лимитов 63 | func (s *Service) OnRun() { 64 | // сразу запускаем проверку значений ограничений 65 | go newCleaner() 66 | for i := 0; i < s.LimitersCount; i++ { 67 | go newLimiter(i + 1) 68 | } 69 | } 70 | 71 | // канал для приема событий отправки писем 72 | func (s *Service) Events() chan *common.SendEvent { 73 | return events 74 | } 75 | 76 | // завершает работу сервиса соединений 77 | func (s *Service) OnFinish() { 78 | close(events) 79 | } 80 | 81 | func (s Service) getLimit(hostnameFrom, hostnameTo string) *Limit { 82 | if config, ok := service.Configs[hostnameFrom]; ok { 83 | if limit, has := config.Limits[hostnameTo]; has { 84 | return limit 85 | } else { 86 | return nil 87 | } 88 | } else { 89 | return nil 90 | } 91 | } 92 | 93 | type Config struct { 94 | // ограничения для почтовых сервисов, в качестве ключа используется домен 95 | Limits map[string]*Limit `yaml:"limits"` 96 | } 97 | -------------------------------------------------------------------------------- /common/event.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "time" 4 | 5 | // тип события приложения 6 | type ApplicationEventKind int 7 | 8 | const ( 9 | // инициализации сервисов 10 | InitApplicationEventKind ApplicationEventKind = iota 11 | 12 | // запуск сервисов 13 | RunApplicationEventKind 14 | 15 | // завершение сервисов 16 | FinishApplicationEventKind 17 | ) 18 | 19 | // событие приложения 20 | type ApplicationEvent struct { 21 | // тип события 22 | Kind ApplicationEventKind 23 | 24 | // данные из файла настроек 25 | Data []byte 26 | 27 | // аргументы командной строки 28 | Args map[string]interface{} 29 | } 30 | 31 | // возвращает аргумент, как булевый тип 32 | func (e *ApplicationEvent) GetBoolArg(key string) bool { 33 | return e.Args[key].(bool) 34 | } 35 | 36 | // возвращает аргумент, как число 37 | func (e *ApplicationEvent) GetIntArg(key string) int { 38 | return e.Args[key].(int) 39 | } 40 | 41 | // возвращает аргумент, как строку 42 | func (e *ApplicationEvent) GetStringArg(key string) string { 43 | return e.Args[key].(string) 44 | } 45 | 46 | // создает событие с указанным типом 47 | func NewApplicationEvent(kind ApplicationEventKind) *ApplicationEvent { 48 | return &ApplicationEvent{Kind: kind} 49 | } 50 | 51 | // результат отправки письма 52 | type SendEventResult int 53 | 54 | const ( 55 | // успех 56 | SuccessSendEventResult SendEventResult = iota 57 | 58 | // превышение лимита 59 | OverlimitSendEventResult 60 | 61 | // ошибка 62 | ErrorSendEventResult 63 | 64 | // повторная отправка через некоторое время 65 | DelaySendEventResult 66 | 67 | // отмена отправки 68 | RevokeSendEventResult 69 | ) 70 | 71 | // событие отправки письма 72 | type SendEvent struct { 73 | // елиент для отправки писем 74 | Client *SmtpClient 75 | 76 | // письмо, полученное из очереди 77 | Message *MailMessage 78 | 79 | // дата создания необходима при получении подключения к почтовому сервису 80 | CreateDate time.Time 81 | 82 | // результат 83 | Result chan SendEventResult 84 | 85 | // количество попыток отправок письма 86 | TryCount int 87 | 88 | // итератор сервисов, участвующих в отправке письма 89 | Iterator *Iterator 90 | 91 | // очередь, в которую необходимо будет положить клиента после отправки письма 92 | Queue *LimitedQueue 93 | } 94 | 95 | // создает событие отправки сообщения 96 | func NewSendEvent(message *MailMessage) *SendEvent { 97 | event := new(SendEvent) 98 | event.Message = message 99 | event.CreateDate = time.Now() 100 | event.Result = make(chan SendEventResult) 101 | event.Iterator = NewIterator(Services) 102 | return event 103 | } 104 | -------------------------------------------------------------------------------- /recipient/code.go: -------------------------------------------------------------------------------- 1 | package recipient 2 | 3 | import "fmt" 4 | 5 | type Code int 6 | 7 | func (c Code) GetName() string { 8 | return codeMessages[c] 9 | } 10 | 11 | func (c Code) GetFormattedName() string { 12 | return fmt.Sprintf(c.GetName(), c) 13 | } 14 | 15 | const ( 16 | StatusCode Code = 211 17 | HelpCode Code = 214 18 | ReadyCode Code = 220 19 | CloseCode Code = 221 20 | CompleteCode Code = 250 21 | ForwardCode Code = 251 22 | AttemptDeliveryCode Code = 252 23 | StartInputCode Code = 354 24 | NotAvailableCode Code = 421 25 | MailboxUnavailableCode Code = 450 26 | AbortedCode Code = 451 27 | NotTakenCode Code = 452 28 | UnableAcceptParamsCode Code = 455 29 | SyntaxErrorCode Code = 500 30 | SyntaxParamErrorCode Code = 501 31 | NotImplementedCode Code = 502 32 | BadSequenceCode Code = 503 33 | ParamNotImplementedCode Code = 504 34 | UserNotFoundCode Code = 550 35 | UserNotLocalCode Code = 551 36 | ExceededStorageCode Code = 552 37 | NameNotAllowedCode Code = 553 38 | TransactionFailedCode Code = 554 39 | ParamsNotRecognizedCode Code = 555 40 | ) 41 | 42 | var ( 43 | codeMessages = map[Code]string{ 44 | StatusCode: "%d System status or system help reply", 45 | HelpCode: "%d Help message", 46 | ReadyCode: "%d %s Service ready", 47 | CloseCode: "%d %s Service closing transmission channel", 48 | CompleteCode: "%d Requested mail action okay, completed", 49 | ForwardCode: "%d User not local", 50 | AttemptDeliveryCode: "%d Cannot VRFY user, but will accept message and attempt delivery", 51 | StartInputCode: "%d Start mail input", 52 | NotAvailableCode: "%d %s Service not available, closing transmission channel", 53 | MailboxUnavailableCode: "%d Requested mail action not taken: mailbox unavailable", 54 | AbortedCode: "%d Requested action aborted: error in processing", 55 | NotTakenCode: "%d Requested action not taken: insufficient system storage", 56 | UnableAcceptParamsCode: "%d Server unable to accommodate parameters", 57 | SyntaxErrorCode: "%d Syntax error, command unrecognized", 58 | SyntaxParamErrorCode: "%d Syntax error in parameters or arguments", 59 | NotImplementedCode: "%d Command not implemented", 60 | BadSequenceCode: "%d Bad sequence of commands", 61 | ParamNotImplementedCode: "%d Command parameter not implemented", 62 | UserNotFoundCode: "%d Requested action not taken: mailbox unavailable", 63 | UserNotLocalCode: "%d User not local", 64 | ExceededStorageCode: "%d Requested mail action aborted: exceeded storage allocation", 65 | NameNotAllowedCode: "%d Requested action not taken: mailbox name not allowed", 66 | TransactionFailedCode: "%d Transaction failed", 67 | ParamsNotRecognizedCode: "%d MAIL FROM/RCPT TO parameters not recognized or not implemented", 68 | } 69 | ) 70 | -------------------------------------------------------------------------------- /cert.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 14197731656133188432 (0xc50879b7ec231b50) 5 | Signature Algorithm: sha1WithRSAEncryption 6 | Issuer: C=RU, ST=Russia, O=PostmanQ, OU=IT, CN=adnwb.ru/emailAddress=byorty@mail.ru 7 | Validity 8 | Not Before: Dec 9 20:44:43 2014 GMT 9 | Not After : Dec 9 20:44:43 2015 GMT 10 | Subject: C=RU, ST=Russia, O=PostmanQ, OU=IT, CN=adnwb.ru/emailAddress=byorty@mail.ru 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | Public-Key: (1024 bit) 14 | Modulus: 15 | 00:cb:f6:cb:60:73:95:de:f5:0c:11:d9:fb:56:87: 16 | fe:32:22:49:d8:13:28:fa:02:0e:03:76:1c:ab:ea: 17 | b3:d3:80:a6:17:3a:46:3d:ac:75:36:c6:8a:fb:be: 18 | 51:55:21:c0:41:aa:63:d3:2e:dd:0a:b9:5d:6e:0d: 19 | 92:bd:c7:f1:a2:f6:07:c7:ae:6c:c2:a5:36:f6:09: 20 | ce:17:db:b6:90:4a:71:bb:30:88:a6:6d:9c:15:b2: 21 | 21:5f:d9:8e:27:8e:e1:25:61:16:61:66:3c:d3:4c: 22 | 0d:60:00:94:ce:96:48:0c:71:81:84:f4:f4:20:6e: 23 | 44:de:bd:e9:b7:ed:7d:c4:13 24 | Exponent: 65537 (0x10001) 25 | X509v3 extensions: 26 | X509v3 Basic Constraints: 27 | CA:FALSE 28 | Netscape Comment: 29 | OpenSSL Generated Certificate 30 | X509v3 Subject Key Identifier: 31 | 46:96:DB:C2:AD:87:9D:2A:A1:B7:C3:3E:AB:1E:68:29:B7:47:92:7E 32 | X509v3 Authority Key Identifier: 33 | keyid:17:D4:6F:0D:40:35:6A:A0:9F:1D:77:A7:E7:A5:D5:A3:EC:D1:BF:EF 34 | 35 | Signature Algorithm: sha1WithRSAEncryption 36 | 2f:db:8d:64:b5:2c:b2:ac:6b:ba:ab:d2:2a:4c:66:01:e7:8a: 37 | 40:0d:3c:21:7d:20:56:9c:36:95:86:29:0f:5e:14:97:3b:8e: 38 | d6:7c:ef:af:5c:22:56:7b:4e:5e:3b:e1:a9:c6:37:e8:2e:ed: 39 | 1f:d1:56:d7:39:52:c1:4e:3a:04:db:78:98:3d:c0:fc:46:29: 40 | d2:d9:a8:c9:a2:50:c3:26:ef:ee:1b:f7:cb:65:89:12:d1:12: 41 | 90:e7:c9:83:aa:e1:a1:db:24:7d:4c:06:d2:3b:d2:19:e3:de: 42 | b3:d3:17:4a:90:cf:2f:59:1f:0d:30:f1:34:70:bb:bc:c0:60: 43 | 8c:cd 44 | -----BEGIN CERTIFICATE----- 45 | MIIC2TCCAkKgAwIBAgIJAMUIebfsIxtQMA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV 46 | BAYTAlJVMQ8wDQYDVQQIDAZSdXNzaWExETAPBgNVBAoMCFBvc3RtYW5RMQswCQYD 47 | VQQLDAJJVDERMA8GA1UEAwwIYWRud2IucnUxHTAbBgkqhkiG9w0BCQEWDmJ5b3J0 48 | eUBtYWlsLnJ1MB4XDTE0MTIwOTIwNDQ0M1oXDTE1MTIwOTIwNDQ0M1owcDELMAkG 49 | A1UEBhMCUlUxDzANBgNVBAgMBlJ1c3NpYTERMA8GA1UECgwIUG9zdG1hblExCzAJ 50 | BgNVBAsMAklUMREwDwYDVQQDDAhhZG53Yi5ydTEdMBsGCSqGSIb3DQEJARYOYnlv 51 | cnR5QG1haWwucnUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMv2y2Bzld71 52 | DBHZ+1aH/jIiSdgTKPoCDgN2HKvqs9OAphc6Rj2sdTbGivu+UVUhwEGqY9Mu3Qq5 53 | XW4Nkr3H8aL2B8eubMKlNvYJzhfbtpBKcbswiKZtnBWyIV/ZjieO4SVhFmFmPNNM 54 | DWAAlM6WSAxxgYT09CBuRN696bftfcQTAgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJ 55 | YIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1Ud 56 | DgQWBBRGltvCrYedKqG3wz6rHmgpt0eSfjAfBgNVHSMEGDAWgBQX1G8NQDVqoJ8d 57 | d6fnpdWj7NG/7zANBgkqhkiG9w0BAQUFAAOBgQAv241ktSyyrGu6q9IqTGYB54pA 58 | DTwhfSBWnDaVhikPXhSXO47WfO+vXCJWe05eO+GpxjfoLu0f0VbXOVLBTjoE23iY 59 | PcD8RinS2ajJolDDJu/uG/fLZYkS0RKQ58mDquGh2yR9TAbSO9IZ496z0xdKkM8v 60 | WR8NMPE0cLu8wGCMzQ== 61 | -----END CERTIFICATE----- 62 | -------------------------------------------------------------------------------- /cmd/client.php: -------------------------------------------------------------------------------- 1 | \r\n"; 19 | $SEND .= "To: $mail_to <$mail_to>\r\n"; 20 | $SEND .= "X-Priority: 3\r\n\r\n"; 21 | } 22 | $SEND .= $message."\r\n"; 23 | if( !$socket = fsockopen($config['smtp_host'], $config['smtp_port'], $errno, $errstr, 30) ) { 24 | if ($config['smtp_debug']) echo $errno."
".$errstr; 25 | return false; 26 | } 27 | 28 | if (!server_parse($socket, "220", __LINE__)) return false; 29 | 30 | fputs($socket, "HELO " . $config['smtp_host'] . "\r\n"); 31 | if (!server_parse($socket, "250", __LINE__)) { 32 | if ($config['smtp_debug']) echo '

Не могу отправить HELO!

'; 33 | fclose($socket); 34 | return false; 35 | } 36 | fputs($socket, "MAIL FROM:<".$config['smtp_from'].">\r\n"); 37 | if (!server_parse($socket, "250", __LINE__)) { 38 | if ($config['smtp_debug']) echo '

Не могу отправить комманду MAIL FROM:

'; 39 | fclose($socket); 40 | return false; 41 | } 42 | fputs($socket, "RCPT TO:<" . $mail_to . ">\r\n"); 43 | 44 | if (!server_parse($socket, "250", __LINE__)) { 45 | if ($config['smtp_debug']) echo '

Не могу отправить комманду RCPT TO:

'; 46 | fclose($socket); 47 | return false; 48 | } 49 | fputs($socket, "DATA\r\n"); 50 | 51 | if (!server_parse($socket, "354", __LINE__)) { 52 | if ($config['smtp_debug']) echo '

Не могу отправить комманду DATA

'; 53 | fclose($socket); 54 | return false; 55 | } 56 | fputs($socket, $SEND."\r\n.\r\n"); 57 | 58 | if (!server_parse($socket, "250", __LINE__)) { 59 | if ($config['smtp_debug']) echo '

Не смог отправить тело письма. Письмо не было отправленно!

'; 60 | fclose($socket); 61 | return false; 62 | } 63 | fputs($socket, "QUIT\r\n"); 64 | fclose($socket); 65 | return TRUE; 66 | } 67 | 68 | function server_parse($socket, $response, $line = __LINE__) { 69 | global $config; 70 | while (@substr($server_response, 3, 1) != ' ') { 71 | if (!($server_response = fgets($socket, 256))) { 72 | if ($config['smtp_debug']) echo "

Проблемы с отправкой почты!

$response
$line
"; 73 | return false; 74 | } 75 | } 76 | echo $server_response, PHP_EOL; 77 | if (!(substr($server_response, 0, 3) == $response)) { 78 | if ($config['smtp_debug']) echo "

Проблемы с отправкой почты!

$response
$line
"; 79 | return false; 80 | } 81 | return true; 82 | } 83 | 84 | smtpmail('recipient@example.com', 'test', 'test message'); 85 | ?> -------------------------------------------------------------------------------- /analyser/writer.go: -------------------------------------------------------------------------------- 1 | package analyser 2 | 3 | import ( 4 | "github.com/byorty/clitable" 5 | "regexp" 6 | "sort" 7 | ) 8 | 9 | // автор таблиц 10 | type TableWriter interface { 11 | 12 | // добавляет идентификатора по ключу 13 | Add(string, int) 14 | 15 | // экспортирует данные от одного автора другому 16 | Export(TableWriter) 17 | 18 | // возвращает идентификатору по ключу 19 | Ids() map[string][]int 20 | 21 | // устанавливает регулярное выражение для ключей 22 | SetKeyPattern(string) 23 | 24 | // устанавливает лимит 25 | SetLimit(int) 26 | 27 | // сигнализирует, нужен ли список email-ов после таблицы 28 | SetNecessaryExport(bool) 29 | 30 | // устанавливает сдвиг 31 | SetOffset(int) 32 | 33 | // устанавливает строки для вывода в таблице 34 | SetRows(RowWriters) 35 | 36 | // устанавливает регулярное выражение для значения строки таблицы 37 | SetValuePattern(string) 38 | 39 | // выводит ьаблицу 40 | Show() 41 | } 42 | 43 | // строки таблицы 44 | type RowWriters map[int]RowWriter 45 | 46 | // строка таблицы 47 | type RowWriter interface { 48 | 49 | // записывает строку в таблицу, если строка удовлетворяет регулярному выражению 50 | Write(*clitable.Table, *regexp.Regexp) 51 | } 52 | 53 | // базовый автор таблицы 54 | type AbstractTableWriter struct { 55 | *clitable.Table 56 | ids map[string][]int 57 | keyPattern string 58 | limit int 59 | necessaryExport bool 60 | offset int 61 | rows RowWriters 62 | valuePattern string 63 | } 64 | 65 | // создает базовый автор таблицы 66 | func newAbstractTableWriter(fields []interface{}) *AbstractTableWriter { 67 | return &AbstractTableWriter{ 68 | Table: clitable.NewTable(fields...), 69 | ids: make(map[string][]int), 70 | } 71 | } 72 | 73 | // добавляет идентификатора по ключу 74 | func (a *AbstractTableWriter) Add(key string, id int) { 75 | if _, ok := a.ids[key]; !ok { 76 | a.ids[key] = make([]int, 0) 77 | } 78 | idsLen := len(a.ids[key]) 79 | if sort.Search(idsLen, func(i int) bool { return a.ids[key][i] >= id }) == idsLen { 80 | a.ids[key] = append(a.ids[key], id) 81 | } 82 | } 83 | 84 | // экспортирует данные от одного автора другому 85 | func (a *AbstractTableWriter) Export(writer TableWriter) { 86 | a.ids = writer.Ids() 87 | } 88 | 89 | // возвращает идентификатору по ключу 90 | func (a *AbstractTableWriter) Ids() map[string][]int { 91 | return a.ids 92 | } 93 | 94 | // устанавливает регулярное выражение для ключей 95 | func (a *AbstractTableWriter) SetKeyPattern(pattern string) { 96 | a.keyPattern = pattern 97 | } 98 | 99 | // устанавливает лимит 100 | func (a *AbstractTableWriter) SetLimit(limit int) { 101 | a.limit = limit 102 | } 103 | 104 | // сигнализирует, нужен ли список email-ов после таблицы 105 | func (a *AbstractTableWriter) SetNecessaryExport(necessaryExport bool) { 106 | a.necessaryExport = necessaryExport 107 | } 108 | 109 | // устанавливает сдвиг 110 | func (a *AbstractTableWriter) SetOffset(offset int) { 111 | a.offset = offset 112 | } 113 | 114 | // устанавливает строки для вывода в таблице 115 | func (a *AbstractTableWriter) SetRows(rows RowWriters) { 116 | a.rows = rows 117 | } 118 | 119 | // устанавливает регулярное выражение для значения строки таблицы 120 | func (a *AbstractTableWriter) SetValuePattern(pattern string) { 121 | a.valuePattern = pattern 122 | } 123 | -------------------------------------------------------------------------------- /connector/seeker.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/logger" 5 | "net" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | var ( 11 | seekerEvents = make(chan *ConnectionEvent) 12 | // семафор, необходим для поиска MX серверов 13 | seekerMutex = new(sync.Mutex) 14 | ) 15 | 16 | // искатель, ищет информацию о сервере 17 | type Seeker struct { 18 | // Идентификатор для логов 19 | id int 20 | } 21 | 22 | // создает и запускает нового искателя 23 | func newSeeker(id int) { 24 | seeker := &Seeker{id} 25 | seeker.run() 26 | } 27 | 28 | // запускает прослушивание событий поиска информации о сервере 29 | func (s *Seeker) run() { 30 | for event := range seekerEvents { 31 | s.seek(event) 32 | } 33 | } 34 | 35 | // ищет информацию о сервере 36 | func (s *Seeker) seek(event *ConnectionEvent) { 37 | hostnameTo := event.Message.HostnameTo 38 | // добавляем новый почтовый домен 39 | seekerMutex.Lock() 40 | if _, ok := mailServers[hostnameTo]; !ok { 41 | logger.By(event.Message.HostnameFrom).Debug("seeker#%d-%d create mail server for %s", event.connectorId, event.Message.Id, hostnameTo) 42 | mailServers[hostnameTo] = &MailServer{ 43 | status: LookupMailServerStatus, 44 | connectorId: event.connectorId, 45 | } 46 | } 47 | seekerMutex.Unlock() 48 | mailServer := mailServers[hostnameTo] 49 | // если пришло несколько несколько писем на один почтовый сервис, 50 | // и информация о сервисе еще не собрана, 51 | // то таким образом блокируем повторную попытку собрать инфомацию о почтовом сервисе 52 | if event.connectorId == mailServer.connectorId && mailServer.status == LookupMailServerStatus { 53 | logger.By(event.Message.HostnameFrom).Debug("seeker#%d-%d look up mx domains for %s...", s.id, event.Message.Id, hostnameTo) 54 | mailServer := mailServers[hostnameTo] 55 | // ищем почтовые сервера для домена 56 | mxes, err := net.LookupMX(hostnameTo) 57 | if err == nil { 58 | mailServer.mxServers = make([]*MxServer, len(mxes)) 59 | for i, mx := range mxes { 60 | mxHostname := strings.TrimRight(mx.Host, ".") 61 | logger.By(event.Message.HostnameFrom).Debug("seeker#%d-%d look up mx domain %s for %s", s.id, event.Message.Id, mxHostname, hostnameTo) 62 | mxServer := newMxServer(mxHostname, event.Message.HostnameFrom) 63 | mxServer.realServerName = s.seekRealServerName(mx.Host) 64 | logger.By(event.Message.HostnameFrom).Debug("seeker#%d-%d look up detect real server name %s", s.id, event.Message.Id, mxServer.realServerName) 65 | mailServer.mxServers[i] = mxServer 66 | } 67 | mailServer.status = SuccessMailServerStatus 68 | logger.By(event.Message.HostnameFrom).Debug("seeker#%d-%d look up %s success", s.id, event.Message.Id, hostnameTo) 69 | } else { 70 | mailServer.status = ErrorMailServerStatus 71 | logger.By(event.Message.HostnameFrom).Warn("seeker#%d-%d can't look up mx domains for %s", s.id, event.Message.Id, hostnameTo) 72 | } 73 | } 74 | event.servers <- mailServer 75 | } 76 | 77 | func (s *Seeker) seekRealServerName(hostname string) string { 78 | parts := strings.Split(hostname, ".") 79 | partsLen := len(parts) 80 | hostname = strings.Join(parts[partsLen-3:partsLen-1], ".") 81 | mxes, err := net.LookupMX(hostname) 82 | if err == nil { 83 | if strings.Contains(mxes[0].Host, hostname) { 84 | return hostname 85 | } else { 86 | return s.seekRealServerName(mxes[0].Host) 87 | } 88 | } else { 89 | return hostname 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /recipient/service.go: -------------------------------------------------------------------------------- 1 | package recipient 2 | 3 | import ( 4 | "fmt" 5 | "github.com/actionpay/postmanq/common" 6 | "github.com/actionpay/postmanq/logger" 7 | yaml "gopkg.in/yaml.v2" 8 | "net" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | service *Service 14 | ) 15 | 16 | func Inst() common.SendingService { 17 | if service == nil { 18 | service = new(Service) 19 | } 20 | return service 21 | } 22 | 23 | type Config struct { 24 | ListenerCount int `yaml:"listenerCount"` 25 | Inbox string `yaml:"inbox"` 26 | MxHostnames []string 27 | } 28 | 29 | type Event struct { 30 | serverHostname string 31 | serverMxHostname string 32 | clientHostname []byte 33 | clientAddr net.Addr 34 | conn *net.TCPConn 35 | message *common.MailMessage 36 | } 37 | 38 | type Service struct { 39 | Configs map[string]*Config `yaml:"postmans"` 40 | } 41 | 42 | func (s *Service) OnInit(event *common.ApplicationEvent) { 43 | err := yaml.Unmarshal(event.Data, s) 44 | if err == nil { 45 | for name, config := range s.Configs { 46 | s.init(config, name) 47 | } 48 | } else { 49 | logger.All().FailExitWithErr(err) 50 | } 51 | } 52 | 53 | func (s *Service) init(conf *Config, hostname string) { 54 | mxes, err := net.LookupMX(hostname) 55 | if err == nil { 56 | conf.MxHostnames = make([]string, len(mxes)) 57 | for i, mx := range mxes { 58 | conf.MxHostnames[i] = strings.TrimRight(mx.Host, ".") 59 | } 60 | if conf.ListenerCount == 0 { 61 | conf.ListenerCount = common.DefaultWorkersCount 62 | } 63 | } else { 64 | logger.By(hostname).FailExit("recipient service - can't lookup mx for %s", hostname) 65 | } 66 | } 67 | 68 | func (s *Service) OnRun() { 69 | for hostname, conf := range s.Configs { 70 | for _, mxHostname := range conf.MxHostnames { 71 | tcpAddr := fmt.Sprintf("%s:2225", mxHostname) 72 | addr, err := net.ResolveTCPAddr("tcp", tcpAddr) 73 | if err == nil { 74 | logger.By(hostname).Info("recipient service - resolve %s success", tcpAddr) 75 | listener, err := net.ListenTCP("tcp", addr) 76 | if err == nil { 77 | logger.By(hostname).Info("recipient service - listen %s success", tcpAddr) 78 | go s.run(hostname, mxHostname, conf, listener) 79 | } else { 80 | logger.By(hostname).Warn("recipient service - can't listen %s, error - %v", tcpAddr, err) 81 | } 82 | } else { 83 | logger.By(hostname).Warn("recipient service - can't resolve %s, error - %v", tcpAddr, err) 84 | } 85 | } 86 | } 87 | } 88 | 89 | func (s *Service) run(hostname, mxHostname string, conf *Config, listener *net.TCPListener) { 90 | events := make(chan *Event) 91 | for i := 0; i < conf.ListenerCount; i++ { 92 | go newRecipient(i, events) 93 | } 94 | for { 95 | conn, err := listener.AcceptTCP() 96 | if err == nil { 97 | logger.By(hostname).Info("recipient service - accept %s success", hostname) 98 | events <- &Event{ 99 | serverHostname: hostname, 100 | serverMxHostname: mxHostname, 101 | clientAddr: conn.RemoteAddr(), 102 | conn: conn, 103 | } 104 | } else { 105 | logger.By(hostname).Warn("recipient service - can't accept %s, error - %v", hostname, err) 106 | } 107 | } 108 | } 109 | 110 | func (s *Service) Events() chan *common.SendEvent { 111 | return nil 112 | } 113 | 114 | func (s *Service) OnFinish() {} 115 | -------------------------------------------------------------------------------- /common/queue.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "sync" 4 | 5 | // потоко-безопасная очередь 6 | type Queue struct { 7 | // флаг, сигнализирующий, что очередь пуста 8 | empty bool 9 | 10 | // элементы очереди 11 | items []interface{} 12 | 13 | // семафор 14 | mutex *sync.Mutex 15 | } 16 | 17 | // создает новую очередь 18 | func NewQueue() *Queue { 19 | return &Queue{ 20 | empty: true, 21 | items: make([]interface{}, 0), 22 | mutex: new(sync.Mutex), 23 | } 24 | } 25 | 26 | // добавляет элемент в конец очереди 27 | func (q *Queue) Push(item interface{}) { 28 | q.mutex.Lock() 29 | if q.empty { 30 | q.empty = false 31 | } 32 | q.items = append(q.items, item) 33 | q.mutex.Unlock() 34 | } 35 | 36 | // достает первый элемент из очереди 37 | func (q *Queue) Pop() interface{} { 38 | var item interface{} 39 | q.mutex.Lock() 40 | if !q.empty { 41 | oldItems := q.items 42 | oldItemsLen := len(oldItems) 43 | if oldItemsLen > 0 { 44 | item = oldItems[oldItemsLen-1] 45 | q.items = oldItems[0 : oldItemsLen-1] 46 | } else { 47 | q.empty = true 48 | } 49 | } 50 | q.mutex.Unlock() 51 | return item 52 | } 53 | 54 | // сигнализирует, что очередь пуста 55 | func (q *Queue) Empty() bool { 56 | var empty bool 57 | q.mutex.Lock() 58 | empty = q.empty 59 | q.mutex.Unlock() 60 | return empty 61 | } 62 | 63 | // возвращает длину очереди 64 | func (q *Queue) Len() int { 65 | var itemsLen int 66 | q.mutex.Lock() 67 | itemsLen = len(q.items) 68 | q.mutex.Unlock() 69 | return itemsLen 70 | } 71 | 72 | // статус очереди 73 | type queueStatus int 74 | 75 | const ( 76 | // лимитированная очередь 77 | limitedQueueStatus queueStatus = iota 78 | 79 | // безлимитная очередь 80 | unlimitedQueueStatus 81 | ) 82 | 83 | // лимитированная очередь, в ней будут храниться клиенты к почтовым сервисам 84 | type LimitedQueue struct { 85 | *Queue 86 | 87 | // статус, говорящий заблокирована очередь или нет 88 | status queueStatus 89 | 90 | // максимальное количество элементов, которое было в очереди 91 | maxLen int 92 | } 93 | 94 | // создает новую лимитированную очередь 95 | func NewLimitQueue() *LimitedQueue { 96 | return &LimitedQueue{ 97 | Queue: NewQueue(), 98 | status: unlimitedQueueStatus, 99 | } 100 | } 101 | 102 | // сигнализирует, что очередь имеет лимит 103 | func (l *LimitedQueue) HasLimit() bool { 104 | l.mutex.Lock() 105 | hasLimit := l.status == limitedQueueStatus 106 | l.mutex.Unlock() 107 | return hasLimit 108 | } 109 | 110 | // устанавливает лимит очереди 111 | func (l *LimitedQueue) HasLimitOn() { 112 | if l.MaxLen() > 0 && !l.HasLimit() { 113 | l.setStatus(limitedQueueStatus) 114 | } 115 | } 116 | 117 | // снимает лимит очереди 118 | func (l *LimitedQueue) HasLimitOff() { 119 | l.setStatus(unlimitedQueueStatus) 120 | } 121 | 122 | // устанавливает статус очереди 123 | func (l *LimitedQueue) setStatus(status queueStatus) { 124 | l.mutex.Lock() 125 | l.status = status 126 | l.mutex.Unlock() 127 | } 128 | 129 | // максимальная длина очереди до того момента, как был установлен лимит 130 | func (l *LimitedQueue) MaxLen() int { 131 | l.mutex.Lock() 132 | maxLen := l.maxLen 133 | l.mutex.Unlock() 134 | return maxLen 135 | } 136 | 137 | // увеличивает максимальную длину очереди 138 | func (l *LimitedQueue) AddMaxLen() { 139 | l.mutex.Lock() 140 | l.maxLen++ 141 | l.mutex.Unlock() 142 | } 143 | -------------------------------------------------------------------------------- /consumer/assistant.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/actionpay/postmanq/common" 6 | "github.com/actionpay/postmanq/logger" 7 | "github.com/streadway/amqp" 8 | ) 9 | 10 | type Assistant struct { 11 | id int 12 | connect *amqp.Connection 13 | srcBinding *AssistantBinding 14 | destBindings map[string]*Binding 15 | } 16 | 17 | func (a *Assistant) run() { 18 | for i := 0; i < a.srcBinding.Handlers; i++ { 19 | go a.consume(i) 20 | } 21 | } 22 | 23 | func (a *Assistant) consume(id int) { 24 | channel, err := a.connect.Channel() 25 | // выбираем из очереди сообщения с запасом 26 | // это нужно для того, чтобы после отправки письма новое уже было готово к отправке 27 | // в тоже время нельзя выбираеть все сообщения из очереди разом, т.к. можно упереться в память 28 | channel.Qos(a.srcBinding.PrefetchCount, 0, false) 29 | deliveries, err := channel.Consume( 30 | a.srcBinding.Queue, // name 31 | "", // consumerTag, 32 | false, // noAck 33 | false, // exclusive 34 | false, // noLocal 35 | false, // noWait 36 | nil, // arguments 37 | ) 38 | if err == nil { 39 | go a.publish(id, channel, deliveries) 40 | } else { 41 | logger.All().Warn("assistant#%d, handler#%d can't consume queue %s", a.id, id, a.srcBinding.Queue) 42 | } 43 | } 44 | 45 | func (a *Assistant) publish(id int, channel *amqp.Channel, deliveries <-chan amqp.Delivery) { 46 | for delivery := range deliveries { 47 | message := new(common.MailMessage) 48 | err := json.Unmarshal(delivery.Body, message) 49 | if err == nil { 50 | message.Init() 51 | logger. 52 | By(message.HostnameFrom). 53 | Info( 54 | "assistant#%d-%d, handler#%d requeue mail#%d: envelope - %s, recipient - %s to %s", 55 | a.id, 56 | message.Id, 57 | id, 58 | message.Id, 59 | message.Envelope, 60 | message.Recipient, 61 | message.HostnameFrom, 62 | ) 63 | if binding, ok := a.destBindings[message.HostnameFrom]; ok { 64 | err = channel.Publish( 65 | binding.Exchange, 66 | binding.Routing, 67 | false, 68 | false, 69 | amqp.Publishing{ 70 | ContentType: "text/plain", 71 | Body: delivery.Body, 72 | DeliveryMode: amqp.Transient, 73 | }, 74 | ) 75 | if err == nil { 76 | logger. 77 | By(message.HostnameFrom). 78 | Info( 79 | "assistant#%d-%d publish mail#%d to exchange %s", 80 | a.id, 81 | message.Id, 82 | message.Id, 83 | binding.Exchange, 84 | ) 85 | delivery.Ack(true) 86 | return 87 | } else { 88 | logger. 89 | By(message.HostnameFrom). 90 | Warn( 91 | "assistant#%d-%d can't publish mail#%d, error - %v", 92 | a.id, 93 | message.Id, 94 | message.Id, 95 | err, 96 | ) 97 | } 98 | } else { 99 | logger. 100 | By(message.HostnameFrom). 101 | Warn( 102 | "assistant#%d-%d can't publish mail#%d, not found exchange for %s", 103 | a.id, 104 | message.Id, 105 | message.Id, 106 | message.HostnameFrom, 107 | ) 108 | } 109 | } else { 110 | logger.All().Warn("assistant#%d can't unmarshal delivery body, body should be json, %v given, error - %v", a.id, delivery.Body, err) 111 | } 112 | delivery.Nack(true, true) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /recipient/recipient.go: -------------------------------------------------------------------------------- 1 | package recipient 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/logger" 5 | "net" 6 | "net/textproto" 7 | ) 8 | 9 | type Recipient struct { 10 | id int 11 | first State 12 | state State 13 | conn net.Conn 14 | } 15 | 16 | func newRecipient(id int, events chan *Event) { 17 | quit := new(QuitState) 18 | noop := new(NoopState) 19 | rset := new(RsetState) 20 | vrfy := new(VrfyState) 21 | 22 | commonPossibles := []State{ 23 | quit, 24 | noop, 25 | rset, 26 | vrfy, 27 | } 28 | 29 | input := new(InputState) 30 | input.SetPossibles(commonPossibles) 31 | 32 | data := new(DataState) 33 | data.SetNext(input) 34 | data.SetPossibles(commonPossibles) 35 | 36 | rcpt := new(RcptState) 37 | rcpt.SetNext(data) 38 | rcpt.SetPossibles(commonPossibles) 39 | 40 | mail := new(MailState) 41 | mail.SetNext(rcpt) 42 | mail.SetPossibles(commonPossibles) 43 | 44 | input.SetNext(mail) 45 | rset.SetNext(mail) 46 | 47 | ehlo := new(EhloState) 48 | ehlo.SetNext(mail) 49 | ehlo.SetPossibles(commonPossibles) 50 | 51 | conn := new(ConnectState) 52 | conn.SetNext(ehlo) 53 | conn.SetPossibles(commonPossibles) 54 | 55 | recipient := &Recipient{ 56 | id: id, 57 | first: conn, 58 | } 59 | for event := range events { 60 | recipient.handle(event) 61 | } 62 | } 63 | 64 | func (r *Recipient) handle(event *Event) { 65 | var id uint 66 | var buf []byte 67 | txt := textproto.NewConn(event.conn) 68 | //status := ReadStatus 69 | r.state = r.first 70 | 71 | statuses := make(StateStatuses) 72 | statuses.Add(ReadStatus) 73 | for status := range statuses { 74 | r.state.SetEvent(event) 75 | 76 | switch status { 77 | case ReadStatus: 78 | id = txt.Next() 79 | txt.StartRequest(id) 80 | buf = r.state.Read(txt) 81 | txt.EndRequest(id) 82 | cmd, cmdLen := r.state.GetCmd() 83 | if r.state.Check(buf, cmd, cmdLen) { 84 | statuses.Add(r.state.Process(buf)) 85 | } else { 86 | statuses.Add(PossibleStatus) 87 | } 88 | logger.By(event.serverHostname).Debug(string(buf)) 89 | 90 | case WriteStatus: 91 | txt.StartResponse(id) 92 | r.state.Write(txt) 93 | txt.EndResponse(id) 94 | 95 | r.state = r.state.GetNext() 96 | statuses.Add(ReadStatus) 97 | 98 | case PossibleStatus: 99 | var possibleStatus StateStatus 100 | var state State 101 | for _, possible := range r.state.GetPossibles() { 102 | possible.SetEvent(event) 103 | cmd, cmdLen := possible.GetCmd() 104 | if possible.Check(buf, cmd, cmdLen) { 105 | possibleStatus = possible.Process(buf) 106 | if possibleStatus != FailureStatus { 107 | state = possible 108 | status = possibleStatus 109 | break 110 | } 111 | } 112 | } 113 | if state == nil { 114 | state := r.first 115 | for state.GetNext() != nil { 116 | 117 | } 118 | txt.PrintfLine(syntaxErrorResp) 119 | } else { 120 | if state.IsUseCurrent() { 121 | state.SetNext(r.state) 122 | } 123 | r.state = state 124 | } 125 | 126 | case FailureStatus: 127 | txt.StartResponse(id) 128 | txt.PrintfLine(r.state.GetError().message) 129 | txt.EndResponse(id) 130 | logger.By(event.serverHostname).Debug("%s: %s", buf, r.state.GetError().message) 131 | //statuses.Add(ReadStatus) 132 | 133 | case QuitStatus: 134 | txt.StartResponse(id) 135 | r.state.Write(txt) 136 | txt.EndResponse(id) 137 | txt.Close() 138 | return 139 | } 140 | } 141 | close(statuses) 142 | } 143 | -------------------------------------------------------------------------------- /cmd/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "github.com/actionpay/postmanq/common" 7 | "github.com/actionpay/postmanq/logger" 8 | "log" 9 | "net" 10 | "net/smtp" 11 | "runtime" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | common.DefaultWorkersCount = runtime.NumCPU() 18 | logger.Inst() 19 | 20 | //showConn() 21 | 22 | logger.By("localhost").Info("start!") 23 | tcpAddr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort("localhost", "0")) 24 | if err == nil { 25 | logger.By("localhost").Info("resolve tcp addr localhost") 26 | dialer := &net.Dialer{ 27 | LocalAddr: tcpAddr, 28 | DualStack: true, 29 | //Timeout: time.Second * 30, 30 | } 31 | hostname := net.JoinHostPort("localhost", "2225") 32 | connection, err := dialer.Dial("tcp", hostname) 33 | if err == nil { 34 | logger.By("localhost").Info("dial localhost:2225") 35 | c, err := smtp.NewClient(connection, "example.com") 36 | //c.Hello("trololo.com") 37 | c.Hello("trololo") 38 | //c, err := smtp.Dial(hostname) 39 | //if err != nil { 40 | // log.Fatal(err) 41 | //} 42 | if err == nil { 43 | // Set the sender and recipient first 44 | if err := c.Mail("sender@example.org"); err != nil { 45 | log.Fatal("Mail", " ", err) 46 | } 47 | if err := c.Rcpt("recipient@example.net"); err != nil { 48 | log.Fatal("Rcpt", " ", err) 49 | } 50 | 51 | // Send the email body. 52 | wc, err := c.Data() 53 | if err != nil { 54 | log.Fatal("Data", " ", err) 55 | } 56 | _, err = fmt.Fprintf(wc, "This is the email body") 57 | if err != nil { 58 | log.Fatal("Fprintf", " ", err) 59 | } 60 | err = wc.Close() 61 | if err != nil { 62 | log.Fatal("Close", " ", err) 63 | } 64 | 65 | // Send the QUIT command and close the connection. 66 | err = c.Quit() 67 | if err != nil { 68 | log.Fatal("Quit", " ", err) 69 | } 70 | 71 | log.Println("success!") 72 | } else { 73 | logger.By("localhost").Info("can't create client") 74 | } 75 | } else { 76 | logger.By("localhost").Info("can't dial localhost:2225") 77 | } 78 | } else { 79 | logger.By("localhost").Info("can't resolve tcp addr localhost") 80 | } 81 | 82 | } 83 | 84 | func showConn() { 85 | logger.By("localhost").Info("start!") 86 | tcpAddr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort("", "0")) 87 | if err == nil { 88 | logger.By("localhost").Info("resolve tcp addr localhost") 89 | dialer := &net.Dialer{ 90 | LocalAddr: tcpAddr, 91 | DualStack: true, 92 | Timeout: time.Second * 30, 93 | } 94 | mxes, _ := net.LookupMX("gmail.com") 95 | for _, mx := range mxes { 96 | mxHostname := strings.TrimRight(mx.Host, ".") 97 | hostname := net.JoinHostPort(mxHostname, "25") 98 | connection, err := dialer.Dial("tcp", hostname) 99 | if err == nil { 100 | logger.By("localhost").Info("dial %s", connection.LocalAddr()) 101 | c, _ := smtp.NewClient(connection, mxHostname) 102 | ////c, err := smtp.Dial(hostname) 103 | ////if err != nil { 104 | //// log.Fatal(err) 105 | ////} 106 | c.StartTLS(&tls.Config{ 107 | GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { 108 | logger.By("localhost").Info("%v", clientHello) 109 | return nil, nil 110 | }, 111 | }) 112 | state, _ := c.TLSConnectionState() 113 | logger.By("localhost").Info("%v", state) 114 | } else { 115 | logger.By("localhost").Info("can't connect %s", hostname) 116 | } 117 | break 118 | } 119 | } else { 120 | logger.By("localhost").Info("can't resolve tcp addr localhost") 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /mailer/service.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "github.com/actionpay/postmanq/common" 8 | "github.com/actionpay/postmanq/logger" 9 | "github.com/byorty/dkim" 10 | yaml "gopkg.in/yaml.v2" 11 | "io/ioutil" 12 | ) 13 | 14 | var ( 15 | // сервис отправки писем 16 | service *Service 17 | // канал для писем 18 | events = make(chan *common.SendEvent) 19 | ) 20 | 21 | // сервис отправки писем 22 | type Service struct { 23 | // количество отправителей 24 | MailersCount int `yaml:"workers"` 25 | 26 | Configs map[string]*Config `yaml:"postmans"` 27 | } 28 | 29 | // создает новый сервис отправки писем 30 | func Inst() common.SendingService { 31 | if service == nil { 32 | service = new(Service) 33 | } 34 | return service 35 | } 36 | 37 | // инициализирует сервис отправки писем 38 | func (s *Service) OnInit(event *common.ApplicationEvent) { 39 | err := yaml.Unmarshal(event.Data, s) 40 | if err == nil { 41 | for name, config := range s.Configs { 42 | s.init(config, name) 43 | } 44 | // указываем заголовки для DKIM 45 | dkim.StdSignableHeaders = []string{ 46 | "From", 47 | "To", 48 | "Subject", 49 | } 50 | if s.MailersCount == 0 { 51 | s.MailersCount = common.DefaultWorkersCount 52 | } 53 | } else { 54 | logger.All().FailExitWithErr(err) 55 | } 56 | } 57 | 58 | func (s *Service) init(conf *Config, hostname string) { 59 | // закрытый ключ должен быть указан обязательно 60 | // поэтому даже не проверяем что указано в переменной 61 | privateKey, err := ioutil.ReadFile(conf.PrivateKeyFilename) 62 | if err == nil { 63 | logger.By(hostname).Debug("mailer service private key %s read success", conf.PrivateKeyFilename) 64 | der, _ := pem.Decode(privateKey) 65 | conf.privateKey, err = x509.ParsePKCS1PrivateKey(der.Bytes) 66 | if err != nil { 67 | logger.By(hostname).Err("mailer service can't decode or parse private key %s", conf.PrivateKeyFilename) 68 | logger.By(hostname).FailExitWithErr(err) 69 | } 70 | } else { 71 | logger.By(hostname).Err("mailer service can't read private key %s", conf.PrivateKeyFilename) 72 | logger.By(hostname).FailExitWithErr(err) 73 | } 74 | // если не задан селектор, устанавливаем селектор по умолчанию 75 | if len(conf.DkimSelector) == 0 { 76 | conf.DkimSelector = "mail" 77 | } 78 | } 79 | 80 | // запускает отправителей и прием сообщений из очереди 81 | func (s *Service) OnRun() { 82 | logger.All().Debug("run mailers apps...") 83 | for i := 0; i < s.MailersCount; i++ { 84 | go newMailer(i + 1) 85 | } 86 | } 87 | 88 | // канал для приема событий отправки писем 89 | func (s *Service) Events() chan *common.SendEvent { 90 | return events 91 | } 92 | 93 | // завершает работу сервиса отправки писем 94 | func (s *Service) OnFinish() { 95 | close(events) 96 | } 97 | 98 | func (s *Service) getDkimSelector(hostname string) string { 99 | if conf, ok := s.Configs[hostname]; ok { 100 | return conf.DkimSelector 101 | } else { 102 | logger.By(hostname).Err("mailer service can't find dkim selector by %s", hostname) 103 | return common.EmptyStr 104 | } 105 | } 106 | 107 | func (s *Service) getPrivateKey(hostname string) *rsa.PrivateKey { 108 | if conf, ok := s.Configs[hostname]; ok { 109 | return conf.privateKey 110 | } else { 111 | logger.By(hostname).Err("mailer service can't find private key by %s", hostname) 112 | return nil 113 | } 114 | } 115 | 116 | type Config struct { 117 | // путь до закрытого ключа 118 | PrivateKeyFilename string `yaml:"privateKey"` 119 | 120 | // селектор 121 | DkimSelector string `yaml:"dkimSelector"` 122 | 123 | // содержимое приватного ключа 124 | privateKey *rsa.PrivateKey 125 | } 126 | -------------------------------------------------------------------------------- /mailer/mailer.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/actionpay/postmanq/common" 7 | "github.com/actionpay/postmanq/logger" 8 | "github.com/byorty/dkim" 9 | ) 10 | 11 | // отправитель письма 12 | type Mailer struct { 13 | // идентификатор для логов 14 | id int 15 | } 16 | 17 | // создает нового отправителя 18 | func newMailer(id int) { 19 | mailer := &Mailer{id} 20 | mailer.run() 21 | } 22 | 23 | // запускает отправителя 24 | func (m *Mailer) run() { 25 | for event := range events { 26 | m.sendMail(event) 27 | } 28 | } 29 | 30 | // подписывает dkim и отправляет письмо 31 | func (m *Mailer) sendMail(event *common.SendEvent) { 32 | message := event.Message 33 | if common.EmailRegexp.MatchString(message.Envelope) && common.EmailRegexp.MatchString(message.Recipient) { 34 | m.prepare(message) 35 | m.send(event) 36 | } else { 37 | common.ReturnMail(event, errors.New(fmt.Sprintf("511 service#%d can't send mail#%d, envelope or ricipient is invalid", m.id, message.Id))) 38 | } 39 | } 40 | 41 | // подписывает dkim 42 | func (m *Mailer) prepare(message *common.MailMessage) { 43 | conf, err := dkim.NewConf(message.HostnameFrom, service.getDkimSelector(message.HostnameFrom)) 44 | if err == nil { 45 | conf[dkim.AUIDKey] = message.Envelope 46 | conf[dkim.CanonicalizationKey] = "relaxed/relaxed" 47 | signer := dkim.NewByKey(conf, service.getPrivateKey(message.HostnameFrom)) 48 | if err == nil { 49 | signed, err := signer.Sign([]byte(message.Body)) 50 | if err == nil { 51 | message.Body = string(signed) 52 | logger.By(message.HostnameFrom).Debug("mailer#%d-%d success sign mail", m.id, message.Id) 53 | } else { 54 | logger.By(message.HostnameFrom).Warn("mailer#%d-%d can't sign mail, error - %v", m.id, message.Id, err) 55 | } 56 | } else { 57 | logger.By(message.HostnameFrom).Warn("mailer#%d-%d can't create dkim signer, error - %v", m.id, message.Id, err) 58 | } 59 | } else { 60 | logger.By(message.HostnameFrom).Warn("mailer#%d-%d can't create dkim config, error - %v", m.id, message.Id, err) 61 | } 62 | } 63 | 64 | // отправляет письмо 65 | func (m *Mailer) send(event *common.SendEvent) { 66 | message := event.Message 67 | worker := event.Client.Worker 68 | logger.By(event.Message.HostnameFrom).Info("mailer#%d-%d begin sending mail", m.id, message.Id) 69 | logger.By(message.HostnameFrom).Debug("mailer#%d-%d receive smtp client#%d", m.id, message.Id, event.Client.Id) 70 | 71 | success := false 72 | event.Client.SetTimeout(common.App.Timeout().Mail) 73 | err := worker.Mail(message.Envelope) 74 | if err == nil { 75 | logger.By(message.HostnameFrom).Debug("mailer#%d-%d send command MAIL FROM: %s", m.id, message.Id, message.Envelope) 76 | event.Client.SetTimeout(common.App.Timeout().Rcpt) 77 | err = worker.Rcpt(message.Recipient) 78 | if err == nil { 79 | logger.By(message.HostnameFrom).Debug("mailer#%d-%d send command RCPT TO: %s", m.id, message.Id, message.Recipient) 80 | event.Client.SetTimeout(common.App.Timeout().Data) 81 | wc, err := worker.Data() 82 | if err == nil { 83 | logger.By(message.HostnameFrom).Debug("mailer#%d-%d send command DATA", m.id, message.Id) 84 | _, err = fmt.Fprint(wc, message.Body) 85 | if err == nil { 86 | wc.Close() 87 | logger.By(message.HostnameFrom).Debug("%s", message.Body) 88 | logger.By(message.HostnameFrom).Debug("mailer#%d-%d send command .", m.id, message.Id) 89 | // стараемся слать письма через уже созданное соединение, 90 | // поэтому после отправки письма не закрываем соединение 91 | err = worker.Reset() 92 | if err == nil { 93 | logger.By(message.HostnameFrom).Debug("mailer#%d-%d send command RSET", m.id, message.Id) 94 | logger.By(event.Message.HostnameFrom).Info("mailer#%d-%d success send mail#%d", m.id, message.Id, message.Id) 95 | success = true 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | event.Client.Wait() 103 | event.Queue.Push(event.Client) 104 | 105 | if success { 106 | // отпускаем поток получателя сообщений из очереди 107 | event.Result <- common.SuccessSendEventResult 108 | } else { 109 | common.ReturnMail(event, err) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # получатели писем, обязательный параметр 2 | consumers: 3 | 4 | # серверы, с которых получаем письма 5 | - uri: amqp://guest:guest@127.0.0.1:5672/postmanq 6 | 7 | assistants: 8 | 9 | # имя обменника 10 | - exchange: example 11 | 12 | # имя очереди 13 | queue: example 14 | 15 | # direct|fanout|topic, по умолчанию fanout, необязательный параметр 16 | type: fanout 17 | 18 | # по умолчанию пустая строка, необязательный параметр 19 | routing: example 20 | 21 | # количество обработчиков очереди, по умолчанию количество ядер процессора, необязательный параметр 22 | workers: 20 23 | 24 | dest: 25 | example1.com: postmanq1 26 | example2.com: postmanq2 27 | example3.com: postmanq3 28 | 29 | bindings: 30 | 31 | # имя обменника 32 | - exchange: postmanq 33 | 34 | # имя очереди 35 | queue: postmanq 36 | 37 | # direct|fanout|topic, по умолчанию fanout, необязательный параметр 38 | type: fanout 39 | 40 | # по умолчанию пустая строка, необязательный параметр 41 | # routing: outbox 42 | 43 | # количество обработчиков очереди, по умолчанию количество ядер процессора, необязательный параметр 44 | workers: 20 45 | 46 | # - если указано name, тогда обменник и очередь именуются одинаково 47 | # name: second 48 | 49 | # количество потоков для проверки лимитов, создания подключений, отправки писем, по умолчанию количество ядер процессора, необязательный параметр 50 | workers: 20 51 | 52 | # таймауты, необязательный параметр 53 | timeouts: 54 | # насколько поток будет засыпать, пока не появится свободное соединение и т.д, необязательный параметр, по умолчанию секунда 55 | sleep: 1s 56 | 57 | # время ожидания отправки новых писем, по истечении времени соединение закрывается, необязательный параметр, по умолчанию 30 секунд 58 | waiting: 30s 59 | 60 | # время ожидания создания нового соединения с почтовым сервисом, необязательный параметр, по умолчанию 5 минут 61 | connection: 5m 62 | 63 | # время ожидания ответа команде HELLO, необязательный параметр, по умолчанию 5 минут 64 | hello: 5m 65 | 66 | # время ожидания ответа команде MAIL, необязательный параметр, по умолчанию 5 минут 67 | mail: 5m 68 | 69 | # время ожидания ответа команде RCPT, необязательный параметр, по умолчанию 5 минут 70 | rcpt: 5m 71 | 72 | # время ожидания ответа команде DATA, необязательный параметр, по умолчанию 10 минут 73 | data: 10m 74 | 75 | # домены, с которых будут рассылаться письма, обязательный параметр 76 | postmans: 77 | 78 | # настройки для домена 79 | example.com: 80 | # уровень логов - debug|info|warning|error, по умолчанию warning, необязательный параметр 81 | logLevel: debug 82 | 83 | # как будут выводиться логи, в консоль или файл, stdout | /path/to/file, по умолчанию stdout, необязательный параметр 84 | logOutput: stdout 85 | 86 | # приватный ключ, публичный ключ должен быть прописан в DNS 87 | privateKey: /path/to/private/key_rsa1 88 | 89 | # сертификат, используется для создания TLS соединений 90 | certificate: /path/to/cert1 91 | 92 | sender: 93 | # селектор dkim, по умолчанию mail, необязательный параметр 94 | dkimSelector: mail 95 | 96 | # ip, с которых будем рассылать письма 97 | ips: [1.1.1.1, 2.2.2.2, 3.3.3.3] 98 | 99 | # домены исключенные из рассылки, необязательный параметр 100 | exclude: [bad.address1.com, bad.address2.com] 101 | 102 | # домен, с которого будем рассылать письма 103 | domain: mail.example.com 104 | 105 | # лимиты, необязательный параметр 106 | limits: 107 | 108 | # хост почтового сервиса 109 | yandex.ru: 110 | 111 | # период, за который учитываем количество отправленных писем, возможные значения - second|minute|hour|day 112 | type: day 113 | 114 | # максимальное количество писем, которое может быть отправлено за период 115 | value: 150 116 | 117 | recipient: 118 | 119 | port: 25 120 | 121 | workers: 20 122 | 123 | queue: example 124 | 125 | domain: mail.example.com -------------------------------------------------------------------------------- /grep/service.go: -------------------------------------------------------------------------------- 1 | package grep 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/actionpay/postmanq/common" 7 | yaml "gopkg.in/yaml.v2" 8 | "os" 9 | "regexp" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | var ( 15 | // сервис ищущий сообщения в логе об отправке письма 16 | service *Service 17 | 18 | // регулярное выражение, по которому находим начало отправки 19 | mailIdRegex = regexp.MustCompile(`mail#((\d)+)+`) 20 | ) 21 | 22 | type Config struct { 23 | // путь до файла с логами 24 | Output string `yaml:"logOutput"` 25 | 26 | // файл с логами 27 | logFile *os.File 28 | } 29 | 30 | // сервис ищущий сообщения в логе об отправке письма 31 | type Service struct { 32 | Configs map[string]*Config `yaml:"postmans"` 33 | } 34 | 35 | // создает новый сервис поиска по логам 36 | func Inst() common.GrepService { 37 | if service == nil { 38 | service = new(Service) 39 | } 40 | return service 41 | } 42 | 43 | // инициализирует сервис 44 | func (s *Service) OnInit(event *common.ApplicationEvent) { 45 | var err error 46 | err = yaml.Unmarshal(event.Data, s) 47 | if err == nil { 48 | for _, config := range s.Configs { 49 | s.init(config) 50 | } 51 | } else { 52 | fmt.Println("grep service can't unmarshal config file") 53 | common.App.Events() <- common.NewApplicationEvent(common.FinishApplicationEventKind) 54 | } 55 | } 56 | 57 | func (s *Service) init(config *Config) { 58 | if common.FilenameRegex.MatchString(config.Output) { 59 | var err error 60 | config.logFile, err = os.OpenFile(config.Output, os.O_RDONLY, os.ModePerm) 61 | if err != nil { 62 | fmt.Println(err) 63 | common.App.Events() <- common.NewApplicationEvent(common.FinishApplicationEventKind) 64 | } 65 | } else { 66 | fmt.Println("grep service can't open logOutput file") 67 | common.App.Events() <- common.NewApplicationEvent(common.FinishApplicationEventKind) 68 | } 69 | } 70 | 71 | // ищет логи об отправке письма 72 | func (s *Service) OnGrep(event *common.ApplicationEvent) { 73 | outs := make(chan string) 74 | group := new(sync.WaitGroup) 75 | group.Add(len(s.Configs)) 76 | if event.GetStringArg("envelope") == "" { 77 | for _, config := range s.Configs { 78 | go s.grep(event, config, outs, group) 79 | } 80 | } else { 81 | parts := strings.Split(event.GetStringArg("envelope"), "@") 82 | if len(parts) == 2 { 83 | if config, ok := s.Configs[parts[1]]; ok { 84 | go s.grep(event, config, outs, group) 85 | } else { 86 | common.App.Events() <- common.NewApplicationEvent(common.FinishApplicationEventKind) 87 | } 88 | } else { 89 | common.App.Events() <- common.NewApplicationEvent(common.FinishApplicationEventKind) 90 | } 91 | } 92 | 93 | go func() { 94 | for out := range outs { 95 | fmt.Println(out) 96 | } 97 | }() 98 | group.Wait() 99 | common.App.Events() <- common.NewApplicationEvent(common.FinishApplicationEventKind) 100 | } 101 | 102 | func (s *Service) grep(event *common.ApplicationEvent, config *Config, outs chan string, group *sync.WaitGroup) { 103 | scanner := bufio.NewScanner(config.logFile) 104 | scanner.Split(bufio.ScanLines) 105 | lines := make(chan string) 106 | 107 | go func() { 108 | for scanner.Scan() { 109 | lines <- scanner.Text() 110 | } 111 | }() 112 | 113 | var expr string 114 | if event.GetStringArg("envelope") == "" { 115 | expr = fmt.Sprintf("recipient - %s", event.GetStringArg("recipient")) 116 | } else { 117 | expr = fmt.Sprintf("envelope - %s, recipient - %s", event.GetStringArg("envelope"), event.GetStringArg("recipient")) 118 | } 119 | 120 | var successExpr, failExpr, failPubExpr, delayExpr, limitExpr string 121 | var mailId string 122 | for line := range lines { 123 | if mailId == "" { 124 | if strings.Contains(line, expr) { 125 | results := mailIdRegex.FindStringSubmatch(line) 126 | if len(results) == 3 { 127 | mailId = results[1] 128 | 129 | successExpr = fmt.Sprintf("%s success send", mailId) 130 | failExpr = fmt.Sprintf("%s publish failure mail to queue", mailId) 131 | failPubExpr = fmt.Sprintf("%s can't publish failure mail to queue", mailId) 132 | delayExpr = fmt.Sprintf("%s detect old dlx queue", mailId) 133 | limitExpr = fmt.Sprintf("%s detect overlimit", mailId) 134 | 135 | outs <- line 136 | } 137 | } 138 | } else { 139 | if strings.Contains(line, mailId) { 140 | outs <- line 141 | } 142 | if strings.Contains(line, successExpr) || 143 | strings.Contains(line, failExpr) || 144 | strings.Contains(line, failPubExpr) || 145 | strings.Contains(line, delayExpr) || 146 | strings.Contains(line, limitExpr) { 147 | mailId = "" 148 | } 149 | } 150 | } 151 | 152 | group.Done() 153 | } 154 | 155 | // завершает работу сервиса 156 | func (s *Service) OnFinish(event *common.ApplicationEvent) { 157 | for _, config := range s.Configs { 158 | config.logFile.Close() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /common/post.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | const ( 12 | // Максимальное количество попыток подключения к почтовику за отправку письма 13 | MaxTryConnectionCount int = 30 14 | AllDomains string = "*" 15 | EmptyStr string = "" 16 | ) 17 | 18 | var ( 19 | // Регулярка для проверки адреса почты, сразу компилируем, чтобы при отправке не терять на этом время 20 | EmailRegexp = regexp.MustCompile(`^[\w\d\.\_\%\+\-]+@([\w\d\.\-]+\.\w{2,5})$`) 21 | HostnameRegex = regexp.MustCompile(`^[\w\d\.\-]+\.\w{2,5}$`) 22 | EmptyStrSlice = []string{} 23 | ) 24 | 25 | // таймауты приложения 26 | type Timeout struct { 27 | Sleep time.Duration `yaml:"sleep"` 28 | Waiting time.Duration `yaml:"waiting"` 29 | Connection time.Duration `yaml:"connection"` 30 | Hello time.Duration `yaml:"hello"` 31 | Mail time.Duration `yaml:"mail"` 32 | Rcpt time.Duration `yaml:"rcpt"` 33 | Data time.Duration `yaml:"data"` 34 | } 35 | 36 | // инициализирует значения таймаутов по умолчанию 37 | func (t *Timeout) Init() { 38 | if t.Sleep == 0 { 39 | t.Sleep = time.Second 40 | } 41 | if t.Waiting == 0 { 42 | t.Waiting = 30 * time.Second 43 | } 44 | if t.Connection == 0 { 45 | t.Connection = 5 * time.Minute 46 | } 47 | if t.Hello == 0 { 48 | t.Hello = 5 * time.Minute 49 | } 50 | if t.Mail == 0 { 51 | t.Mail = 5 * time.Minute 52 | } 53 | if t.Rcpt == 0 { 54 | t.Rcpt = 5 * time.Minute 55 | } 56 | if t.Data == 0 { 57 | t.Data = 10 * time.Minute 58 | } 59 | } 60 | 61 | // тип отложенной очереди 62 | type DelayedBindingType int 63 | 64 | const ( 65 | UnknownDelayedBinding DelayedBindingType = iota 66 | SecondDelayedBinding 67 | ThirtySecondDelayedBinding 68 | MinuteDelayedBinding 69 | FiveMinutesDelayedBinding 70 | TenMinutesDelayedBinding 71 | TwentyMinutesDelayedBinding 72 | ThirtyMinutesDelayedBinding 73 | FortyMinutesDelayedBinding 74 | FiftyMinutesDelayedBinding 75 | HourDelayedBinding 76 | SixHoursDelayedBinding 77 | DayDelayedBinding 78 | NotSendDelayedBinding 79 | ) 80 | 81 | // ошибка во время отпрвки письма 82 | type MailError struct { 83 | // сообщение 84 | Message string `json:"message"` 85 | 86 | // код ошибки 87 | Code int `json:"code"` 88 | } 89 | 90 | // письмо 91 | type MailMessage struct { 92 | // идентификатор для логов 93 | Id int64 `json:"-"` 94 | 95 | // отправитель 96 | Envelope string `json:"envelope"` 97 | 98 | // получатель 99 | Recipient string `json:"recipient"` 100 | 101 | // тело письма 102 | Body string `json:"body"` 103 | 104 | // домен отправителя, удобно сразу получить и использовать далее 105 | HostnameFrom string `json:"-"` 106 | 107 | // Домен получателя, удобно сразу получить и использовать далее 108 | HostnameTo string `json:"-"` 109 | 110 | // дата создания, используется в основном сервисом ограничений 111 | CreatedDate time.Time `json:"-"` 112 | 113 | // тип очереди, в которою письмо уже было отправлено после неудачной отправки, ипользуется для цепочки очередей 114 | BindingType DelayedBindingType `json:"bindingType"` 115 | 116 | // ошибка отправки 117 | Error *MailError `json:"error"` 118 | } 119 | 120 | // инициализирует письмо 121 | func (m *MailMessage) Init() { 122 | m.Id = time.Now().UnixNano() 123 | m.CreatedDate = time.Now() 124 | if hostname, err := m.getHostnameFromEmail(m.Envelope); err == nil { 125 | m.HostnameFrom = hostname 126 | } 127 | if hostname, err := m.getHostnameFromEmail(m.Recipient); err == nil { 128 | m.HostnameTo = hostname 129 | } 130 | } 131 | 132 | // получает домен из адреса 133 | func (m *MailMessage) getHostnameFromEmail(email string) (string, error) { 134 | matches := EmailRegexp.FindAllStringSubmatch(email, -1) 135 | if len(matches) == 1 && len(matches[0]) == 2 { 136 | return matches[0][1], nil 137 | } else { 138 | return "", errors.New("invalid email address") 139 | } 140 | } 141 | 142 | // возвращает письмо обратно в очередь после ошибки во время отправки 143 | func ReturnMail(event *SendEvent, err error) { 144 | // необходимо проверить сообщение на наличие кода ошибки 145 | // обычно код идет первым 146 | if err != nil { 147 | errorMessage := err.Error() 148 | parts := strings.Split(errorMessage, " ") 149 | if len(parts) > 0 { 150 | // пытаемся получить код 151 | code, e := strconv.Atoi(strings.TrimSpace(parts[0])) 152 | // и создать ошибку 153 | // письмо с ошибкой вернется в другую очередь, отличную от письмо без ошибки 154 | if e == nil { 155 | event.Message.Error = &MailError{errorMessage, code} 156 | } 157 | } 158 | } 159 | 160 | // если в событии уже создан клиент 161 | if event.Client != nil { 162 | if event.Client.Worker != nil { 163 | // сбрасываем цепочку команд к почтовому сервису 164 | event.Client.Worker.Reset() 165 | } 166 | } 167 | 168 | // отпускаем поток получателя сообщений из очереди 169 | if event.Message.Error == nil { 170 | event.Result <- DelaySendEventResult 171 | } else { 172 | if event.Message.Error.Code == 421 { 173 | event.Result <- DelaySendEventResult 174 | } else { 175 | event.Result <- ErrorSendEventResult 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /logger/service.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/actionpay/postmanq/common" 6 | yaml "gopkg.in/yaml.v2" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | ) 11 | 12 | // уровень логирования 13 | type Level int 14 | 15 | // уровни логирования 16 | const ( 17 | DebugLevel Level = iota 18 | InfoLevel 19 | WarningLevel 20 | ErrorLevel 21 | ) 22 | 23 | // название уровней логирования 24 | const ( 25 | DebugLevelName = "debug" 26 | InfoLevelName = "info" 27 | WarningLevelName = "warning" 28 | ErrorLevelName = "error" 29 | ) 30 | 31 | var ( 32 | // названия уровней логирования, используется непосредственно в момент создания записи в лог 33 | logLevelById = map[Level]string{ 34 | DebugLevel: DebugLevelName, 35 | InfoLevel: InfoLevelName, 36 | WarningLevel: WarningLevelName, 37 | ErrorLevel: ErrorLevelName, 38 | } 39 | // уровни логирования по названию, используется для удобной инициализации сервиса логирования 40 | logLevelByName = map[string]Level{ 41 | DebugLevelName: DebugLevel, 42 | InfoLevelName: InfoLevel, 43 | WarningLevelName: WarningLevel, 44 | ErrorLevelName: ErrorLevel, 45 | } 46 | // канал логирования 47 | messages = make(chan *Message) 48 | messagesChanPool = make(map[string]chan *Message) 49 | service *Service 50 | ) 51 | 52 | // сервис логирования 53 | type Service struct { 54 | Config 55 | 56 | Configs map[string]*Config `yaml:"postmans"` 57 | } 58 | 59 | // создает новый сервис логирования 60 | func Inst() common.SendingService { 61 | if service == nil { 62 | service = new(Service) 63 | for i := 0; i < common.DefaultWorkersCount; i++ { 64 | go service.listenCommonMessags() 65 | } 66 | service.Configs = map[string]*Config{ 67 | "localhost": &Config{ 68 | LevelName: "debug", 69 | Output: "stdout", 70 | }, 71 | } 72 | service.init() 73 | } 74 | return service 75 | } 76 | 77 | func (s *Service) listenCommonMessags() { 78 | for message := range messages { 79 | if message.Hostname == common.AllDomains { 80 | for _, messagesChan := range messagesChanPool { 81 | messagesChan <- message 82 | } 83 | } else { 84 | if messagesChan, ok := messagesChanPool[message.Hostname]; ok { 85 | messagesChan <- message 86 | } 87 | } 88 | } 89 | } 90 | 91 | func (s *Service) init() { 92 | for name, config := range s.Configs { 93 | messagesChan := make(chan *Message) 94 | var level Level 95 | if existsLevel, ok := logLevelByName[config.LevelName]; ok { 96 | level = existsLevel 97 | } else { 98 | level = DebugLevel 99 | } 100 | for i := 0; i < common.DefaultWorkersCount; i++ { 101 | var writer Writer 102 | if common.FilenameRegex.MatchString(config.Output) { // проверяем получили ли из настроек имя файла 103 | // получаем директорию, в которой лежит файл 104 | dir := filepath.Dir(config.Output) 105 | // смотрим, что она реально существует 106 | if _, err := os.Stat(dir); os.IsNotExist(err) { 107 | All().FailExit("directory %s is not exists", dir) 108 | } else { 109 | writer = &FileWriter{ 110 | filename: config.Output, 111 | level: level, 112 | } 113 | } 114 | } else if len(config.Output) == 0 || config.Output == "stdout" { 115 | writer = &StdoutWriter{ 116 | level: level, 117 | } 118 | } 119 | if writer != nil { 120 | go s.listenMessages(messagesChan, writer) 121 | } 122 | } 123 | messagesChanPool[name] = messagesChan 124 | } 125 | } 126 | 127 | // подписывает авторов на получение сообщений для логирования 128 | func (s *Service) listenMessages(messagesChan chan *Message, writer Writer) { 129 | for message := range messagesChan { 130 | if writer.getLevel() <= message.Level { 131 | s.writeMessage(writer, message) 132 | } 133 | } 134 | } 135 | 136 | // пишет сообщение в лог 137 | func (s *Service) writeMessage(writer Writer, message *Message) { 138 | writer.writeString( 139 | fmt.Sprintf( 140 | "PostmanQ | %v | %s: %s\n", 141 | time.Now().Format("2006-01-02 15:04:05"), 142 | logLevelById[message.Level], 143 | fmt.Sprintf(message.Message, message.Args...), 144 | ), 145 | ) 146 | } 147 | 148 | // инициализирует сервис логирования 149 | func (s *Service) OnInit(event *common.ApplicationEvent) { 150 | err := yaml.Unmarshal(event.Data, s) 151 | if err == nil { 152 | s.OnFinish() 153 | // заново инициализируем вывод для логов 154 | delete(service.Configs, "default") 155 | messages = make(chan *Message) 156 | messagesChanPool = make(map[string]chan *Message) 157 | for i := 0; i < common.DefaultWorkersCount; i++ { 158 | go s.listenCommonMessags() 159 | } 160 | s.init() 161 | } else { 162 | All().FailExitWithErr(err) 163 | } 164 | } 165 | 166 | // ничего не делает, авторы логов уже пишут 167 | func (s *Service) OnRun() {} 168 | 169 | // не учавствеут в отправке писем 170 | func (s *Service) Events() chan *common.SendEvent { 171 | return nil 172 | } 173 | 174 | // закрывает канал логирования 175 | func (s *Service) OnFinish() { 176 | close(messages) 177 | for name, messagesChan := range messagesChanPool { 178 | close(messagesChan) 179 | delete(messagesChanPool, name) 180 | } 181 | messagesChanPool = nil 182 | } 183 | 184 | type Config struct { 185 | // название уровня логирования, устанавливается в конфиге 186 | LevelName string `yaml:"logLevel"` 187 | 188 | // название вывода логов 189 | Output string `yaml:"logOutput"` 190 | } 191 | -------------------------------------------------------------------------------- /application/abstract.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "github.com/actionpay/postmanq/logger" 6 | "io/ioutil" 7 | "time" 8 | ) 9 | 10 | type FireAction interface { 11 | Fire(common.Application, *common.ApplicationEvent, interface{}) 12 | } 13 | 14 | type PreFireAction interface { 15 | FireAction 16 | PreFire(common.Application, *common.ApplicationEvent) 17 | } 18 | 19 | type PostFireAction interface { 20 | FireAction 21 | PostFire(common.Application, *common.ApplicationEvent) 22 | } 23 | 24 | var ( 25 | actions = map[common.ApplicationEventKind]FireAction{ 26 | common.InitApplicationEventKind: InitFireAction((*Abstract).FireInit), 27 | common.RunApplicationEventKind: RunFireAction((*Abstract).FireRun), 28 | common.FinishApplicationEventKind: FinishFireAction((*Abstract).FireFinish), 29 | } 30 | ) 31 | 32 | // базовое приложение 33 | type Abstract struct { 34 | // путь до конфигурационного файла 35 | configFilename string 36 | 37 | // сервисы приложения, отправляющие письма 38 | services []interface{} 39 | 40 | // канал событий приложения 41 | events chan *common.ApplicationEvent 42 | 43 | // флаг, сигнализирующий окончание работы приложения 44 | done chan bool 45 | 46 | CommonTimeout common.Timeout `yaml:"timeouts"` 47 | } 48 | 49 | // проверяет валидность пути к файлу с настройками 50 | func (a *Abstract) IsValidConfigFilename(filename string) bool { 51 | return len(filename) > 0 && filename != common.ExampleConfigYaml 52 | } 53 | 54 | // запускает основной цикл приложения 55 | func (a *Abstract) run(app common.Application, event *common.ApplicationEvent) { 56 | app.SetDone(make(chan bool)) 57 | // создаем каналы для событий 58 | app.SetEvents(make(chan *common.ApplicationEvent, 3)) 59 | go func() { 60 | for event := range app.Events() { 61 | action := actions[event.Kind] 62 | 63 | if preAction, ok := action.(PreFireAction); ok { 64 | preAction.PreFire(app, event) 65 | } 66 | 67 | for _, service := range app.Services() { 68 | action.Fire(app, event, service) 69 | } 70 | 71 | if postAction, ok := action.(PostFireAction); ok { 72 | postAction.PostFire(app, event) 73 | } 74 | } 75 | close(app.Events()) 76 | }() 77 | app.Events() <- event 78 | <-app.Done() 79 | } 80 | 81 | func (a Abstract) GetConfigFilename() string { 82 | return a.configFilename 83 | } 84 | 85 | // устанавливает путь к файлу с настройками 86 | func (a *Abstract) SetConfigFilename(configFilename string) { 87 | a.configFilename = configFilename 88 | } 89 | 90 | // устанавливает канал событий приложения 91 | func (a *Abstract) SetEvents(events chan *common.ApplicationEvent) { 92 | a.events = events 93 | } 94 | 95 | // возвращает канал событий приложения 96 | func (a *Abstract) Events() chan *common.ApplicationEvent { 97 | return a.events 98 | } 99 | 100 | // устанавливает канал завершения приложения 101 | func (a *Abstract) SetDone(done chan bool) { 102 | a.done = done 103 | } 104 | 105 | // возвращает канал завершения приложения 106 | func (a *Abstract) Done() chan bool { 107 | return a.done 108 | } 109 | 110 | // возвращает сервисы, используемые приложением 111 | func (a *Abstract) Services() []interface{} { 112 | return a.services 113 | } 114 | 115 | // инициализирует сервисы 116 | func (a *Abstract) FireInit(event *common.ApplicationEvent, abstractService interface{}) { 117 | service := abstractService.(common.Service) 118 | service.OnInit(event) 119 | } 120 | 121 | // инициализирует приложение 122 | func (a *Abstract) Init(event *common.ApplicationEvent) {} 123 | 124 | // запускает приложение 125 | func (a *Abstract) Run() {} 126 | 127 | // запускает приложение с аргументами 128 | func (a *Abstract) RunWithArgs(args ...interface{}) {} 129 | 130 | // запускает сервисы приложения 131 | func (a *Abstract) FireRun(event *common.ApplicationEvent, abstractService interface{}) {} 132 | 133 | // останавливает сервисы приложения 134 | func (a *Abstract) FireFinish(event *common.ApplicationEvent, abstractService interface{}) {} 135 | 136 | // возвращает таймауты приложения 137 | func (a *Abstract) Timeout() common.Timeout { 138 | return a.CommonTimeout 139 | } 140 | 141 | type InitFireAction func(*Abstract, *common.ApplicationEvent, interface{}) 142 | 143 | func (i InitFireAction) Fire(app common.Application, event *common.ApplicationEvent, abstractService interface{}) { 144 | app.FireInit(event, abstractService) 145 | } 146 | 147 | func (i InitFireAction) PreFire(app common.Application, event *common.ApplicationEvent) { 148 | // пытаемся прочитать конфигурационный файл 149 | bytes, err := ioutil.ReadFile(app.GetConfigFilename()) 150 | if err == nil { 151 | event.Data = bytes 152 | app.Init(event) 153 | } else { 154 | logger.All().FailExit("application can't read configuration file, error - %v", err) 155 | } 156 | } 157 | 158 | func (i InitFireAction) PostFire(app common.Application, event *common.ApplicationEvent) { 159 | event.Kind = common.RunApplicationEventKind 160 | app.Events() <- event 161 | } 162 | 163 | type RunFireAction func(*Abstract, *common.ApplicationEvent, interface{}) 164 | 165 | func (r RunFireAction) Fire(app common.Application, event *common.ApplicationEvent, abstractService interface{}) { 166 | app.FireRun(event, abstractService) 167 | } 168 | 169 | type FinishFireAction func(*Abstract, *common.ApplicationEvent, interface{}) 170 | 171 | func (f FinishFireAction) Fire(app common.Application, event *common.ApplicationEvent, abstractService interface{}) { 172 | app.FireFinish(event, abstractService) 173 | } 174 | 175 | func (f FinishFireAction) PostFire(app common.Application, event *common.ApplicationEvent) { 176 | time.Sleep(30 * time.Second) 177 | app.Done() <- true 178 | } 179 | -------------------------------------------------------------------------------- /consumer/sign.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "github.com/actionpay/postmanq/common" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | // карта признаков ошибок, используется для распределения неотправленных сообщений по очередям для ошибок 10 | errorSignsMap = ErrorSignsMap{ 11 | 501: ErrorSigns{ 12 | ErrorSign{RecipientFailureBindingType, []string{ 13 | "bad address syntax", 14 | }}, 15 | }, 16 | 502: ErrorSigns{ 17 | ErrorSign{TechnicalFailureBindingType, []string{ 18 | "syntax error", 19 | }}, 20 | }, 21 | 503: ErrorSigns{ 22 | ErrorSign{TechnicalFailureBindingType, []string{ 23 | "sender not yet given", 24 | "sender already", 25 | "bad sequence", 26 | "commands were rejected", 27 | "rcpt first", 28 | "rcpt command", 29 | "mail first", 30 | "mail command", 31 | "mail before", 32 | }}, 33 | ErrorSign{RecipientFailureBindingType, []string{ 34 | "account blocked", 35 | "user unknown", 36 | }}, 37 | }, 38 | 504: ErrorSigns{ 39 | ErrorSign{RecipientFailureBindingType, []string{ 40 | "mailbox is disabled", 41 | }}, 42 | }, 43 | 511: ErrorSigns{ 44 | ErrorSign{RecipientFailureBindingType, []string{ 45 | "can't lookup", 46 | }}, 47 | }, 48 | 540: ErrorSigns{ 49 | ErrorSign{RecipientFailureBindingType, []string{ 50 | "recipient address rejected", 51 | "account has been suspended", 52 | "account deleted", 53 | }}, 54 | }, 55 | 550: ErrorSigns{ 56 | ErrorSign{TechnicalFailureBindingType, []string{ 57 | "sender verify failed", 58 | "callout verification failed:", 59 | "relay", 60 | "verification failed", 61 | "unnecessary spaces", 62 | "host lookup failed", 63 | "client host rejected", 64 | "backresolv", 65 | "can't resolve hostname", 66 | "reverse", 67 | "authentication required", 68 | "bad commands", 69 | "double-checking", 70 | "system has detected that", 71 | "more information", 72 | "message has been blocked", 73 | "unsolicited mail", 74 | "blacklist", 75 | "black list", 76 | "not allowed to send", 77 | "dns operator", 78 | }}, 79 | ErrorSign{RecipientFailureBindingType, []string{ 80 | "unknown", 81 | "no such", 82 | "not exist", 83 | "disabled", 84 | "invalid mailbox", 85 | "not found", 86 | "mailbox unavailable", 87 | "has been suspended", 88 | "inactive", 89 | "account unavailable", 90 | "addresses failed", 91 | "mailbox is frozen", 92 | "address rejected", 93 | "administrative prohibition", 94 | "cannot deliver", 95 | "unrouteable address", 96 | "user banned", 97 | "policy rejection", 98 | "verify recipient", 99 | "mailbox locked", 100 | "blocked", 101 | "no mailbox", 102 | "bad destination mailbox", 103 | "not stored this user", 104 | "homo hominus", 105 | }}, 106 | ErrorSign{ConnectionFailureBindingType, []string{ 107 | "spam", 108 | "is full", 109 | "over quota", 110 | "quota exceeded", 111 | "message rejected", 112 | "was not accepted", 113 | "content denied", 114 | "timeout", 115 | "support.google.com", 116 | }}, 117 | }, 118 | 552: ErrorSigns{ 119 | ErrorSign{ConnectionFailureBindingType, []string{ 120 | "receiving disabled", 121 | "is full", 122 | "over quot", 123 | "to big", 124 | }}, 125 | }, 126 | 553: ErrorSigns{ 127 | ErrorSign{RecipientFailureBindingType, []string{ 128 | "list of allowed", 129 | "ecipient has been denied", 130 | }}, 131 | ErrorSign{TechnicalFailureBindingType, []string{ 132 | "relay", 133 | }}, 134 | ErrorSign{ConnectionFailureBindingType, []string{ 135 | "does not accept mail from", 136 | }}, 137 | }, 138 | 554: ErrorSigns{ 139 | ErrorSign{TechnicalFailureBindingType, []string{ 140 | "relay access denied", 141 | "unresolvable address", 142 | "blocked using", 143 | }}, 144 | ErrorSign{RecipientFailureBindingType, []string{ 145 | "recipient address rejected", 146 | "user doesn't have", 147 | "no such user", 148 | "inactive user", 149 | "user unknown", 150 | "has been disabled", 151 | "should log in", 152 | "no mailbox here", 153 | }}, 154 | ErrorSign{ConnectionFailureBindingType, []string{ 155 | "spam message rejected", 156 | "suspicion of spam", 157 | "synchronization error", 158 | "refused", 159 | }}, 160 | }, 161 | 571: ErrorSigns{ 162 | ErrorSign{ConnectionFailureBindingType, []string{ 163 | "relay", 164 | }}, 165 | }, 166 | 578: ErrorSigns{ 167 | ErrorSign{ConnectionFailureBindingType, []string{ 168 | "address rejected with reverse-check", 169 | }}, 170 | }, 171 | } 172 | ) 173 | 174 | // карта признаков ошибок, в качестве ключа используется код ошибки, полученной от почтового сервиса 175 | type ErrorSignsMap map[int]ErrorSigns 176 | 177 | // отдает идентификатор очереди, в которую необходимо положить письмо с ошибкой 178 | func (e ErrorSignsMap) BindingType(message *common.MailMessage) FailureBindingType { 179 | if signs, ok := e[message.Error.Code]; ok { 180 | return signs.BindingType(message) 181 | } else { 182 | return UnknownFailureBindingType 183 | } 184 | } 185 | 186 | // признаки ошибок 187 | type ErrorSigns []ErrorSign 188 | 189 | // отдает идентификатор очереди, в которую необходимо положить письмо с ошибкой 190 | func (e ErrorSigns) BindingType(message *common.MailMessage) FailureBindingType { 191 | bindingType := UnknownFailureBindingType 192 | for _, sign := range e { 193 | if sign.resemble(message) { 194 | bindingType = sign.bindingType 195 | break 196 | } 197 | } 198 | return bindingType 199 | } 200 | 201 | // признак ошибки 202 | type ErrorSign struct { 203 | // идентификатор очереди 204 | bindingType FailureBindingType 205 | 206 | // возможные части сообщения, по которым ошибка соотносится с очередью для ошибок 207 | parts []string 208 | } 209 | 210 | // ищет возможные части сообщения в сообщении ошибки 211 | func (e ErrorSign) resemble(message *common.MailMessage) bool { 212 | hasPart := false 213 | for _, part := range e.parts { 214 | if strings.Contains(message.Error.Message, part) { 215 | hasPart = true 216 | break 217 | } 218 | } 219 | return hasPart 220 | } 221 | -------------------------------------------------------------------------------- /connector/service.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "github.com/actionpay/postmanq/common" 8 | "github.com/actionpay/postmanq/logger" 9 | yaml "gopkg.in/yaml.v2" 10 | "io/ioutil" 11 | "net" 12 | "strings" 13 | ) 14 | 15 | var ( 16 | // сервис создания соединения 17 | service *Service 18 | 19 | // канал для приема событий отправки писем 20 | events = make(chan *common.SendEvent) 21 | 22 | // почтовые сервисы будут хранится в карте по домену 23 | mailServers = make(map[string]*MailServer) 24 | 25 | cipherSuites = []uint16{ 26 | tls.TLS_RSA_WITH_AES_128_CBC_SHA, 27 | tls.TLS_RSA_WITH_AES_256_CBC_SHA, 28 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 29 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 30 | tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 31 | tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 32 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 33 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 34 | } 35 | ) 36 | 37 | // сервис, управляющий соединениями к почтовым сервисам 38 | // письма могут отсылаться в несколько потоков, почтовый сервис может разрешить несколько подключений с одного IP 39 | // количество подключений может быть не равно количеству отсылающих потоков 40 | // если доверить управление подключениями отправляющим потокам, тогда это затруднит общее управление подключениями 41 | // поэтому создание подключений и предоставление имеющихся подключений отправляющим потокам вынесено в отдельный сервис 42 | type Service struct { 43 | // количество горутин устанавливающих соединения к почтовым сервисам 44 | ConnectorsCount int `yaml:"workers"` 45 | 46 | Configs map[string]*Config `yaml:"postmans"` 47 | } 48 | 49 | // создает новый сервис соединений 50 | func Inst() *Service { 51 | if service == nil { 52 | service = new(Service) 53 | } 54 | return service 55 | } 56 | 57 | // инициализирует сервис соединений 58 | func (s *Service) OnInit(event *common.ApplicationEvent) { 59 | err := yaml.Unmarshal(event.Data, s) 60 | if err == nil { 61 | for name, config := range s.Configs { 62 | s.init(config, name) 63 | } 64 | if s.ConnectorsCount == 0 { 65 | s.ConnectorsCount = common.DefaultWorkersCount 66 | } 67 | } else { 68 | logger.All().FailExit("connection service can't unmarshal config, error - %v", err) 69 | } 70 | } 71 | 72 | func (s *Service) init(conf *Config, hostname string) { 73 | // если указан путь до сертификата 74 | if len(conf.CertFilename) > 0 { 75 | conf.tlsConfig = &tls.Config{ 76 | ClientAuth: tls.RequireAndVerifyClientCert, 77 | CipherSuites: cipherSuites, 78 | MinVersion: tls.VersionTLS12, 79 | SessionTicketsDisabled: true, 80 | } 81 | 82 | // пытаемся прочитать сертификат 83 | pemBytes, err := ioutil.ReadFile(conf.CertFilename) 84 | if err == nil { 85 | // получаем сертификат 86 | pemBlock, _ := pem.Decode(pemBytes) 87 | cert, _ := x509.ParseCertificate(pemBlock.Bytes) 88 | pool := x509.NewCertPool() 89 | pool.AddCert(cert) 90 | conf.tlsConfig.RootCAs = pool 91 | conf.tlsConfig.ClientCAs = pool 92 | } else { 93 | logger.By(hostname).FailExit("connection service can't read certificate %s, error - %v", conf.CertFilename, err) 94 | } 95 | cert, err := tls.LoadX509KeyPair(conf.CertFilename, conf.PrivateKeyFilename) 96 | if err == nil { 97 | conf.tlsConfig.Certificates = []tls.Certificate{ 98 | cert, 99 | } 100 | } else { 101 | logger.By(hostname).FailExit("connection service can't load certificate %s, private key %s, error - %v", conf.CertFilename, conf.PrivateKeyFilename, err) 102 | } 103 | } else { 104 | logger.By(hostname).Debug("connection service - certificate is not defined") 105 | } 106 | conf.addressesLen = len(conf.Addresses) 107 | if conf.addressesLen == 0 { 108 | logger.By(hostname).FailExit("connection service - ips should be defined") 109 | } 110 | mxes, err := net.LookupMX(hostname) 111 | if err == nil { 112 | conf.hostname = strings.TrimRight(mxes[0].Host, ".") 113 | } else { 114 | logger.By(hostname).FailExit("connection service - can't lookup mx for %s", hostname) 115 | } 116 | } 117 | 118 | // запускает горутины 119 | func (s *Service) OnRun() { 120 | for i := 0; i < s.ConnectorsCount; i++ { 121 | id := i + 1 122 | go newPreparer(id) 123 | go newSeeker(id) 124 | go newConnector(id) 125 | } 126 | } 127 | 128 | // канал для приема событий отправки писем 129 | func (s *Service) Events() chan *common.SendEvent { 130 | return events 131 | } 132 | 133 | // завершает работу сервиса соединений 134 | func (s *Service) OnFinish() { 135 | close(events) 136 | } 137 | 138 | func (s Service) getTlsConfig(hostname string) *tls.Config { 139 | if conf, ok := s.Configs[hostname]; ok { 140 | //tlsConfig := new(tls.Config) 141 | //tlsConfig.Certificates = conf.certs 142 | //tlsConfig.RootCAs = conf.pool 143 | //tlsConfig.ClientCAs = conf.pool 144 | //tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert 145 | //tlsConfig.CipherSuites = cipherSuites 146 | //tlsConfig.MinVersion = tls.VersionTLS12 147 | //tlsConfig.SessionTicketsDisabled = true 148 | return conf.tlsConfig 149 | } else { 150 | logger.By(hostname).Err("connection service can't make tls config by %s", hostname) 151 | return nil 152 | } 153 | } 154 | 155 | func (s Service) getAddresses(hostname string) []string { 156 | if conf, ok := s.Configs[hostname]; ok { 157 | return conf.Addresses 158 | } else { 159 | logger.By(hostname).Err("connection service can't find ips by %s", hostname) 160 | return common.EmptyStrSlice 161 | } 162 | } 163 | 164 | func (s Service) getAddress(hostname string, id int) string { 165 | if conf, ok := s.Configs[hostname]; ok { 166 | return conf.Addresses[id%conf.addressesLen] 167 | } else { 168 | logger.By(hostname).Err("connection service can't find ip by %s", hostname) 169 | return common.EmptyStr 170 | } 171 | } 172 | 173 | func (s Service) getHostname(hostname string) string { 174 | if conf, ok := s.Configs[hostname]; ok { 175 | return conf.hostname 176 | } else { 177 | logger.By(hostname).Err("connection service can't find hostname by %s", hostname) 178 | return common.EmptyStr 179 | } 180 | } 181 | 182 | // событие создания соединения 183 | type ConnectionEvent struct { 184 | *common.SendEvent 185 | 186 | // канал для получения почтового сервиса после поиска информации о его серверах 187 | servers chan *MailServer 188 | 189 | // почтовый сервис, которому будет отправлено письмо 190 | server *MailServer 191 | 192 | // идентификатор заготовщика запросившего поиск информации о почтовом сервисе 193 | connectorId int 194 | 195 | // адрес, с которого будет отправлено письмо 196 | address string 197 | } 198 | 199 | type Config struct { 200 | // путь до файла с закрытым ключом 201 | PrivateKeyFilename string `yaml:"privateKey"` 202 | 203 | // путь до файла с сертификатом 204 | CertFilename string `yaml:"certificate"` 205 | 206 | // ip с которых будем рассылать письма 207 | Addresses []string `yaml:"ips"` 208 | 209 | // количество ip 210 | addressesLen int 211 | 212 | tlsConfig *tls.Config 213 | 214 | hostname string 215 | } 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #PostmanQ 2 | 3 | PostmanQ - это высокопроизводительный почтовый сервер(MTA). 4 | На сервере под управлением Ubuntu 12.04 с 8-ми ядерным процессором и 32ГБ оперативной памяти 5 | PostmanQ рассылает более 300 писем в секунду. 6 | 7 | Для работы PostmanQ потребуется AMQP-сервер, в котором будут храниться письма. 8 | 9 | PostmanQ разбирает одну или несколько очередей одного или нескольких AMQP-серверов с письмами и отправляет письма по SMTP сторонним почтовым сервисам. 10 | 11 | ##Возможности 12 | 13 | 1. PostmanQ может работать с несколькими AMQP-серверами и очередями каждого из серверов. 14 | 2. PostmanQ умеет работать через TLS соединение. 15 | 3. PostmanQ рассылает письма с разных IP для каждого из доменов. 16 | 4. PostmanQ подписывает DKIM для каждого письма разными ключами для каждого из доменов. 17 | 5. PostmanQ следит за количеством отправленных писем почтовому сервису. 18 | 6. PostmanQ исключает письма из рассылки по заданным доменам. 19 | 7. PostmanQ попробует отослать письмо попозже, если возникла сетевая ошибка, письмо попало в [серый список](http://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D1%80%D1%8B%D0%B9_%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA) или количество отправленных писем почтовому сервису уже максимально. 20 | 8. PostmanQ положит в отдельную очередь письма, которые не удалось отправить из-за 5ХХ ошибки 21 | 22 | ##Как это работает? 23 | 24 | 1. Нам потребуется AMQP-сервер, например [RabbitMQ](https://www.rabbitmq.com), и [go](http://golang.org/) для компиляции PostmanQ. 25 | 2. Выполняем предварительную подготовку и установку PostmanQ. Инструкции описаны ниже. 26 | 3. Запускаем RabbitMQ и PostmanQ. 27 | 4. Кладем в очередь письмо. Письмо должно быть в формате json и иметь вид 28 | 29 | { 30 | "envelope": "sender@mail.foo", 31 | "recipient": "recipient@mail.foo", 32 | "body": "письмо с заголовками и содержимым" 33 | } 34 | 35 | 5. PostmanQ забирает письмо из очереди. 36 | 6. Проверяет необходимо ли исключить письмо из рассылки по домену. 37 | 7. Проверяет ограничение на количество отправленных писем для почтового сервиса. 38 | 8. Открывает TLS или обычное соединение. 39 | 9. Создает DKIM. 40 | 10. Отправляет письмо стороннему почтовому сервису. 41 | 11. Если произошла сетевая ошибка, то письмо перекладывается в одну из очередей для повторной отправки. 42 | 12. Если произошла 5ХХ ошибка, то письмо перекладывается в очередь с проблемными письмами, повторная отправка не производится. 43 | 44 | ##Предварительная подготовка 45 | 46 | Чтобы наши письма отправлялись безопасно и доходили до адресатов, не попадая в спам, нам необходимо создать сертификат, публичный и закрытый ключ для каждого домена. 47 | 48 | Закрытый ключ будет использоваться для подписи DKIM. 49 | 50 | Публичный ключ необходимо указать в DNS записи для того, чтобы сторонние почтовые сервисы могли валидировать DKIM наших писем. 51 | 52 | Сертификат будет использоваться для создания TLS соединений к удаленным почтовым сервисами. 53 | 54 | cd /some/path 55 | # создаем корневой ключ 56 | openssl genrsa -out rootCA.key 2048 57 | # создаем корневой сертификат на 10000 дней 58 | openssl req -x509 -new -key rootCA.key -days 10000 -out rootCA.crt 59 | # создаем приватный ключ 60 | openssl genrsa -out private.key 2048 61 | # создаем запрос на сертификат 62 | openssl req -new -key private.key -out request.csr 63 | # подписываем сертификат 64 | openssl x509 -req -in request.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out example.crt -days 5000 65 | # создаем публичный ключ из приватного 66 | openssl rsa -in private.key -pubout > public.key 67 | 68 | Теперь необходимо настроить DNS. 69 | 70 | PostmanQ должен представляться в команде HELO/EHLO своим полным доменным именем(FQDN) почты. 71 | 72 | FQDN почты должно быть указано в A записи с внешним IP. 73 | 74 | PTR запись должна указывать на FQDN почты. 75 | 76 | MX запись должна указывать на FQDN почты. 77 | 78 | Также необходимо указать DKIM и SPF записи. 79 | 80 | mail.example.com. A 1.2.3.4 81 | 4.3.2.1.in-addr.arpa. IN PTR mail.example.com. 82 | _domainkey.example.com. TXT "t=s; o=~;" 83 | selector._domainkey.example.com. 3600 IN TXT "k=rsa\; t=s\; p=содержимое public.key" 84 | example.com. IN TXT "v=spf1 +a +mx ~all" 85 | 86 | Selector-ом может быть любым словом на латинице. Значение selector-а необходимо указать в настройках PostmanQ в поле dkimSelector. 87 | 88 | Если PTR запись отсутствует, то письма могут попадать в спам, либо почтовые сервисы могут отклонять отправку. 89 | 90 | Также необходимо увеличить количество открываемых файловых дескрипторов, иначе PostmanQ не сможет открывать новые соединения, и письма будут падать в одну из очередей для повторной отправки. 91 | 92 | Затем устанавливаем AMQP-сервер, например [RabbitMQ](https://www.rabbitmq.com). 93 | 94 | Теперь наши письма не будут попадать в спам, и все готово для установки PostmanQ. 95 | 96 | ##Установка 97 | 98 | Сначала уcтанавливаем [go](http://golang.org/doc/install). Затем устанавливаем PostmanQ: 99 | 100 | cd /some/path && mkdir postmanq && cd postmanq/ 101 | export GOPATH=/some/path/postmanq/ 102 | export GOBIN=/some/path/postmanq/bin/ 103 | go get -d github.com/actionpay/postmanq/cmd 104 | cd src/github.com/actionpay/postmanq 105 | git checkout v.3.1 106 | go install cmd/postmanq.go 107 | go install cmd/pmq-grep.go 108 | go install cmd/pmq-publish.go 109 | go install cmd/pmq-report.go 110 | ln -s /some/path/postmanq/bin/postmanq /usr/bin/ 111 | ln -s /some/path/postmanq/bin/pmq-grep /usr/bin/ 112 | ln -s /some/path/postmanq/bin/pmq-publish /usr/bin/ 113 | ln -s /some/path/postmanq/bin/pmq-report /usr/bin/ 114 | 115 | Затем берем из репозитория config.yaml и пишем свой файл с настройками. Все настройки подробно описаны в самом config.yaml. 116 | 117 | ##Использование 118 | 119 | sudo rabbitmq-server -detached 120 | postmanq -f /path/to/config.yaml 121 | 122 | ##Утилиты 123 | 124 | Для PostmanQ создано несколько утилит, призванных облегчить работу с логами и очередями рассылок - pmq-grep, pmq-publish, pmq-report. 125 | Вызов каждой из утилит без аргументов покажет ее использование. 126 | 127 | ###pmq-grep 128 | 129 | Если PostmanQ пишет логи в файл, то с помощью pmq-grep можно вытащить из лога все записи по определенному email получателя. 130 | 131 | ###pmq-publish 132 | 133 | Если вы что то не прописали в DNS, или операционная система не может открыть столько соединений, сколько необходимо для PostmanQ, то велика вероятность, 134 | что письма не будут отправляться, и PostmanQ будет складывать письма в очередь для ошибок или в одну из очередей для повторной отправки. 135 | После устранения проблемы, возможно, понадобится срочно разослать неотправленные письма. Как раз для этого и существует pmq-publish. 136 | С помощью pmq-publish можно переложить письма, например, из очереди для ошибок в очередь для отправки, отфильтровав письма по коду ошибки, полученной от почтового сервиса. 137 | 138 | ###pmq-report 139 | 140 | С помощью pmq-report можно посмотреть - по какой причине письмо попало в очередь для ошибок. -------------------------------------------------------------------------------- /cmd/push_mails.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/streadway/amqp" 7 | "sync" 8 | ) 9 | 10 | func main() { 11 | 12 | // amqpURI := "amqp://admin:admin0987654321@192.168.13.130:5672/postmanq" 13 | amqpURI := "amqp://solomonov:123123123@localhost:5672/postmanq" 14 | messageCount := 1 15 | // hasError := true 16 | hasError := false 17 | exchange := "postmanq" 18 | // exchange := "postmanq.failure.recipient" 19 | envelope := "robotron@ap-ok.ru" 20 | recipient := "3keltw+1191enyyarlwg@sharklasers.com" 21 | //recipient := "asolomonoff@gmail.com" 22 | // recipient := "byorty@yandex.ru"" 23 | // recipient := "byorty@mail.ru" 24 | // recipient := "byorty@fastmail.com" 25 | // recipient := "byorty@outlook.com" 26 | // recipient := "byorty@qip.ru" 27 | // recipient := "byorty@tut.by" 28 | // recipient := "asolomonoff@yahoo.com" 29 | // recipient := "byorty@nextmail.ru" 30 | // recipient := "byorty@rambler.ru" 31 | // recipient := "solomonov@km.ru" 32 | // recipient := "byorty@zmail.ru" 33 | // recipient := "byorty@meta.ua" 34 | // recipient := "byorty@e1.ru" 35 | // recipient := "byorty@inet.ua" 36 | // recipient := "recipient": "byorty@bigmir.net" 37 | // recipient := "byorty@chat.ru" 38 | 39 | message := `MIME-Version: 1.0 40 | Content-Type: multipart/mixed; 41 | boundary="=_cb8a36ec3e182808407241c0bfcb545b"; 42 | Content-Transfer-Encoding: 7bit 43 | Subject: =?utf-8?B?0JLQvtGB0YHRgtCw0L3QvtCy0LvQtdC90LjQtSDQv9Cw0YDQvtC70Y8g0L3QsCBhY3Rpb25wYXkucnU=?= 44 | To: =?utf-8?B?0KHQvtC70L7QvNC+0L3QvtCyINCQ0LvQtdC60YHQtdC5?= <` + recipient + `> 45 | From: =?utf-8?B?QWN0aW9ucGF5?= <` + envelope + `> 46 | Return-Path: ` + envelope + ` 47 | 48 | 49 | --=_cb8a36ec3e182808407241c0bfcb545b 50 | Content-Type: multipart/alternative; 51 | boundary="=_2898bd2e10dc12426ad35bd35ff604b6"; 52 | 53 | 54 | --=_2898bd2e10dc12426ad35bd35ff604b6 55 | Content-Type: text/plain;charset="utf-8"; 56 | Content-Transfer-Encoding: quoted-printable 57 | 58 | =d0=a7=d1=82=d0=be=d0=b1=d1=8b =d0=b2=d0=be=d1=81=d1=81=d1=82=d0=b0=d0=bd= 59 | =d0=be=d0=b2=d0=b8=d1=82=d1=8c =d0=bf=d0=b0=d1=80=d0=be=d0=bb=d1=8c =d0=bd= 60 | =d0=b0 =d1=81=d0=b0=d0=b9=d1=82=d0=b5 actionpay.ru, =d0=bf=d1=80=d0=be=d0= 61 | =b9=d0=b4=d0=b8=d1=82=d0=b5 =d0=bf=d0=be =d1=81=d1=81=d1=8b=d0=bb=d0=ba=d0= 62 | =b5: http://actionpay.ru/ru/forget/act:restore;token:34b0c8449ad2dfbc3e6bd94= 63 | 1f70cbe69=0a=d0=98=d0=bb=d0=b8 =d0=b2=d0=b2=d0=b5=d0=b4=d0=b8=d1=82=d0=b5 = 64 | =d0=b2=d1=80=d1=83=d1=87=d0=bd=d1=83=d1=8e =d0=ba=d0=be=d0=b4: 34b0c8449ad2d= 65 | fbc3e6bd941f70cbe69 =d0=bf=d1=80=d0=be=d0=b9=d0=b4=d1=8f =d0=bf=d0=be =d1= 66 | =81=d1=81=d1=8b=d0=bb=d0=ba=d0=b5: http://actionpay.ru/ru/forget/act:restore= 67 | =0a=d0=9d=d0=b0=d0=bf=d0=be=d0=bc=d0=b8=d0=bd=d0=b0=d0=b5=d0=bc, =d0=92=d0= 68 | =b0=d1=88 =d0=bb=d0=be=d0=b3=d0=b8=d0=bd: solomonov 69 | 70 | --=_2898bd2e10dc12426ad35bd35ff604b6 71 | Content-Type: text/html;charset="utf-8"; 72 | Content-Transfer-Encoding: quoted-printable 73 | 74 | =0a=0a =0a =d0=92=d0=be=d1=81=d1=81=d1=82=d0=b0=d0=bd=d0= 76 | =be=d0=b2=d0=bb=d0=b5=d0=bd=d0=b8=d0=b5 =d0=bf=d0=b0=d1=80=d0=be=d0=bb=d1= 77 | =8f =d0=bd=d0=b0 actionpay.ru=0a=0a=0a
=0a =0a
=0a
=0a

=d0=92=d0=be=d1=81=d1=81=d1=82=d0=b0=d0=bd=d0=be=d0=b2= 84 | =d0=bb=d0=b5=d0=bd=d0=b8=d0=b5 =d0=bf=d0=b0=d1=80=d0=be=d0=bb=d1=8f =d0=bd= 85 | =d0=b0 actionpay.ru

=0a

=0a =d0=a7=d1=82=d0=be=d0=b1=d1=8b =d0=b2= 86 | =d0=be=d1=81=d1=81=d1=82=d0=b0=d0=bd=d0=be=d0=b2=d0=b8=d1=82=d1=8c =d0=bf= 87 | =d0=b0=d1=80=d0=be=d0=bb=d1=8c =d0=bd=d0=b0 =d1=81=d0=b0=d0=b9=d1=82=d0=b5 a= 88 | ctionpay.ru, =d0=bf=d1=80=d0=be=d0=b9=d0=b4=d0=b8=d1=82=d0=b5 =d0=bf=d0=be = 89 | =d1=81=d1=81=d1=8b=d0=bb=d0=ba=d0=b5:
=0a http://ac= 91 | tionpay.ru/ru/forget/act:restore;token:34b0c8449ad2dfbc3e6bd941f70cbe69 = 92 |

=0a

=0a =d0=98=d0=bb=d0=b8 =d0=b2=d0=b2=d0=b5=d0=b4=d0=b8=d1=82=d0= 93 | =b5 =d0=b2=d1=80=d1=83=d1=87=d0=bd=d1=83=d1=8e =d0=ba=d0=be=d0=b4: 34b0c8= 94 | 449ad2dfbc3e6bd941f70cbe69
=0a =d0=bf=d1=80=d0=be=d0=b9=d0=b4= 95 | =d1=8f =d0=bf=d0=be =d1=81=d1=81=d1=8b=d0=bb=d0=ba=d0=b5:
=0a http://actionpay.ru/ru/forge= 97 | t/act:restore

=0a

=0a =d0=9d=d0=b0=d0=bf=d0=be=d0=bc=d0=b8=d0= 98 | =bd=d0=b0=d0=b5=d0=bc, =d0=92=d0=b0=d1=88 =d0=bb=d0=be=d0=b3=d0=b8=d0=bd: so= 99 | lomonov

=0a


=d0=9a=d1=80=d1=83=d0=b3=d0=bb=d0=be=d1=81=d1= 100 | =83=d1=82=d0=be=d1=87=d0=bd=d0=b0=d1=8f =d1=81=d0=bb=d1=83=d0=b6=d0=b1=d0= 101 | =b0 =d1=82=d0=b5=d1=85=d0=bd=d0=b8=d1=87=d0=b5=d1=81=d0=ba=d0=be=d0=b9 =d0= 102 | =bf=d0=be=d0=b4=d0=b4=d0=b5=d1=80=d0=b6=d0=ba=d0=b8 =d0=b2=d0=b5=d0=b1=d0= 103 | =bc=d0=b0=d1=81=d1=82=d0=b5=d1=80=d0=be=d0=b2:

=0a ICQ: 643-= 104 | 964-852
=0a Skype: actionpay24
=0a E-mail: support@actionpay.ru<= 105 | br />=0a

=0a

© 2010-2014, All rights reserved. Affiliate network =c2=abActionpay= 107 | =c2=bb

=0a
=0a=0a 108 | 109 | --=_2898bd2e10dc12426ad35bd35ff604b6-- 110 | 111 | --=_cb8a36ec3e182808407241c0bfcb545b-- 112 | ` 113 | 114 | fmt.Println("dialing ", amqpURI) 115 | connection, err := amqp.Dial(amqpURI) 116 | if err != nil { 117 | fmt.Errorf("Dial: %s", err) 118 | } 119 | defer connection.Close() 120 | 121 | fmt.Println("got Connection, getting Channel") 122 | channel, err := connection.Channel() 123 | if err != nil { 124 | fmt.Errorf("Channel: %s", err) 125 | } 126 | fmt.Println("got Channel") 127 | 128 | group := new(sync.WaitGroup) 129 | group.Add(messageCount) 130 | msg := map[string]interface{}{ 131 | "envelope": envelope, 132 | "recipient": recipient, 133 | "body": message, 134 | } 135 | if hasError { 136 | msg["error"] = map[string]interface{}{ 137 | "code": 551, 138 | "message": "unknown trololo", 139 | } 140 | } 141 | js, err := json.Marshal(msg) 142 | for i := 0; i < messageCount; i++ { 143 | go func() { 144 | if err = channel.Publish( 145 | exchange, 146 | "", // routing to 0 or more queues 147 | false, // mandatory 148 | false, // immediate 149 | amqp.Publishing{ 150 | Headers: amqp.Table{}, 151 | ContentType: "text/plain", 152 | ContentEncoding: "", 153 | Body: js, 154 | DeliveryMode: amqp.Transient, // 1=non-persistent, 2=persistent 155 | Priority: 0, // 0-9 156 | // a bunch of application/implementation-specific fields 157 | }, 158 | ); err != nil { 159 | fmt.Errorf("Exchange Publish: %s", err) 160 | } 161 | group.Done() 162 | }() 163 | } 164 | group.Wait() 165 | } 166 | -------------------------------------------------------------------------------- /analyser/service.go: -------------------------------------------------------------------------------- 1 | package analyser 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "github.com/actionpay/postmanq/common" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | var ( 16 | // сервис аналитики 17 | service = new(Service) 18 | 19 | // поля для агрегации по коду 20 | codeFields = []interface{}{ 21 | "Code", 22 | "Mails count", 23 | } 24 | 25 | // поля для агрегации по адресу 26 | addressFields = []interface{}{ 27 | "Address", 28 | "Mails count", 29 | } 30 | 31 | // поля для отчета 32 | detailFields = []interface{}{ 33 | "Envelope", 34 | "Recipient", 35 | "Code", 36 | "Message", 37 | "Sending count", 38 | } 39 | 40 | // автор таблицы с кодами 41 | codesWriter = newDetailTableWriter(detailFields) 42 | 43 | // автор таблицы с получателями 44 | recipientsWriter = newDetailTableWriter(detailFields) 45 | 46 | // автор таблицы с отправителями 47 | envelopesWriter = newDetailTableWriter(detailFields) 48 | 49 | // автор таблицы со всеми отчетами 50 | allWriter = newDetailTableWriter(detailFields) 51 | 52 | // автор агрегированной таблицы 53 | aggrWriter = newAggregateTableWriter([]interface{}{ 54 | "Mails count", 55 | "Code count", 56 | "Envelopes count", 57 | "Recipients count", 58 | }) 59 | ) 60 | 61 | // сервис получает и анализирует неотправленные письма 62 | type Service struct { 63 | // семафор 64 | mutex *sync.Mutex 65 | 66 | // канал для получения событий отправки 67 | events chan *common.SendEvent 68 | 69 | // отчеты 70 | reports RowWriters 71 | } 72 | 73 | // возвращает объект сервиса 74 | func Inst() *Service { 75 | return service 76 | } 77 | 78 | // инициализирует сервис 79 | func (s *Service) OnInit(event *common.ApplicationEvent) { 80 | s.events = make(chan *common.SendEvent) 81 | s.reports = make(RowWriters) 82 | s.mutex = new(sync.Mutex) 83 | } 84 | 85 | // запускает получение событий и данных от пользователя 86 | func (s *Service) OnShowReport() { 87 | for i := 0; i < common.DefaultWorkersCount; i++ { 88 | go s.receiveMessages() 89 | } 90 | scanner := bufio.NewScanner(os.Stdin) 91 | for scanner.Scan() { 92 | s.findReports(strings.Split(scanner.Text(), " ")) 93 | } 94 | } 95 | 96 | // слушает канал получения событий 97 | func (s *Service) receiveMessages() { 98 | for event := range s.events { 99 | s.receiveMessage(event) 100 | } 101 | } 102 | 103 | // получает событие 104 | func (s *Service) receiveMessage(event *common.SendEvent) { 105 | if event.Message == nil { 106 | close(s.events) 107 | s.findReports([]string{}) 108 | } else { 109 | var message = event.Message 110 | var report *Report 111 | 112 | // пытаемся потоко безопасно найти или создать отчет 113 | s.mutex.Lock() 114 | reportsLen := len(s.reports) 115 | for _, rawExistsReport := range s.reports { 116 | existsReport := rawExistsReport.(*Report) 117 | if existsReport.Envelope == message.Envelope && 118 | existsReport.Recipient == message.Recipient && 119 | existsReport.Code == message.Error.Code { 120 | report = existsReport 121 | break 122 | } 123 | } 124 | if report == nil { 125 | report = &Report{ 126 | Id: reportsLen + 1, 127 | Envelope: message.Envelope, 128 | Recipient: message.Recipient, 129 | Code: message.Error.Code, 130 | Message: message.Error.Message, 131 | } 132 | report.CreatedDates = make([]time.Time, 0) 133 | s.reports[report.Id] = report 134 | } 135 | 136 | report.CreatedDates = append(report.CreatedDates, message.CreatedDate) 137 | isValidCode := report.Code > 0 138 | code := strconv.Itoa(report.Code) 139 | 140 | if isValidCode { 141 | codesWriter.Add(code, report.Id) 142 | } 143 | envelopesWriter.Add(report.Envelope, report.Id) 144 | recipientsWriter.Add(report.Recipient, report.Id) 145 | s.mutex.Unlock() 146 | } 147 | } 148 | 149 | // принимает от пользователя команды из терминала и выводит соответствующую таблицу с данными 150 | func (s *Service) findReports(args []string) { 151 | var writer TableWriter 152 | var necessaryAll bool 153 | var necessaryCode string 154 | var necessaryEnvelope string 155 | var necessaryRecipient string 156 | var necessaryExport bool 157 | var necessaryOnly bool 158 | var pattern string 159 | var limit int 160 | var offset int 161 | 162 | flagSet := flag.NewFlagSet("service", flag.ContinueOnError) 163 | flagSet.BoolVar(&necessaryAll, "a", false, "show all reports") 164 | flagSet.StringVar(&necessaryCode, "c", common.InvalidInputString, "show reports by code") 165 | flagSet.StringVar(&necessaryEnvelope, "e", common.InvalidInputString, "show reports by envelope") 166 | flagSet.StringVar(&necessaryRecipient, "r", common.InvalidInputString, "show reports by recipient") 167 | flagSet.BoolVar(&necessaryExport, "E", false, "export addresses recipients") 168 | flagSet.BoolVar(&necessaryOnly, "O", false, "show codes or envelopes or recipients without reports") 169 | flagSet.StringVar(&pattern, "s", common.InvalidInputString, "search by envelope or recipient or mail body") 170 | flagSet.IntVar(&limit, "l", common.InvalidInputInt, "limit reports") 171 | flagSet.IntVar(&offset, "o", common.InvalidInputInt, "offset reports") 172 | err := flagSet.Parse(args) 173 | 174 | if err == nil { 175 | switch { 176 | case len(necessaryCode) > 0: 177 | if necessaryOnly { 178 | writer = newKeyAggregateTableWriter(codeFields) 179 | writer.Export(codesWriter) 180 | } else { 181 | writer = codesWriter 182 | writer.SetKeyPattern(necessaryCode) 183 | } 184 | case len(necessaryEnvelope) > 0: 185 | if necessaryOnly { 186 | writer = newKeyAggregateTableWriter(addressFields) 187 | writer.Export(envelopesWriter) 188 | } else { 189 | writer = envelopesWriter 190 | writer.SetKeyPattern(necessaryEnvelope) 191 | } 192 | case len(necessaryRecipient) > 0: 193 | if necessaryOnly { 194 | writer = newKeyAggregateTableWriter(addressFields) 195 | writer.Export(recipientsWriter) 196 | } else { 197 | writer = recipientsWriter 198 | writer.SetKeyPattern(necessaryRecipient) 199 | } 200 | case necessaryAll: 201 | writer = allWriter 202 | default: 203 | s.printUsage(flagSet) 204 | writer = aggrWriter 205 | } 206 | } else { 207 | s.printUsage(flagSet) 208 | writer = aggrWriter 209 | } 210 | 211 | if _, ok := writer.(*AggregateTableWriter); ok { 212 | writer.SetRows(RowWriters{ 213 | 1: AggregateRow{ 214 | len(s.reports), 215 | len(codesWriter.Ids()), 216 | len(envelopesWriter.Ids()), 217 | len(recipientsWriter.Ids()), 218 | }, 219 | }) 220 | } else { 221 | writer.SetLimit(limit) 222 | writer.SetNecessaryExport(necessaryExport) 223 | writer.SetOffset(offset) 224 | writer.SetRows(s.reports) 225 | writer.SetValuePattern(pattern) 226 | } 227 | writer.Show() 228 | } 229 | 230 | // выводит подсказку по работе с сервисом 231 | func (s *Service) printUsage(flagSet *flag.FlagSet) { 232 | fmt.Println() 233 | fmt.Println("Usage: -acer *|regex [-s] [-E] [-O] [-l] [-o]") 234 | flagSet.VisitAll(common.PrintUsage) 235 | fmt.Println("Example:") 236 | fmt.Println(" -c * -O show error codes without reports") 237 | fmt.Println(" -c 550 -l 100 show 100 reports with 550 error") 238 | fmt.Println(" -c 550 -s gmail.com show reports with 550 error and hostname gmail.com") 239 | fmt.Println(" -c * -l 100 -o 200 show reports with limit and offset") 240 | } 241 | 242 | // возвращает канал для отправки событий 243 | func (s *Service) Events() chan *common.SendEvent { 244 | return s.events 245 | } 246 | -------------------------------------------------------------------------------- /consumer/binding.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "fmt" 5 | "github.com/actionpay/postmanq/common" 6 | "github.com/actionpay/postmanq/logger" 7 | "github.com/streadway/amqp" 8 | "time" 9 | ) 10 | 11 | // тип точки обмена 12 | type ExchangeType string 13 | 14 | const ( 15 | DirectExchangeType ExchangeType = "direct" 16 | FanoutExchangeType = "fanout" 17 | TopicExchangeType = "topic" 18 | ) 19 | 20 | // тип точки обмена для неотправленного письма 21 | type FailureBindingType int 22 | 23 | const ( 24 | // проблемы с адресатом 25 | RecipientFailureBindingType FailureBindingType = iota 26 | 27 | // технические проблемы: неверная последовательность команд, косяки с dns 28 | TechnicalFailureBindingType 29 | 30 | // проблемы с подключеним к почтовому сервису 31 | ConnectionFailureBindingType 32 | 33 | // неизвестная проблема 34 | UnknownFailureBindingType 35 | ) 36 | 37 | var ( 38 | failureBindingTypeTplNames = map[FailureBindingType]string{ 39 | RecipientFailureBindingType: "%s.failure.recipient", 40 | TechnicalFailureBindingType: "%s.failure.technical", 41 | ConnectionFailureBindingType: "%s.failure.connection", 42 | UnknownFailureBindingType: "%s.failure.unknown", 43 | } 44 | 45 | // отложенные очереди вообще 46 | // письмо отправляется повторно при возниковении ошибки во время отправки 47 | delayedBindings = map[common.DelayedBindingType]*Binding{ 48 | common.SecondDelayedBinding: newDelayedBinding("%s.dlx.second", time.Second), 49 | common.ThirtySecondDelayedBinding: newDelayedBinding("%s.dlx.thirty.second", time.Second*30), 50 | common.MinuteDelayedBinding: newDelayedBinding("%s.dlx.minute", time.Minute), 51 | common.FiveMinutesDelayedBinding: newDelayedBinding("%s.dlx.five.minutes", time.Minute*5), 52 | common.TenMinutesDelayedBinding: newDelayedBinding("%s.dlx.ten.minutes", time.Minute*10), 53 | common.TwentyMinutesDelayedBinding: newDelayedBinding("%s.dlx.twenty.minutes", time.Minute*20), 54 | common.ThirtyMinutesDelayedBinding: newDelayedBinding("%s.dlx.thirty.minutes", time.Minute*30), 55 | common.FortyMinutesDelayedBinding: newDelayedBinding("%s.dlx.forty.minutes", time.Minute*40), 56 | common.FiftyMinutesDelayedBinding: newDelayedBinding("%s.dlx.fifty.minutes", time.Minute*50), 57 | common.HourDelayedBinding: newDelayedBinding("%s.dlx.hour", time.Hour), 58 | common.SixHoursDelayedBinding: newDelayedBinding("%s.dlx.six.hours", time.Hour*6), 59 | common.DayDelayedBinding: newDelayedBinding("%s.dlx.day", time.Hour*24), 60 | common.NotSendDelayedBinding: newBinding("%s.not.send"), 61 | } 62 | 63 | // отложенные очереди для лимитов 64 | limitBindings = []common.DelayedBindingType{ 65 | common.SecondDelayedBinding, 66 | common.MinuteDelayedBinding, 67 | common.HourDelayedBinding, 68 | common.DayDelayedBinding, 69 | } 70 | 71 | limitBindingsLen = len(limitBindings) 72 | 73 | // цепочка очередей, используемых для повторной отправки писем 74 | // в качестве ключа используется текущий тип очереди, а в качестве значения следующий 75 | bindingsChain = map[common.DelayedBindingType]common.DelayedBindingType{ 76 | common.UnknownDelayedBinding: common.SecondDelayedBinding, 77 | common.SecondDelayedBinding: common.ThirtySecondDelayedBinding, 78 | common.ThirtySecondDelayedBinding: common.MinuteDelayedBinding, 79 | common.MinuteDelayedBinding: common.FiveMinutesDelayedBinding, 80 | common.FiveMinutesDelayedBinding: common.TenMinutesDelayedBinding, 81 | common.TenMinutesDelayedBinding: common.TwentyMinutesDelayedBinding, 82 | common.TwentyMinutesDelayedBinding: common.ThirtyMinutesDelayedBinding, 83 | common.ThirtyMinutesDelayedBinding: common.FortyMinutesDelayedBinding, 84 | common.FortyMinutesDelayedBinding: common.FiftyMinutesDelayedBinding, 85 | common.FiftyMinutesDelayedBinding: common.HourDelayedBinding, 86 | common.HourDelayedBinding: common.SixHoursDelayedBinding, 87 | common.SixHoursDelayedBinding: common.NotSendDelayedBinding, 88 | } 89 | ) 90 | 91 | // связка точки обмена и очереди 92 | type Binding struct { 93 | // имя точки обмена и очереди 94 | Name string `yaml:"name"` 95 | 96 | // имя точки обмена 97 | Exchange string `yaml:"exchange"` 98 | 99 | // аргументы точки обмена 100 | ExchangeArgs amqp.Table 101 | 102 | // имя очереди 103 | Queue string `yaml:"queue"` 104 | 105 | // аргументы очереди 106 | QueueArgs amqp.Table 107 | 108 | // тип точки обмена 109 | Type ExchangeType `yaml:"type"` 110 | 111 | // ключ маршрутизации 112 | Routing string `yaml:"routing"` 113 | 114 | // количество потоков, разбирающих очередь 115 | Handlers int `yaml:"workers"` 116 | 117 | // количество сообщений, получаемых одновременно 118 | PrefetchCount int `yaml:"prefetchCount"` 119 | 120 | // отложенные очереди 121 | delayedBindings map[common.DelayedBindingType]*Binding 122 | 123 | // очереди для ошибок 124 | failureBindings map[FailureBindingType]*Binding 125 | } 126 | 127 | // создает связку обложенной точки обмена и очереди 128 | func newDelayedBinding(name string, duration time.Duration) *Binding { 129 | binding := newBinding(name) 130 | binding.QueueArgs = amqp.Table{ 131 | "x-message-ttl": int64(duration.Seconds()) * 1000, 132 | } 133 | return binding 134 | } 135 | 136 | // создает связку точки обмена и очереди 137 | func newBinding(name string) *Binding { 138 | return &Binding{Name: name} 139 | } 140 | 141 | // инициализирует связку параметрами по умолчанию 142 | func (b *Binding) init() { 143 | if len(b.Type) == 0 { 144 | b.Type = FanoutExchangeType 145 | } 146 | if len(b.Name) > 0 { 147 | b.Exchange = b.Name 148 | b.Queue = b.Name 149 | } 150 | // по умолчанию очередь разбирают столько рутин сколько ядер 151 | if b.Handlers == 0 { 152 | b.Handlers = common.DefaultWorkersCount 153 | } 154 | if b.PrefetchCount == 0 { 155 | b.PrefetchCount = 2 156 | } 157 | } 158 | 159 | // объявляет точку обмена и очередь и связывает их 160 | func (b *Binding) declare(channel *amqp.Channel) { 161 | err := channel.ExchangeDeclare( 162 | b.Exchange, // name of the exchange 163 | string(b.Type), // type 164 | true, // durable 165 | false, // delete when complete 166 | false, // internal 167 | false, // noWait 168 | b.ExchangeArgs, // arguments 169 | ) 170 | if err != nil { 171 | logger.All().FailExit("consumer can't declare exchange %s, error - %v", b.Exchange, err) 172 | } 173 | 174 | _, err = channel.QueueDeclare( 175 | b.Queue, // name of the queue 176 | true, // durable 177 | false, // delete when usused 178 | false, // exclusive 179 | false, // noWait 180 | b.QueueArgs, // arguments 181 | ) 182 | if err != nil { 183 | logger.All().FailExit("consumer can't declare queue %s, error - %v", b.Queue, err) 184 | } 185 | 186 | err = channel.QueueBind( 187 | b.Queue, // name of the queue 188 | b.Routing, // bindingKey 189 | b.Exchange, // sourceExchange 190 | false, // noWait 191 | nil, // arguments 192 | ) 193 | if err != nil { 194 | logger.All().FailExit("consumer can't bind queue %s to exchange %s, error - %v", b.Queue, b.Exchange, err) 195 | } 196 | } 197 | 198 | // объявляет отложенную точку обмена и очередь и связывает их 199 | func (b *Binding) declareDelayed(binding *Binding, channel *amqp.Channel) { 200 | b.Exchange = fmt.Sprintf(b.Name, binding.Exchange) 201 | b.Queue = fmt.Sprintf(b.Name, binding.Queue) 202 | if b.QueueArgs != nil { 203 | b.QueueArgs["x-dead-letter-exchange"] = binding.Exchange 204 | } 205 | b.Type = binding.Type 206 | b.declare(channel) 207 | } 208 | 209 | type AssistantBinding struct { 210 | Binding 211 | 212 | Dest map[string]string `yaml:"dest"` 213 | 214 | destBindings []*Binding 215 | } 216 | -------------------------------------------------------------------------------- /consumer/service.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "fmt" 5 | "github.com/actionpay/postmanq/common" 6 | "github.com/actionpay/postmanq/logger" 7 | "github.com/streadway/amqp" 8 | yaml "gopkg.in/yaml.v2" 9 | "net/url" 10 | "sync" 11 | ) 12 | 13 | var ( 14 | // сервис получения сообщений 15 | service common.SendingService 16 | 17 | // канал для получения событий 18 | events = make(chan *common.SendEvent) 19 | ) 20 | 21 | // сервис получения сообщений 22 | type Service struct { 23 | // настройка получателей сообщений 24 | Configs []*Config `yaml:"consumers"` 25 | 26 | // подключения к очередям 27 | connections map[string]*amqp.Connection 28 | 29 | // получатели сообщений из очереди 30 | consumers map[string][]*Consumer 31 | 32 | assistants map[string][]*Assistant 33 | } 34 | 35 | // создает новый сервис получения сообщений 36 | func Inst() common.SendingService { 37 | if service == nil { 38 | service := new(Service) 39 | service.connections = make(map[string]*amqp.Connection) 40 | service.consumers = make(map[string][]*Consumer) 41 | service.assistants = make(map[string][]*Assistant) 42 | return service 43 | } 44 | return service 45 | } 46 | 47 | // инициализирует сервис 48 | func (s *Service) OnInit(event *common.ApplicationEvent) { 49 | logger.All().Debug("init consumer service") 50 | // получаем настройки 51 | err := yaml.Unmarshal(event.Data, s) 52 | if err == nil { 53 | consumersCount := 0 54 | assistantsCount := 0 55 | for _, config := range s.Configs { 56 | connect, err := amqp.Dial(config.URI) 57 | if err == nil { 58 | channel, err := connect.Channel() 59 | if err == nil { 60 | consumers := make([]*Consumer, len(config.Bindings)) 61 | for i, binding := range config.Bindings { 62 | binding.init() 63 | // объявляем очередь 64 | binding.declare(channel) 65 | 66 | binding.delayedBindings = make(map[common.DelayedBindingType]*Binding) 67 | // объявляем отложенные очереди 68 | for delayedBindingType, delayedBinding := range delayedBindings { 69 | delayedBinding.declareDelayed(binding, channel) 70 | binding.delayedBindings[delayedBindingType] = delayedBinding 71 | } 72 | 73 | binding.failureBindings = make(map[FailureBindingType]*Binding) 74 | for failureBindingType, tplName := range failureBindingTypeTplNames { 75 | failureBinding := new(Binding) 76 | failureBinding.Exchange = fmt.Sprintf(tplName, binding.Exchange) 77 | failureBinding.Queue = fmt.Sprintf(tplName, binding.Queue) 78 | failureBinding.Type = binding.Type 79 | failureBinding.declare(channel) 80 | binding.failureBindings[failureBindingType] = failureBinding 81 | } 82 | 83 | consumersCount++ 84 | consumers[i] = NewConsumer(consumersCount, connect, binding) 85 | } 86 | assistants := make([]*Assistant, len(config.Assistants)) 87 | for i, assistantBinding := range config.Assistants { 88 | assistantBinding.init() 89 | // объявляем очередь 90 | assistantBinding.declare(channel) 91 | 92 | destBindings := make(map[string]*Binding) 93 | for domain, exchange := range assistantBinding.Dest { 94 | for _, consumer := range consumers { 95 | if consumer.binding.Exchange == exchange { 96 | destBindings[domain] = consumer.binding 97 | break 98 | } 99 | } 100 | } 101 | 102 | assistantsCount++ 103 | assistants[i] = &Assistant{ 104 | id: assistantsCount, 105 | connect: connect, 106 | srcBinding: assistantBinding, 107 | destBindings: destBindings, 108 | } 109 | } 110 | 111 | s.connections[config.URI] = connect 112 | s.consumers[config.URI] = consumers 113 | s.assistants[config.URI] = assistants 114 | // слушаем закрытие соединения 115 | s.reconnect(connect, config) 116 | } else { 117 | logger.All().FailExit("consumer service can't get channel to %s, error - %v", config.URI, err) 118 | } 119 | } else { 120 | logger.All().FailExit("consumer service can't connect to %s, error - %v", config.URI, err) 121 | } 122 | } 123 | } else { 124 | logger.All().FailExit("consumer service can't unmarshal config, error - %v", err) 125 | } 126 | } 127 | 128 | // объявляет слушателя закрытия соединения 129 | func (s *Service) reconnect(connect *amqp.Connection, config *Config) { 130 | closeErrors := connect.NotifyClose(make(chan *amqp.Error)) 131 | go s.notifyCloseError(config, closeErrors) 132 | } 133 | 134 | // слушает закрытие соединения 135 | func (s *Service) notifyCloseError(config *Config, closeErrors chan *amqp.Error) { 136 | for closeError := range closeErrors { 137 | logger.All().Warn("consumer service close connection %s with error - %v, restart...", config.URI, closeError) 138 | connect, err := amqp.Dial(config.URI) 139 | if err == nil { 140 | s.connections[config.URI] = connect 141 | closeErrors = nil 142 | if apps, ok := s.consumers[config.URI]; ok { 143 | for _, app := range apps { 144 | app.connect = connect 145 | } 146 | s.reconnect(connect, config) 147 | } 148 | logger.All().Debug("consumer service reconnect to amqp server %s", config.URI) 149 | } else { 150 | logger.All().Warn("consumer service can't reconnect to amqp server %s with error - %v", config.URI, err) 151 | } 152 | } 153 | } 154 | 155 | // запускает сервис 156 | func (s *Service) OnRun() { 157 | logger.All().Debug("run consumers...") 158 | for _, consumers := range s.consumers { 159 | s.runConsumers(consumers) 160 | } 161 | for _, assistants := range s.assistants { 162 | s.runAssistants(assistants) 163 | } 164 | } 165 | 166 | // запускает получателей 167 | func (s *Service) runConsumers(consumers []*Consumer) { 168 | for _, consumer := range consumers { 169 | go consumer.run() 170 | } 171 | } 172 | 173 | func (s *Service) runAssistants(assistants []*Assistant) { 174 | for _, assistant := range assistants { 175 | go assistant.run() 176 | } 177 | } 178 | 179 | // останавливает получателей 180 | func (s *Service) OnFinish() { 181 | logger.All().Debug("stop consumers...") 182 | for _, connect := range s.connections { 183 | if connect != nil { 184 | err := connect.Close() 185 | if err != nil { 186 | logger.All().WarnWithErr(err) 187 | } 188 | } 189 | } 190 | close(events) 191 | } 192 | 193 | // канал для приема событий отправки писем 194 | func (s *Service) Events() chan *common.SendEvent { 195 | return events 196 | } 197 | 198 | // запускает получение сообщений с ошибками и пересылает их другому сервису 199 | func (s *Service) OnShowReport() { 200 | waiter := newWaiter() 201 | group := new(sync.WaitGroup) 202 | 203 | var delta int 204 | for _, apps := range s.consumers { 205 | for _, app := range apps { 206 | delta += app.binding.Handlers 207 | } 208 | } 209 | group.Add(delta) 210 | for _, apps := range s.consumers { 211 | go func() { 212 | for _, app := range apps { 213 | for i := 0; i < app.binding.Handlers; i++ { 214 | go app.consumeFailureMessages(group) 215 | } 216 | } 217 | }() 218 | } 219 | group.Wait() 220 | waiter.Stop() 221 | 222 | sendEvent := common.NewSendEvent(nil) 223 | sendEvent.Iterator.Next().(common.ReportService).Events() <- sendEvent 224 | } 225 | 226 | // перекладывает сообщения из очереди в очередь 227 | func (s *Service) OnPublish(event *common.ApplicationEvent) { 228 | group := new(sync.WaitGroup) 229 | delta := 0 230 | for uri, apps := range s.consumers { 231 | var necessaryPublish bool 232 | if len(event.GetStringArg("host")) > 0 { 233 | parsedUri, err := url.Parse(uri) 234 | if err == nil && parsedUri.Host == event.GetStringArg("host") { 235 | necessaryPublish = true 236 | } else { 237 | necessaryPublish = false 238 | } 239 | } else { 240 | necessaryPublish = true 241 | } 242 | if necessaryPublish { 243 | for _, app := range apps { 244 | delta += app.binding.Handlers 245 | for i := 0; i < app.binding.Handlers; i++ { 246 | go app.consumeAndPublishMessages(event, group) 247 | } 248 | } 249 | } 250 | } 251 | group.Add(delta) 252 | group.Wait() 253 | fmt.Println("done") 254 | common.App.Events() <- common.NewApplicationEvent(common.FinishApplicationEventKind) 255 | } 256 | 257 | // получатель сообщений из очереди 258 | type Config struct { 259 | URI string `yaml:"uri"` 260 | Assistants []*AssistantBinding `yaml:"assistants"` 261 | Bindings []*Binding `yaml:"bindings"` 262 | } 263 | -------------------------------------------------------------------------------- /connector/connector.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/actionpay/postmanq/common" 7 | "github.com/actionpay/postmanq/logger" 8 | "net" 9 | "net/smtp" 10 | "time" 11 | ) 12 | 13 | var ( 14 | connectorEvents = make(chan *ConnectionEvent) 15 | ) 16 | 17 | // соединитель, устанавливает соединение к почтовому сервису 18 | type Connector struct { 19 | // Идентификатор для логов 20 | id int 21 | } 22 | 23 | // создает и запускает новый соединитель 24 | func newConnector(id int) { 25 | connector := &Connector{id} 26 | connector.run() 27 | } 28 | 29 | // запускает прослушивание событий создания соединений 30 | func (c *Connector) run() { 31 | for event := range connectorEvents { 32 | c.connect(event) 33 | } 34 | } 35 | 36 | // устанавливает соединение к почтовому сервису 37 | func (c *Connector) connect(event *ConnectionEvent) { 38 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d try find connection", c.id, event.Message.Id) 39 | goto receiveConnect 40 | 41 | receiveConnect: 42 | event.TryCount++ 43 | var targetClient *common.SmtpClient 44 | 45 | // смотрим все mx сервера почтового сервиса 46 | for _, mxServer := range event.server.mxServers { 47 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d try receive connection for %s", c.id, event.Message.Id, mxServer.hostname) 48 | 49 | // пробуем получить клиента 50 | event.Queue, _ = mxServer.queues[event.address] 51 | client := event.Queue.Pop() 52 | if client != nil { 53 | targetClient = client.(*common.SmtpClient) 54 | logger.By(event.Message.HostnameFrom).Debug("connector%d-%d found free smtp client#%d", c.id, event.Message.Id, targetClient.Id) 55 | } 56 | 57 | // создаем новое соединение к почтовому сервису 58 | // если не удалось найти клиента 59 | // или клиент разорвал соединение 60 | if (targetClient == nil && !event.Queue.HasLimit()) || 61 | (targetClient != nil && targetClient.Status == common.DisconnectedSmtpClientStatus) { 62 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d can't find free smtp client for %s", c.id, event.Message.Id, mxServer.hostname) 63 | c.createSmtpClient(mxServer, event, &targetClient) 64 | } 65 | 66 | if targetClient != nil { 67 | break 68 | } 69 | } 70 | 71 | // если клиент не создан, значит мы создали максимум соединений к почтовому сервису 72 | if targetClient == nil { 73 | // приостановим работу горутины 74 | goto waitConnect 75 | } else { 76 | targetClient.Wakeup() 77 | event.Client = targetClient 78 | // передаем событие отправителю 79 | event.Iterator.Next().(common.SendingService).Events() <- event.SendEvent 80 | } 81 | return 82 | 83 | waitConnect: 84 | if event.TryCount >= common.MaxTryConnectionCount { 85 | common.ReturnMail( 86 | event.SendEvent, 87 | errors.New(fmt.Sprintf("connector#%d can't connect to %s", c.id, event.Message.HostnameTo)), 88 | ) 89 | } else { 90 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d can't find free connections, wait...", c.id, event.Message.Id) 91 | time.Sleep(common.App.Timeout().Sleep) 92 | goto receiveConnect 93 | } 94 | return 95 | } 96 | 97 | // создает соединение к почтовому сервису 98 | func (c *Connector) createSmtpClient(mxServer *MxServer, event *ConnectionEvent, ptrSmtpClient **common.SmtpClient) { 99 | // устанавливаем ip, с которого бцдем отсылать письмо 100 | tcpAddr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(event.address, "0")) 101 | if err == nil { 102 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d resolve tcp address %s", c.id, event.Message.Id, tcpAddr.String()) 103 | dialer := &net.Dialer{ 104 | Timeout: common.App.Timeout().Connection, 105 | LocalAddr: tcpAddr, 106 | } 107 | hostname := net.JoinHostPort(mxServer.hostname, "25") 108 | // создаем соединение к почтовому сервису 109 | connection, err := dialer.Dial("tcp", hostname) 110 | if err == nil { 111 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d connect to %s", c.id, event.Message.Id, hostname) 112 | connection.SetDeadline(time.Now().Add(common.App.Timeout().Hello)) 113 | client, err := smtp.NewClient(connection, mxServer.hostname) 114 | if err == nil { 115 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d create client to %s", c.id, event.Message.Id, mxServer.hostname) 116 | err = client.Hello(service.getHostname(event.Message.HostnameFrom)) 117 | if err == nil { 118 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d send command HELLO: %s", c.id, event.Message.Id, event.Message.HostnameFrom) 119 | // проверяем доступно ли TLS 120 | if mxServer.useTLS { 121 | mxServer.useTLS, _ = client.Extension("STARTTLS") 122 | } 123 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d use TLS %v", c.id, event.Message.Id, mxServer.useTLS) 124 | // создаем TLS или обычное соединение 125 | if mxServer.useTLS { 126 | c.initTlsSmtpClient(mxServer, event, ptrSmtpClient, connection, client) 127 | } else { 128 | c.initSmtpClient(mxServer, event, ptrSmtpClient, connection, client) 129 | } 130 | } else { 131 | client.Quit() 132 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d can't create client to %s, err - %v", c.id, event.Message.Id, mxServer.hostname, err) 133 | } 134 | } else { 135 | // если не удалось создать клиента, 136 | // возможно, на почтовом сервисе стоит ограничение на количество активных клиентов 137 | // ставим лимит очереди, чтобы не пытаться открывать новые соединения и не создавать новые клиенты 138 | event.Queue.HasLimitOn() 139 | connection.Close() 140 | logger.By(event.Message.HostnameFrom).Warn("connector#%d-%d can't create client to %s, err - %v", c.id, event.Message.Id, mxServer.hostname, err) 141 | } 142 | } else { 143 | // если не удалось установить соединение, 144 | // возможно, на почтовом сервисе стоит ограничение на количество соединений 145 | // ставим лимит очереди, чтобы не пытаться открывать новые соединения 146 | event.Queue.HasLimitOn() 147 | logger.By(event.Message.HostnameFrom).Warn("connector#%d-%d can't dial to %s, err - %v", c.id, event.Message.Id, hostname, err) 148 | } 149 | } else { 150 | logger.By(event.Message.HostnameFrom).Warn("connector#%d-%d can't resolve tcp address %s, err - %v", c.id, event.Message.Id, tcpAddr.String(), err) 151 | } 152 | } 153 | 154 | // открывает защищенное соединение 155 | func (c *Connector) initTlsSmtpClient(mxServer *MxServer, event *ConnectionEvent, ptrSmtpClient **common.SmtpClient, connection net.Conn, client *smtp.Client) { 156 | // если есть какие данные о сертификате и к серверу можно создать TLS соединение 157 | conf := service.getTlsConfig(event.Message.HostnameFrom) 158 | if conf != nil && mxServer.useTLS { 159 | // открываем TLS соединение 160 | err := client.StartTLS(conf) 161 | // если все нормально, создаем клиента 162 | if err == nil { 163 | c.initSmtpClient(mxServer, event, ptrSmtpClient, connection, client) 164 | } else { 165 | // если не удалось создать TLS соединение 166 | // говорим, что не надо больше создавать TLS соединение 167 | mxServer.dontUseTLS() 168 | // разрываем созданое соединение 169 | // это необходимо, т.к. не все почтовые сервисы позволяют продолжить отправку письма 170 | // после неудачной попытке создать TLS соединение 171 | client.Quit() 172 | // создаем обычное соединие 173 | c.createSmtpClient(mxServer, event, ptrSmtpClient) 174 | } 175 | } else { 176 | c.initSmtpClient(mxServer, event, ptrSmtpClient, connection, client) 177 | } 178 | } 179 | 180 | // создает или инициализирует клиента 181 | func (c *Connector) initSmtpClient(mxServer *MxServer, event *ConnectionEvent, ptrSmtpClient **common.SmtpClient, connection net.Conn, client *smtp.Client) { 182 | isNil := *ptrSmtpClient == nil 183 | if isNil { 184 | var count int 185 | for _, queue := range mxServer.queues { 186 | count += queue.MaxLen() 187 | } 188 | *ptrSmtpClient = &common.SmtpClient{ 189 | Id: count + 1, 190 | } 191 | // увеличиваем максимальную длину очереди 192 | event.Queue.AddMaxLen() 193 | } 194 | smtpClient := *ptrSmtpClient 195 | smtpClient.Conn = connection 196 | smtpClient.Worker = client 197 | smtpClient.ModifyDate = time.Now() 198 | if isNil { 199 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d create smtp client#%d for %s", c.id, event.Message.Id, smtpClient.Id, mxServer.hostname) 200 | } else { 201 | logger.By(event.Message.HostnameFrom).Debug("connector#%d-%d reopen smtp client#%d for %s", c.id, event.Message.Id, smtpClient.Id, mxServer.hostname) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /recipient/state.go: -------------------------------------------------------------------------------- 1 | package recipient 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/actionpay/postmanq/common" 7 | "github.com/actionpay/postmanq/logger" 8 | "net/textproto" 9 | "strings" 10 | ) 11 | 12 | type StateStatus int 13 | 14 | const ( 15 | FailureStatus StateStatus = iota + 1 16 | ReadStatus 17 | WriteStatus 18 | PossibleStatus 19 | QuitStatus 20 | ) 21 | 22 | type StateError struct { 23 | message string 24 | } 25 | 26 | type State interface { 27 | SetEvent(*Event) 28 | GetNext() State 29 | SetNext(State) 30 | GetPossibles() []State 31 | SetPossibles([]State) 32 | Read(*textproto.Conn) []byte 33 | Process([]byte) StateStatus 34 | Write(*textproto.Conn) 35 | GetId() uint 36 | SetId(uint) 37 | IsUseCurrent() bool 38 | GetCmd() ([]byte, int) 39 | Check([]byte, []byte, int) bool 40 | GetError() *StateError 41 | } 42 | 43 | var ( 44 | crlf = "\r\n" 45 | greetResp = fmt.Sprintf("%d %s", ReadyCode, "%s ESMTP") 46 | ehloExtensions = []string{ 47 | fmt.Sprintf("%d-STARTTLS", CompleteCode), 48 | fmt.Sprintf("%d-SIZE", CompleteCode), 49 | fmt.Sprintf("%d HELP", CompleteCode), 50 | } 51 | ehloResp = fmt.Sprintf("%d-%s", CompleteCode, "%s ready to serve") + crlf + strings.Join(ehloExtensions, crlf) 52 | heloResp = fmt.Sprintf("%d %s", CompleteCode, "%s ready to serve") 53 | completeResp = fmt.Sprintf("%d OK", CompleteCode) 54 | startInputResp = fmt.Sprintf(StartInputCode.GetName(), StartInputCode) 55 | closeResp = CloseCode.GetName() 56 | syntaxErrorResp = SyntaxErrorCode.GetName() 57 | 58 | emptyCmd = []byte{} 59 | ehlo = []byte("EHLO") 60 | ehloLen = len(ehlo) 61 | helo = []byte("HELO") 62 | heloLen = len(helo) 63 | mailCmd = []byte("MAIL FROM:") 64 | mailCmdLen = len(mailCmd) 65 | rcptCmd = []byte("RCPT TO:") 66 | rcptCmdLen = len(rcptCmd) 67 | dataCmd = []byte("DATA") 68 | dataCmdLen = len(dataCmd) 69 | quitCmd = []byte("QUIT") 70 | quitCmdLen = len(quitCmd) 71 | noopCmd = []byte("NOOP") 72 | noopCmdLen = len(quitCmd) 73 | rsetCmd = []byte("RSET") 74 | rsetCmdLen = len(quitCmd) 75 | vrfyCmd = []byte("VRFY") 76 | vrfyCmdLen = len(quitCmd) 77 | ) 78 | 79 | type BaseState struct { 80 | event *Event 81 | next State 82 | possibles []State 83 | error *StateError 84 | id uint 85 | } 86 | 87 | func (b *BaseState) SetEvent(event *Event) { 88 | b.event = event 89 | } 90 | 91 | func (b BaseState) GetNext() State { 92 | return b.next 93 | } 94 | 95 | func (b *BaseState) SetNext(next State) { 96 | b.next = next 97 | } 98 | 99 | func (b BaseState) GetPossibles() []State { 100 | return b.possibles 101 | } 102 | 103 | func (b *BaseState) SetPossibles(possibles []State) { 104 | b.possibles = possibles 105 | } 106 | 107 | func (b BaseState) GetId() uint { 108 | return b.id 109 | } 110 | 111 | func (b *BaseState) SetId(id uint) { 112 | b.id = id 113 | } 114 | 115 | func (b BaseState) GetError() *StateError { 116 | return b.error 117 | } 118 | 119 | func (b BaseState) Check(line []byte, cmd []byte, cmdLen int) bool { 120 | return len(line) >= cmdLen && bytes.Equal(cmd, bytes.ToUpper(line[:cmdLen])) 121 | } 122 | 123 | func (b BaseState) Read(conn *textproto.Conn) []byte { 124 | line, err := conn.ReadLineBytes() 125 | if err == nil { 126 | return line 127 | } else { 128 | return nil 129 | } 130 | } 131 | 132 | func (b BaseState) IsUseCurrent() bool { 133 | return false 134 | } 135 | 136 | func (b *BaseState) wrongParams() StateStatus { 137 | b.error = &StateError{ 138 | message: SyntaxParamErrorCode.GetFormattedName(), 139 | } 140 | return FailureStatus 141 | } 142 | 143 | type ConnectState struct { 144 | BaseState 145 | } 146 | 147 | func (c *ConnectState) Read(conn *textproto.Conn) []byte { 148 | return nil 149 | } 150 | 151 | func (c *ConnectState) Process(line []byte) StateStatus { 152 | return WriteStatus 153 | } 154 | 155 | func (c *ConnectState) Write(conn *textproto.Conn) { 156 | conn.PrintfLine(greetResp, c.event.serverHostname) 157 | logger.By(c.event.serverHostname).Debug(greetResp, c.event.serverHostname) 158 | } 159 | 160 | func (c *ConnectState) GetCmd() ([]byte, int) { 161 | return emptyCmd, 0 162 | } 163 | 164 | type EhloState struct { 165 | BaseState 166 | useEhlo bool 167 | } 168 | 169 | func (e *EhloState) Check(line []byte, cmd []byte, cmdLen int) bool { 170 | isEhlo := e.BaseState.Check(line, ehlo, ehloLen) 171 | if isEhlo { 172 | e.useEhlo = true 173 | return isEhlo 174 | } else { 175 | return e.BaseState.Check(line, helo, heloLen) 176 | } 177 | } 178 | 179 | func (e *EhloState) GetCmd() ([]byte, int) { 180 | return ehlo, ehloLen 181 | } 182 | 183 | func (e *EhloState) Process(line []byte) StateStatus { 184 | var status StateStatus 185 | if e.useEhlo { 186 | status = e.receiveClientHostname(line, ehlo, ehloLen) 187 | } else { 188 | status = e.receiveClientHostname(line, helo, heloLen) 189 | } 190 | return status 191 | } 192 | 193 | func (e *EhloState) receiveClientHostname(line []byte, cmd []byte, cmdLen int) StateStatus { 194 | hostname := bytes.TrimSpace(line[cmdLen:]) 195 | if common.HostnameRegex.Match(hostname) { 196 | e.event.clientHostname = hostname 197 | return WriteStatus 198 | } else { 199 | return e.wrongParams() 200 | } 201 | } 202 | 203 | func (e *EhloState) Write(conn *textproto.Conn) { 204 | var resp string 205 | 206 | if e.useEhlo { 207 | resp = ehloResp 208 | } else { 209 | resp = heloResp 210 | } 211 | 212 | conn.PrintfLine(resp, e.event.serverMxHostname) 213 | logger.By(e.event.serverHostname).Debug(resp, e.event.serverMxHostname) 214 | } 215 | 216 | type MailState struct { 217 | BaseState 218 | } 219 | 220 | func (m *MailState) GetCmd() ([]byte, int) { 221 | return mailCmd, mailCmdLen 222 | } 223 | 224 | func (m *MailState) Process(line []byte) StateStatus { 225 | envelope := line[mailCmdLen+1 : len(line)-1] 226 | if common.EmailRegexp.Match(envelope) { 227 | m.event.message = &common.MailMessage{ 228 | Envelope: string(envelope), 229 | } 230 | return WriteStatus 231 | } else { 232 | return m.wrongParams() 233 | } 234 | } 235 | 236 | func (m *MailState) Write(conn *textproto.Conn) { 237 | conn.PrintfLine(completeResp) 238 | logger.By(m.event.serverHostname).Debug(completeResp) 239 | } 240 | 241 | type RcptState struct { 242 | BaseState 243 | } 244 | 245 | func (r *RcptState) GetCmd() ([]byte, int) { 246 | return rcptCmd, rcptCmdLen 247 | } 248 | 249 | func (r *RcptState) Process(line []byte) StateStatus { 250 | recipient := line[rcptCmdLen+1 : len(line)-1] 251 | if common.EmailRegexp.Match(recipient) { 252 | r.event.message.Recipient = string(recipient) 253 | return WriteStatus 254 | } else { 255 | return r.wrongParams() 256 | } 257 | } 258 | 259 | func (r *RcptState) Write(conn *textproto.Conn) { 260 | conn.PrintfLine(completeResp) 261 | logger.By(r.event.serverHostname).Debug(completeResp) 262 | } 263 | 264 | type DataState struct { 265 | BaseState 266 | } 267 | 268 | func (d *DataState) GetCmd() ([]byte, int) { 269 | return dataCmd, dataCmdLen 270 | } 271 | 272 | func (d *DataState) Process(line []byte) StateStatus { 273 | return WriteStatus 274 | } 275 | 276 | func (d *DataState) Write(conn *textproto.Conn) { 277 | conn.PrintfLine(startInputResp) 278 | logger.By(d.event.serverHostname).Debug(startInputResp) 279 | } 280 | 281 | type InputState struct { 282 | BaseState 283 | } 284 | 285 | func (i *InputState) Read(conn *textproto.Conn) []byte { 286 | line, err := conn.ReadDotBytes() 287 | if err == nil { 288 | return line 289 | } else { 290 | return nil 291 | } 292 | } 293 | 294 | func (i *InputState) GetCmd() ([]byte, int) { 295 | return emptyCmd, 0 296 | } 297 | 298 | func (i *InputState) Process(line []byte) StateStatus { 299 | i.event.message.Body = string(line) 300 | logger.By(i.event.serverHostname).Debug( 301 | "envelope: %s, recipient: %s, body: %s", 302 | i.event.message.Envelope, 303 | i.event.message.Recipient, 304 | i.event.message.Body, 305 | ) 306 | return WriteStatus 307 | } 308 | 309 | func (i *InputState) Write(conn *textproto.Conn) { 310 | conn.PrintfLine(completeResp) 311 | logger.By(i.event.serverHostname).Debug(completeResp) 312 | } 313 | 314 | type QuitState struct { 315 | BaseState 316 | } 317 | 318 | func (q *QuitState) GetCmd() ([]byte, int) { 319 | return quitCmd, quitCmdLen 320 | } 321 | 322 | func (q *QuitState) Process(line []byte) StateStatus { 323 | return QuitStatus 324 | } 325 | 326 | func (q *QuitState) Write(conn *textproto.Conn) { 327 | conn.PrintfLine(closeResp, CloseCode, q.event.serverMxHostname) 328 | logger.By(q.event.serverHostname).Debug(closeResp, CloseCode, q.event.serverMxHostname) 329 | conn.Close() 330 | } 331 | 332 | type NoopState struct { 333 | BaseState 334 | } 335 | 336 | func (n *NoopState) GetCmd() ([]byte, int) { 337 | return noopCmd, noopCmdLen 338 | } 339 | 340 | func (n *NoopState) Process(line []byte) StateStatus { 341 | return WriteStatus 342 | } 343 | 344 | func (n *NoopState) Write(conn *textproto.Conn) { 345 | conn.PrintfLine(completeResp) 346 | logger.By(n.event.serverHostname).Debug(completeResp) 347 | } 348 | 349 | func (n NoopState) IsUseCurrent() bool { 350 | return true 351 | } 352 | 353 | type RsetState struct { 354 | BaseState 355 | } 356 | 357 | func (r *RsetState) GetCmd() ([]byte, int) { 358 | return rsetCmd, rsetCmdLen 359 | } 360 | 361 | func (r *RsetState) Process(line []byte) StateStatus { 362 | r.event.message = nil 363 | return WriteStatus 364 | } 365 | 366 | func (r *RsetState) Write(conn *textproto.Conn) { 367 | conn.PrintfLine(completeResp) 368 | logger.By(r.event.serverHostname).Debug(completeResp) 369 | } 370 | 371 | type VrfyState struct { 372 | BaseState 373 | } 374 | 375 | func (v *VrfyState) GetCmd() ([]byte, int) { 376 | return vrfyCmd, vrfyCmdLen 377 | } 378 | 379 | func (v *VrfyState) Process(line []byte) StateStatus { 380 | if common.EmailRegexp.Match(line[vrfyCmdLen+1:]) { 381 | return WriteStatus 382 | } else { 383 | return FailureStatus 384 | } 385 | } 386 | 387 | func (v *VrfyState) Write(conn *textproto.Conn) { 388 | conn.PrintfLine(completeResp) 389 | logger.By(v.event.serverHostname).Debug(completeResp) 390 | } 391 | 392 | func (v VrfyState) IsUseCurrent() bool { 393 | return true 394 | } 395 | -------------------------------------------------------------------------------- /consumer/consumer.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/actionpay/postmanq/common" 7 | "github.com/actionpay/postmanq/logger" 8 | "github.com/streadway/amqp" 9 | "regexp" 10 | "sync" 11 | ) 12 | 13 | var ( 14 | // обработчики результата отправки письма 15 | resultHandlers = map[common.SendEventResult]func(*Consumer, *amqp.Channel, *common.MailMessage){ 16 | common.ErrorSendEventResult: (*Consumer).handleErrorSend, 17 | common.DelaySendEventResult: (*Consumer).handleDelaySend, 18 | common.OverlimitSendEventResult: (*Consumer).handleOverlimitSend, 19 | } 20 | ) 21 | 22 | // получатель сообщений из очереди 23 | type Consumer struct { 24 | id int 25 | connect *amqp.Connection 26 | binding *Binding 27 | deliveries <-chan amqp.Delivery 28 | } 29 | 30 | // создает нового получателя 31 | func NewConsumer(id int, connect *amqp.Connection, binding *Binding) *Consumer { 32 | app := new(Consumer) 33 | app.id = id 34 | app.connect = connect 35 | app.binding = binding 36 | return app 37 | } 38 | 39 | // запускает получение сообщений из очереди в заданное количество потоков 40 | func (c *Consumer) run() { 41 | for i := 0; i < c.binding.Handlers; i++ { 42 | go c.consume(i) 43 | } 44 | } 45 | 46 | // подключается к очереди для получения сообщений 47 | func (c *Consumer) consume(id int) { 48 | channel, err := c.connect.Channel() 49 | // выбираем из очереди сообщения с запасом 50 | // это нужно для того, чтобы после отправки письма новое уже было готово к отправке 51 | // в тоже время нельзя выбираеть все сообщения из очереди разом, т.к. можно упереться в память 52 | channel.Qos(c.binding.PrefetchCount, 0, false) 53 | deliveries, err := channel.Consume( 54 | c.binding.Queue, // name 55 | "", // consumerTag, 56 | false, // noAck 57 | false, // exclusive 58 | false, // noLocal 59 | false, // noWait 60 | nil, // arguments 61 | ) 62 | if err == nil { 63 | go c.consumeDeliveries(id, channel, deliveries) 64 | } else { 65 | logger.All().Warn("consumer#%d, handler#%d can't consume queue %s", c.id, id, c.binding.Queue) 66 | } 67 | } 68 | 69 | // получает сообщения из очереди и отправляет их другим сервисам 70 | func (c *Consumer) consumeDeliveries(id int, channel *amqp.Channel, deliveries <-chan amqp.Delivery) { 71 | for delivery := range deliveries { 72 | message := new(common.MailMessage) 73 | err := json.Unmarshal(delivery.Body, message) 74 | if err == nil { 75 | // инициализируем параметры письма 76 | message.Init() 77 | logger. 78 | By(message.HostnameFrom). 79 | Info( 80 | "consumer#%d-%d, handler#%d send mail#%d: envelope - %s, recipient - %s to mailer", 81 | c.id, 82 | message.Id, 83 | id, 84 | message.Id, 85 | message.Envelope, 86 | message.Recipient, 87 | ) 88 | 89 | event := common.NewSendEvent(message) 90 | logger.By(message.HostnameFrom).Debug("consumer#%d-%d send event", c.id, message.Id) 91 | event.Iterator.Next().(common.SendingService).Events() <- event 92 | // ждем результата, 93 | // во время ожидания поток блокируется 94 | // если этого не сделать, тогда невозможно будет подтвердить получение сообщения из очереди 95 | if handler, ok := resultHandlers[<-event.Result]; ok { 96 | handler(c, channel, message) 97 | } 98 | message = nil 99 | event = nil 100 | } else { 101 | failureBinding := c.binding.failureBindings[TechnicalFailureBindingType] 102 | err = channel.Publish( 103 | failureBinding.Exchange, 104 | failureBinding.Routing, 105 | false, 106 | false, 107 | amqp.Publishing{ 108 | ContentType: "text/plain", 109 | Body: delivery.Body, 110 | DeliveryMode: amqp.Transient, 111 | }, 112 | ) 113 | logger.All().Warn("consumer#%d can't unmarshal delivery body, body should be json, %s given", c.id, string(delivery.Body)) 114 | } 115 | // всегда подтверждаем получение сообщения 116 | // даже если во время отправки письма возникли ошибки, 117 | // мы уже положили это письмо в другую очередь 118 | delivery.Ack(true) 119 | } 120 | } 121 | 122 | // обрабатывает письма, которые не удалось отправить 123 | func (c *Consumer) handleErrorSend(channel *amqp.Channel, message *common.MailMessage) { 124 | // если есть ошибка при отправке, значит мы попали в серый список https://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D1%80%D1%8B%D0%B9_%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA 125 | // или получили какую то ошибку от почтового сервиса, что он не может 126 | // отправить письмо указанному адресату или выполнить какую то команду 127 | var failureBinding *Binding 128 | // если ошибка связана с невозможностью отправить письмо адресату 129 | // перекладываем письмо в очередь для плохих писем 130 | // и пусть отправители сами с ними разбираются 131 | if message.Error.Code >= 500 && message.Error.Code < 600 { 132 | failureBinding = c.binding.failureBindings[errorSignsMap.BindingType(message)] 133 | } else if message.Error.Code == 450 || message.Error.Code == 451 { // мы точно попали в серый список, надо повторить отправку письма попозже 134 | failureBinding = delayedBindings[common.ThirtyMinutesDelayedBinding] 135 | } else { 136 | failureBinding = c.binding.failureBindings[UnknownFailureBindingType] 137 | } 138 | jsonMessage, err := json.Marshal(message) 139 | if err == nil { 140 | // кладем в очередь 141 | err = channel.Publish( 142 | failureBinding.Exchange, 143 | failureBinding.Routing, 144 | false, 145 | false, 146 | amqp.Publishing{ 147 | ContentType: "text/plain", 148 | Body: jsonMessage, 149 | DeliveryMode: amqp.Transient, 150 | }, 151 | ) 152 | if err == nil { 153 | logger. 154 | By(message.HostnameFrom). 155 | Debug( 156 | "consumer#%d-%d publish failure mail to queue %s, message: %s, code: %d", 157 | c.id, 158 | message.Id, 159 | failureBinding.Queue, 160 | message.Error.Message, 161 | message.Error.Code, 162 | ) 163 | } else { 164 | logger. 165 | By(message.HostnameFrom). 166 | Debug( 167 | "consumer#%d-%d can't publish failure mail to queue %s, message: %s, code: %d, publish error% %v", 168 | c.id, 169 | message.Id, 170 | failureBinding.Queue, 171 | message.Error.Message, 172 | message.Error.Code, 173 | err, 174 | ) 175 | logger.By(message.HostnameFrom).WarnWithErr(err) 176 | } 177 | } else { 178 | logger.By(message.HostnameFrom).WarnWithErr(err) 179 | } 180 | } 181 | 182 | // обрабатывает письма, которые нужно отправить позже 183 | func (c *Consumer) handleDelaySend(channel *amqp.Channel, message *common.MailMessage) { 184 | logger. 185 | By(message.HostnameFrom). 186 | Debug( 187 | "consumer%d-%d find dlx queue", 188 | c.id, 189 | message.Id, 190 | ) 191 | bindingType := common.UnknownDelayedBinding 192 | if message.Error != nil { 193 | logger. 194 | By(message.HostnameFrom). 195 | Debug( 196 | "consumer%d-%d detect error, message: %s, code: %d", 197 | c.id, 198 | message.Id, 199 | message.Error.Message, 200 | message.Error.Code, 201 | ) 202 | } 203 | logger. 204 | By(message.HostnameFrom). 205 | Debug( 206 | "consumer%d-%d detect old dlx queue type#%v", 207 | c.id, 208 | message.Id, 209 | message.BindingType, 210 | ) 211 | // если нам просто не удалось отправить письмо, берем следующую очередь из цепочки 212 | if chainBinding, ok := bindingsChain[message.BindingType]; ok { 213 | bindingType = chainBinding 214 | } 215 | c.publishDelayedMessage(channel, bindingType, message) 216 | } 217 | 218 | // обрабатывает письма, которые превысили лимит отправки 219 | func (c *Consumer) handleOverlimitSend(channel *amqp.Channel, message *common.MailMessage) { 220 | bindingType := common.UnknownDelayedBinding 221 | logger.By(message.HostnameFrom).Debug("consumer#%d-%d detect overlimit, find dlx queue", c.id, message.Id) 222 | for i := 0; i < limitBindingsLen; i++ { 223 | if limitBindings[i] == message.BindingType { 224 | bindingType = limitBindings[i] 225 | break 226 | } 227 | } 228 | c.publishDelayedMessage(channel, bindingType, message) 229 | } 230 | 231 | // кладет письмо обратно в одну из отложенных очередей 232 | func (c *Consumer) publishDelayedMessage(channel *amqp.Channel, bindingType common.DelayedBindingType, message *common.MailMessage) { 233 | // получаем очередь, проверяем, что она реально есть 234 | // а что? а вдруг нет) 235 | if delayedBinding, ok := c.binding.delayedBindings[bindingType]; ok { 236 | message.BindingType = bindingType 237 | jsonMessage, err := json.Marshal(message) 238 | if err == nil { 239 | // кладем в очередь 240 | err = channel.Publish( 241 | delayedBinding.Exchange, 242 | delayedBinding.Routing, 243 | false, 244 | false, 245 | amqp.Publishing{ 246 | ContentType: "text/plain", 247 | Body: []byte(jsonMessage), 248 | DeliveryMode: amqp.Transient, 249 | }, 250 | ) 251 | if err == nil { 252 | logger.By(message.HostnameFrom).Debug("consumer#%d-%d publish failure mail to queue %s", c.id, message.Id, delayedBinding.Queue) 253 | } else { 254 | logger.All().Warn("consumer#%d-%d can't publish failure mail to queue %s, error - %v", c.id, message.Id, delayedBinding.Queue, err) 255 | } 256 | } else { 257 | logger.All().Warn("consumer#%d-%d can't marshal mail to json", c.id, message.Id) 258 | } 259 | } else { 260 | logger.All().Warn("consumer#%d-%d unknow delayed type#%v", c.id, message.Id, bindingType) 261 | } 262 | } 263 | 264 | // получает письма из всех очередей с ошибками 265 | func (c *Consumer) consumeFailureMessages(group *sync.WaitGroup) { 266 | channel, err := c.connect.Channel() 267 | if err == nil { 268 | for _, failureBinding := range c.binding.failureBindings { 269 | for { 270 | delivery, ok, _ := channel.Get(failureBinding.Queue, false) 271 | if ok { 272 | message := new(common.MailMessage) 273 | err = json.Unmarshal(delivery.Body, message) 274 | if err == nil { 275 | sendEvent := common.NewSendEvent(message) 276 | sendEvent.Iterator.Next().(common.ReportService).Events() <- sendEvent 277 | } 278 | } else { 279 | break 280 | } 281 | } 282 | } 283 | group.Done() 284 | } else { 285 | logger.All().WarnWithErr(err) 286 | } 287 | } 288 | 289 | // получает сообщения из одной очереди и кладет их в другую 290 | func (c *Consumer) consumeAndPublishMessages(event *common.ApplicationEvent, group *sync.WaitGroup) { 291 | channel, err := c.connect.Channel() 292 | if err == nil { 293 | var envelopeRegex, recipientRegex *regexp.Regexp 294 | srcBinding := c.findBindingByQueueName(event.GetStringArg("srcQueue")) 295 | if srcBinding == nil { 296 | fmt.Println("source queue should be defined") 297 | common.App.Events() <- common.NewApplicationEvent(common.FinishApplicationEventKind) 298 | } 299 | destBinding := c.findBindingByQueueName(event.GetStringArg("destQueue")) 300 | if destBinding == nil { 301 | fmt.Println("destination queue should be defined") 302 | common.App.Events() <- common.NewApplicationEvent(common.FinishApplicationEventKind) 303 | } 304 | if srcBinding == destBinding { 305 | fmt.Println("source and destination queue should be different") 306 | common.App.Events() <- common.NewApplicationEvent(common.FinishApplicationEventKind) 307 | } 308 | if len(event.GetStringArg("envelope")) > 0 { 309 | envelopeRegex, _ = regexp.Compile(event.GetStringArg("envelope")) 310 | } 311 | if len(event.GetStringArg("recipient")) > 0 { 312 | recipientRegex, _ = regexp.Compile(event.GetStringArg("recipient")) 313 | } 314 | 315 | publishDeliveries := make([]amqp.Delivery, 0) 316 | for { 317 | delivery, ok, _ := channel.Get(srcBinding.Queue, false) 318 | if ok { 319 | message := new(common.MailMessage) 320 | err = json.Unmarshal(delivery.Body, message) 321 | if err == nil { 322 | var necessaryPublish bool 323 | if (event.GetIntArg("code") > common.InvalidInputInt && event.GetIntArg("code") == message.Error.Code) || 324 | (envelopeRegex != nil && envelopeRegex.MatchString(message.Envelope)) || 325 | (recipientRegex != nil && recipientRegex.MatchString(message.Recipient)) || 326 | (event.GetIntArg("code") == common.InvalidInputInt && envelopeRegex == nil && recipientRegex == nil) { 327 | necessaryPublish = true 328 | } 329 | if necessaryPublish { 330 | fmt.Printf( 331 | "find mail#%d: envelope - %s, recipient - %s\n", 332 | message.Id, 333 | message.Envelope, 334 | message.Recipient, 335 | ) 336 | publishDeliveries = append(publishDeliveries, delivery) 337 | } 338 | } 339 | } else { 340 | break 341 | } 342 | } 343 | 344 | for _, delivery := range publishDeliveries { 345 | err = channel.Publish( 346 | destBinding.Exchange, 347 | destBinding.Routing, 348 | false, 349 | false, 350 | amqp.Publishing{ 351 | ContentType: "text/plain", 352 | Body: delivery.Body, 353 | DeliveryMode: amqp.Transient, 354 | }, 355 | ) 356 | if err == nil { 357 | delivery.Ack(true) 358 | } else { 359 | delivery.Nack(true, true) 360 | } 361 | } 362 | group.Done() 363 | } else { 364 | logger.All().WarnWithErr(err) 365 | } 366 | } 367 | 368 | // ищет связку по имени 369 | func (c *Consumer) findBindingByQueueName(queueName string) *Binding { 370 | var binding *Binding 371 | 372 | if c.binding.Queue == queueName { 373 | binding = c.binding 374 | } 375 | 376 | if binding == nil { 377 | for _, failureBinding := range c.binding.failureBindings { 378 | if failureBinding.Queue == queueName { 379 | binding = failureBinding 380 | break 381 | } 382 | } 383 | } 384 | 385 | if binding == nil { 386 | for _, delayedBinding := range c.binding.delayedBindings { 387 | if delayedBinding.Queue == queueName { 388 | binding = delayedBinding 389 | break 390 | } 391 | } 392 | } 393 | 394 | return binding 395 | } 396 | --------------------------------------------------------------------------------