parse.go raw

   1  package smtp
   2  
   3  import (
   4  	"fmt"
   5  	"strconv"
   6  	"strings"
   7  	"time"
   8  )
   9  
  10  // cutPrefixFold is a version of strings.CutPrefix which is case-insensitive.
  11  func cutPrefixFold(s, prefix string) (string, bool) {
  12  	if len(s) < len(prefix) || !strings.EqualFold(s[:len(prefix)], prefix) {
  13  		return "", false
  14  	}
  15  	return s[len(prefix):], true
  16  }
  17  
  18  func parseCmd(line string) (cmd string, arg string, err error) {
  19  	line = strings.TrimRight(line, "\r\n")
  20  
  21  	l := len(line)
  22  	switch {
  23  	case strings.HasPrefix(strings.ToUpper(line), "STARTTLS"):
  24  		return "STARTTLS", "", nil
  25  	case l == 0:
  26  		return "", "", nil
  27  	case l < 4:
  28  		return "", "", fmt.Errorf("command too short: %q", line)
  29  	case l == 4:
  30  		return strings.ToUpper(line), "", nil
  31  	case l == 5:
  32  		// Too long to be only command, too short to have args
  33  		return "", "", fmt.Errorf("mangled command: %q", line)
  34  	}
  35  
  36  	// If we made it here, command is long enough to have args
  37  	if line[4] != ' ' {
  38  		// There wasn't a space after the command?
  39  		return "", "", fmt.Errorf("mangled command: %q", line)
  40  	}
  41  
  42  	return strings.ToUpper(line[0:4]), strings.TrimSpace(line[5:]), nil
  43  }
  44  
  45  // Takes the arguments proceeding a command and files them
  46  // into a map[string]string after uppercasing each key.  Sample arg
  47  // string:
  48  //
  49  //	" BODY=8BITMIME SIZE=1024 SMTPUTF8"
  50  //
  51  // The leading space is mandatory.
  52  func parseArgs(s string) (map[string]string, error) {
  53  	argMap := map[string]string{}
  54  	for _, arg := range strings.Fields(s) {
  55  		m := strings.Split(arg, "=")
  56  		switch len(m) {
  57  		case 2:
  58  			argMap[strings.ToUpper(m[0])] = m[1]
  59  		case 1:
  60  			argMap[strings.ToUpper(m[0])] = ""
  61  		default:
  62  			return nil, fmt.Errorf("failed to parse arg string: %q", arg)
  63  		}
  64  	}
  65  	return argMap, nil
  66  }
  67  
  68  func parseHelloArgument(arg string) (string, error) {
  69  	domain := arg
  70  	if idx := strings.IndexRune(arg, ' '); idx >= 0 {
  71  		domain = arg[:idx]
  72  	}
  73  	if domain == "" {
  74  		return "", fmt.Errorf("invalid domain")
  75  	}
  76  	return domain, nil
  77  }
  78  
  79  // Parses the BY argument defined in RFC2852 section 4.
  80  // Returns pointer to options or nil if invalid.
  81  func parseDeliverByArgument(arg string) *DeliverByOptions {
  82  	secondsStr, modeStr, ok := strings.Cut(arg, ";")
  83  	if !ok {
  84  		return nil
  85  	}
  86  	modeStr, traceValue := strings.CutSuffix(modeStr, "T")
  87  	if modeStr != string(DeliverByNotify) && modeStr != string(DeliverByReturn) {
  88  		return nil
  89  	}
  90  	modeValue := DeliverByMode(modeStr)
  91  	secondsValue, err := strconv.Atoi(secondsStr)
  92  	if err != nil || (modeValue == DeliverByReturn && secondsValue < 1) {
  93  		return nil
  94  	}
  95  	return &DeliverByOptions{
  96  		Time:  time.Duration(secondsValue) * time.Second,
  97  		Mode:  modeValue,
  98  		Trace: traceValue,
  99  	}
 100  }
 101  
 102  // parser parses command arguments defined in RFC 5321 section 4.1.2.
 103  type parser struct {
 104  	s string
 105  }
 106  
 107  func (p *parser) peekByte() (byte, bool) {
 108  	if len(p.s) == 0 {
 109  		return 0, false
 110  	}
 111  	return p.s[0], true
 112  }
 113  
 114  func (p *parser) readByte() (byte, bool) {
 115  	ch, ok := p.peekByte()
 116  	if ok {
 117  		p.s = p.s[1:]
 118  	}
 119  	return ch, ok
 120  }
 121  
 122  func (p *parser) acceptByte(ch byte) bool {
 123  	got, ok := p.peekByte()
 124  	if !ok || got != ch {
 125  		return false
 126  	}
 127  	p.readByte()
 128  	return true
 129  }
 130  
 131  func (p *parser) expectByte(ch byte) error {
 132  	if !p.acceptByte(ch) {
 133  		if len(p.s) == 0 {
 134  			return fmt.Errorf("expected '%v', got EOF", string(ch))
 135  		} else {
 136  			return fmt.Errorf("expected '%v', got '%v'", string(ch), string(p.s[0]))
 137  		}
 138  	}
 139  	return nil
 140  }
 141  
 142  func (p *parser) parseReversePath() (string, error) {
 143  	if strings.HasPrefix(p.s, "<>") {
 144  		p.s = strings.TrimPrefix(p.s, "<>")
 145  		return "", nil
 146  	}
 147  	return p.parsePath()
 148  }
 149  
 150  func (p *parser) parsePath() (string, error) {
 151  	hasBracket := p.acceptByte('<')
 152  	if p.acceptByte('@') {
 153  		i := strings.IndexByte(p.s, ':')
 154  		if i < 0 {
 155  			return "", fmt.Errorf("malformed a-d-l")
 156  		}
 157  		p.s = p.s[i+1:]
 158  	}
 159  	mbox, err := p.parseMailbox()
 160  	if err != nil {
 161  		return "", fmt.Errorf("in mailbox: %v", err)
 162  	}
 163  	if hasBracket {
 164  		if err := p.expectByte('>'); err != nil {
 165  			return "", err
 166  		}
 167  	}
 168  	return mbox, nil
 169  }
 170  
 171  func (p *parser) parseMailbox() (string, error) {
 172  	localPart, err := p.parseLocalPart()
 173  	if err != nil {
 174  		return "", fmt.Errorf("in local-part: %v", err)
 175  	} else if localPart == "" {
 176  		return "", fmt.Errorf("local-part is empty")
 177  	}
 178  
 179  	if err := p.expectByte('@'); err != nil {
 180  		return "", err
 181  	}
 182  
 183  	var sb strings.Builder
 184  	sb.WriteString(localPart)
 185  	sb.WriteByte('@')
 186  
 187  	for {
 188  		ch, ok := p.peekByte()
 189  		if !ok {
 190  			break
 191  		}
 192  		if ch == ' ' || ch == '\t' || ch == '>' {
 193  			break
 194  		}
 195  		p.readByte()
 196  		sb.WriteByte(ch)
 197  	}
 198  
 199  	if strings.HasSuffix(sb.String(), "@") {
 200  		return "", fmt.Errorf("domain is empty")
 201  	}
 202  
 203  	return sb.String(), nil
 204  }
 205  
 206  func (p *parser) parseLocalPart() (string, error) {
 207  	var sb strings.Builder
 208  
 209  	if p.acceptByte('"') { // quoted-string
 210  		for {
 211  			ch, ok := p.readByte()
 212  			switch ch {
 213  			case '\\':
 214  				ch, ok = p.readByte()
 215  			case '"':
 216  				return sb.String(), nil
 217  			}
 218  			if !ok {
 219  				return "", fmt.Errorf("malformed quoted-string")
 220  			}
 221  			sb.WriteByte(ch)
 222  		}
 223  	} else { // dot-string
 224  		for {
 225  			ch, ok := p.peekByte()
 226  			if !ok {
 227  				return sb.String(), nil
 228  			}
 229  			switch ch {
 230  			case '@':
 231  				return sb.String(), nil
 232  			case '(', ')', '<', '>', '[', ']', ':', ';', '\\', ',', '"', ' ', '\t':
 233  				return "", fmt.Errorf("malformed dot-string")
 234  			}
 235  			p.readByte()
 236  			sb.WriteByte(ch)
 237  		}
 238  	}
 239  }
 240