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  }