mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
- Rename BUILD_DATE to BUILDDATE in goxc - Update travis config - Follow linter recommendations for inbucket.go - Follow linter recommendations for config package - Follow linter recommendations for log package - Follow linter recommendations for pop3d package - Follow linter recommendations for smtpd package - Follow linter recommendations for web package - Fix Id -> ID in templates - Add shebang to REST demo scripts - Add or refine many comments
219 lines
5.3 KiB
Go
219 lines
5.3 KiB
Go
package smtpd
|
|
|
|
import (
|
|
"bytes"
|
|
"container/list"
|
|
"crypto/sha1"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
// ParseMailboxName takes a localPart string (ex: "user+ext" without "@domain")
|
|
// and returns just the mailbox name (ex: "user"). Returns an error if
|
|
// localPart contains invalid characters; it won't accept any that must be
|
|
// quoted according to RFC3696.
|
|
func ParseMailboxName(localPart string) (result string, err error) {
|
|
if localPart == "" {
|
|
return "", fmt.Errorf("Mailbox name cannot be empty")
|
|
}
|
|
result = strings.ToLower(localPart)
|
|
|
|
invalid := make([]byte, 0, 10)
|
|
|
|
for i := 0; i < len(result); i++ {
|
|
c := result[i]
|
|
switch {
|
|
case 'a' <= c && c <= 'z':
|
|
case '0' <= c && c <= '9':
|
|
case bytes.IndexByte([]byte("!#$%&'*+-=/?^_`.{|}~"), c) >= 0:
|
|
default:
|
|
invalid = append(invalid, c)
|
|
}
|
|
}
|
|
|
|
if len(invalid) > 0 {
|
|
return "", fmt.Errorf("Mailbox name contained invalid character(s): %q", invalid)
|
|
}
|
|
|
|
if idx := strings.Index(result, "+"); idx > -1 {
|
|
result = result[0:idx]
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// HashMailboxName accepts a mailbox name and hashes it. Inbucket uses this as
|
|
// the directory to house the mailbox
|
|
func HashMailboxName(mailbox string) string {
|
|
h := sha1.New()
|
|
if _, err := io.WriteString(h, mailbox); err != nil {
|
|
// This shouldn't ever happen
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%x", h.Sum(nil))
|
|
}
|
|
|
|
// JoinStringList joins a List containing strings by commas
|
|
func JoinStringList(listOfStrings *list.List) string {
|
|
if listOfStrings.Len() == 0 {
|
|
return ""
|
|
}
|
|
s := make([]string, 0, listOfStrings.Len())
|
|
for e := listOfStrings.Front(); e != nil; e = e.Next() {
|
|
s = append(s, e.Value.(string))
|
|
}
|
|
return strings.Join(s, ",")
|
|
}
|
|
|
|
// ValidateDomainPart returns true if the domain part complies to RFC3696, RFC1035
|
|
func ValidateDomainPart(domain string) bool {
|
|
if len(domain) == 0 {
|
|
return false
|
|
}
|
|
if len(domain) > 255 {
|
|
return false
|
|
}
|
|
if domain[len(domain)-1] != '.' {
|
|
domain += "."
|
|
}
|
|
prev := '.'
|
|
labelLen := 0
|
|
hasAlphaNum := false
|
|
|
|
for _, c := range domain {
|
|
switch {
|
|
case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
|
|
('0' <= c && c <= '9') || c == '_':
|
|
// Must contain some of these to be a valid label
|
|
hasAlphaNum = true
|
|
labelLen++
|
|
case c == '-':
|
|
if prev == '.' {
|
|
// Cannot lead with hyphen
|
|
return false
|
|
}
|
|
case c == '.':
|
|
if prev == '.' || prev == '-' {
|
|
// Cannot end with hyphen or double-dot
|
|
return false
|
|
}
|
|
if labelLen > 63 {
|
|
return false
|
|
}
|
|
if !hasAlphaNum {
|
|
return false
|
|
}
|
|
labelLen = 0
|
|
hasAlphaNum = false
|
|
default:
|
|
// Unknown character
|
|
return false
|
|
}
|
|
prev = c
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ParseEmailAddress unescapes an email address, and splits the local part from the domain part.
|
|
// An error is returned if the local or domain parts fail validation following the guidelines
|
|
// in RFC3696.
|
|
func ParseEmailAddress(address string) (local string, domain string, err error) {
|
|
if address == "" {
|
|
return "", "", fmt.Errorf("Empty address")
|
|
}
|
|
if len(address) > 320 {
|
|
return "", "", fmt.Errorf("Address exceeds 320 characters")
|
|
}
|
|
if address[0] == '@' {
|
|
return "", "", fmt.Errorf("Address cannot start with @ symbol")
|
|
}
|
|
if address[0] == '.' {
|
|
return "", "", fmt.Errorf("Address cannot start with a period")
|
|
}
|
|
|
|
// Loop over address parsing out local part
|
|
buf := new(bytes.Buffer)
|
|
prev := byte('.')
|
|
inCharQuote := false
|
|
inStringQuote := false
|
|
LOOP:
|
|
for i := 0; i < len(address); i++ {
|
|
c := address[i]
|
|
switch {
|
|
case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'):
|
|
// Letters are OK
|
|
_ = buf.WriteByte(c)
|
|
inCharQuote = false
|
|
case '0' <= c && c <= '9':
|
|
// Numbers are OK
|
|
_ = buf.WriteByte(c)
|
|
inCharQuote = false
|
|
case bytes.IndexByte([]byte("!#$%&'*+-/=?^_`{|}~"), c) >= 0:
|
|
// These specials can be used unquoted
|
|
_ = buf.WriteByte(c)
|
|
inCharQuote = false
|
|
case c == '.':
|
|
// A single period is OK
|
|
if prev == '.' {
|
|
// Sequence of periods is not permitted
|
|
return "", "", fmt.Errorf("Sequence of periods is not permitted")
|
|
}
|
|
_ = buf.WriteByte(c)
|
|
inCharQuote = false
|
|
case c == '\\':
|
|
inCharQuote = true
|
|
case c == '"':
|
|
if inCharQuote {
|
|
_ = buf.WriteByte(c)
|
|
inCharQuote = false
|
|
} else if inStringQuote {
|
|
inStringQuote = false
|
|
} else {
|
|
if i == 0 {
|
|
inStringQuote = true
|
|
} else {
|
|
return "", "", fmt.Errorf("Quoted string can only begin at start of address")
|
|
}
|
|
}
|
|
case c == '@':
|
|
if inCharQuote || inStringQuote {
|
|
_ = buf.WriteByte(c)
|
|
inCharQuote = false
|
|
} else {
|
|
// End of local-part
|
|
if i > 63 {
|
|
return "", "", fmt.Errorf("Local part must not exceed 64 characters")
|
|
}
|
|
if prev == '.' {
|
|
return "", "", fmt.Errorf("Local part cannot end with a period")
|
|
}
|
|
domain = address[i+1:]
|
|
break LOOP
|
|
}
|
|
case c > 127:
|
|
return "", "", fmt.Errorf("Characters outside of US-ASCII range not permitted")
|
|
default:
|
|
if inCharQuote || inStringQuote {
|
|
_ = buf.WriteByte(c)
|
|
inCharQuote = false
|
|
} else {
|
|
return "", "", fmt.Errorf("Character %q must be quoted", c)
|
|
}
|
|
}
|
|
prev = c
|
|
}
|
|
if inCharQuote {
|
|
return "", "", fmt.Errorf("Cannot end address with unterminated quoted-pair")
|
|
}
|
|
if inStringQuote {
|
|
return "", "", fmt.Errorf("Cannot end address with unterminated string quote")
|
|
}
|
|
|
|
if !ValidateDomainPart(domain) {
|
|
return "", "", fmt.Errorf("Domain part validation failed")
|
|
}
|
|
|
|
return buf.String(), domain, nil
|
|
}
|