github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/state/smtptest/types.go (about) 1 /* 2 * Copyright (c) 2022-present unTill Pro, Ltd. 3 */ 4 5 package smtptest 6 7 import ( 8 "io" 9 "strings" 10 11 "github.com/emersion/go-smtp" 12 ) 13 14 type Server interface { 15 Port() int32 16 Messages(username, password string) chan Message 17 Close() error 18 } 19 20 type server struct { 21 port int 22 messages map[credentials]chan Message 23 server *smtp.Server 24 } 25 26 func (s *server) Login(_ *smtp.ConnectionState, username, password string) (smtp.Session, error) { 27 ch, ok := s.messages[credentials{ 28 username: username, 29 password: password, 30 }] 31 if !ok { 32 return nil, errUnauthorized 33 } 34 return &session{ch: ch}, nil 35 } 36 func (s *server) AnonymousLogin(_ *smtp.ConnectionState) (smtp.Session, error) { 37 panic("anonymous login not allowed") 38 } 39 func (s *server) Port() int32 { return int32(s.port) } 40 func (s *server) Messages(username, password string) chan Message { 41 return s.messages[credentials{ 42 username: username, 43 password: password, 44 }] 45 } 46 func (s *server) Close() error { 47 for c := range s.messages { 48 close(s.messages[c]) 49 } 50 return s.server.Close() 51 } 52 53 type credentials struct { 54 username string 55 password string 56 } 57 58 type session struct { 59 ch chan Message 60 recipients []string 61 data string 62 } 63 64 func (s *session) Reset() {} 65 func (s *session) Logout() error { 66 s.ch <- s.message() 67 return nil 68 } 69 func (s *session) Mail(_ string, _ smtp.MailOptions) error { return nil } 70 func (s *session) Rcpt(to string) error { 71 s.recipients = append(s.recipients, to) 72 return nil 73 } 74 func (s *session) Data(r io.Reader) error { 75 bb, err := io.ReadAll(r) 76 if err != nil { 77 return err 78 } 79 s.data = string(bb) 80 return nil 81 } 82 func (s *session) message() Message { 83 msg := Message{ 84 ccMap: make(map[string]bool), 85 toMap: make(map[string]bool), 86 } 87 var bodyStartLine int 88 89 lines := strings.Split(s.data, "\r\n") 90 for i, line := range lines { 91 if line == "" { 92 bodyStartLine = i + 1 93 break 94 } 95 pair := strings.SplitN(line, ":", 2) 96 switch pair[0] { 97 case "Subject": 98 msg.Subject = strings.TrimSpace(pair[1]) 99 case "From": 100 msg.From = strings.Trim(pair[1], " <>") 101 case "To": 102 for _, to := range strings.Split(pair[1], ",") { 103 to = strings.Trim(to, " <>") 104 msg.To = append(msg.To, to) 105 msg.toMap[to] = true 106 } 107 case "Cc": 108 for _, cc := range strings.Split(pair[1], ",") { 109 cc = strings.Trim(cc, " <>") 110 msg.CC = append(msg.CC, cc) 111 msg.ccMap[cc] = true 112 } 113 } 114 } 115 116 for _, recipient := range s.recipients { 117 if msg.toMap[recipient] { 118 continue 119 } 120 if msg.ccMap[recipient] { 121 continue 122 } 123 msg.BCC = append(msg.BCC, recipient) 124 } 125 126 body := strings.Builder{} 127 for i := bodyStartLine; i < len(lines); i++ { 128 body.WriteString(lines[i]) 129 } 130 msg.Body = body.String() 131 132 return msg 133 } 134 135 type Message struct { 136 Subject string 137 From string 138 To []string 139 CC []string 140 BCC []string 141 Body string 142 ccMap map[string]bool 143 toMap map[string]bool 144 } 145 146 type Option func(s Server) 147 148 func WithCredentials(username, password string) Option { 149 return func(s Server) { 150 s.(*server).messages[credentials{ 151 username: username, 152 password: password, 153 }] = make(chan Message, defaultMessagesChannelSize) 154 } 155 }