mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-25 14:30:28 -05:00
* adds pkg/jmap/jmap_integration_test.go
* uses ghcr.io/stalwartlabs/stalwart:v0.13.2-alpine
* can be disabled by setting one of the following environment
variables, in the same fashion as ca0493b28
- CI=woodpecker
- CI_SYSTEM_NAME=woodpecker
- USE_TESTCONTAINERS=false
* dependencies:
- bump github.com/go-test/deep from 1.1.0 to 1.1.1
- add github.com/cention-sany/utf7
- add github.com/dustinkirkland/golang-petname
- add github.com/emersion/go-imap/v2
- add github.com/emersion/go-message
- add github.com/emersion/go-sasl
- add github.com/go-crypt/crypt
- add github.com/go-crypt/x
- add github.com/gogs/chardet
- add github.com/inbucket/html2text
- add github.com/jhilleryerd/enmime/v2
- add github.com/ssor/bom
- add gopkg.in/loremipsum.v1
382 lines
8.4 KiB
Go
382 lines
8.4 KiB
Go
package mail
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"net/mail"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/emersion/go-message"
|
|
)
|
|
|
|
const dateLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
|
|
|
|
type headerParser struct {
|
|
s string
|
|
}
|
|
|
|
func (p *headerParser) len() int {
|
|
return len(p.s)
|
|
}
|
|
|
|
func (p *headerParser) empty() bool {
|
|
return p.len() == 0
|
|
}
|
|
|
|
func (p *headerParser) peek() byte {
|
|
return p.s[0]
|
|
}
|
|
|
|
func (p *headerParser) consume(c byte) bool {
|
|
if p.empty() || p.peek() != c {
|
|
return false
|
|
}
|
|
p.s = p.s[1:]
|
|
return true
|
|
}
|
|
|
|
// skipSpace skips the leading space and tab characters.
|
|
func (p *headerParser) skipSpace() {
|
|
p.s = strings.TrimLeft(p.s, " \t")
|
|
}
|
|
|
|
// skipCFWS skips CFWS as defined in RFC5322. It returns false if the CFWS is
|
|
// malformed.
|
|
func (p *headerParser) skipCFWS() bool {
|
|
p.skipSpace()
|
|
|
|
for {
|
|
if !p.consume('(') {
|
|
break
|
|
}
|
|
|
|
if _, ok := p.consumeComment(); !ok {
|
|
return false
|
|
}
|
|
|
|
p.skipSpace()
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (p *headerParser) consumeComment() (string, bool) {
|
|
// '(' already consumed.
|
|
depth := 1
|
|
|
|
var comment string
|
|
for {
|
|
if p.empty() || depth == 0 {
|
|
break
|
|
}
|
|
|
|
if p.peek() == '\\' && p.len() > 1 {
|
|
p.s = p.s[1:]
|
|
} else if p.peek() == '(' {
|
|
depth++
|
|
} else if p.peek() == ')' {
|
|
depth--
|
|
}
|
|
|
|
if depth > 0 {
|
|
comment += p.s[:1]
|
|
}
|
|
|
|
p.s = p.s[1:]
|
|
}
|
|
|
|
return comment, depth == 0
|
|
}
|
|
|
|
func (p *headerParser) parseAtomText(dot bool) (string, error) {
|
|
i := 0
|
|
for {
|
|
r, size := utf8.DecodeRuneInString(p.s[i:])
|
|
if size == 1 && r == utf8.RuneError {
|
|
return "", fmt.Errorf("mail: invalid UTF-8 in atom-text: %q", p.s)
|
|
} else if size == 0 || !isAtext(r, dot) {
|
|
break
|
|
}
|
|
i += size
|
|
}
|
|
if i == 0 {
|
|
return "", errors.New("mail: invalid string")
|
|
}
|
|
|
|
var atom string
|
|
atom, p.s = p.s[:i], p.s[i:]
|
|
return atom, nil
|
|
}
|
|
|
|
func isAtext(r rune, dot bool) bool {
|
|
switch r {
|
|
case '.':
|
|
return dot
|
|
// RFC 5322 3.2.3 specials
|
|
case '(', ')', '[', ']', ';', '@', '\\', ',':
|
|
return false
|
|
case '<', '>', '"', ':':
|
|
return false
|
|
}
|
|
return isVchar(r)
|
|
}
|
|
|
|
// isVchar reports whether r is an RFC 5322 VCHAR character.
|
|
func isVchar(r rune) bool {
|
|
// Visible (printing) characters
|
|
return '!' <= r && r <= '~' || isMultibyte(r)
|
|
}
|
|
|
|
// isMultibyte reports whether r is a multi-byte UTF-8 character
|
|
// as supported by RFC 6532
|
|
func isMultibyte(r rune) bool {
|
|
return r >= utf8.RuneSelf
|
|
}
|
|
|
|
func (p *headerParser) parseNoFoldLiteral() (string, error) {
|
|
if !p.consume('[') {
|
|
return "", errors.New("mail: missing '[' in no-fold-literal")
|
|
}
|
|
|
|
i := 0
|
|
for {
|
|
r, size := utf8.DecodeRuneInString(p.s[i:])
|
|
if size == 1 && r == utf8.RuneError {
|
|
return "", fmt.Errorf("mail: invalid UTF-8 in no-fold-literal: %q", p.s)
|
|
} else if size == 0 || !isDtext(r) {
|
|
break
|
|
}
|
|
i += size
|
|
}
|
|
var lit string
|
|
lit, p.s = p.s[:i], p.s[i:]
|
|
|
|
if !p.consume(']') {
|
|
return "", errors.New("mail: missing ']' in no-fold-literal")
|
|
}
|
|
return "[" + lit + "]", nil
|
|
}
|
|
|
|
func isDtext(r rune) bool {
|
|
switch r {
|
|
case '[', ']', '\\':
|
|
return false
|
|
}
|
|
return isVchar(r)
|
|
}
|
|
|
|
func (p *headerParser) parseMsgID() (string, error) {
|
|
if !p.skipCFWS() {
|
|
return "", errors.New("mail: malformed parenthetical comment")
|
|
}
|
|
|
|
if !p.consume('<') {
|
|
return "", errors.New("mail: missing '<' in msg-id")
|
|
}
|
|
|
|
left, err := p.parseAtomText(true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !p.consume('@') {
|
|
return "", errors.New("mail: missing '@' in msg-id")
|
|
}
|
|
|
|
var right string
|
|
if !p.empty() && p.peek() == '[' {
|
|
// no-fold-literal
|
|
right, err = p.parseNoFoldLiteral()
|
|
} else {
|
|
right, err = p.parseAtomText(true)
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !p.consume('>') {
|
|
return "", errors.New("mail: missing '>' in msg-id")
|
|
}
|
|
|
|
if !p.skipCFWS() {
|
|
return "", errors.New("mail: malformed parenthetical comment")
|
|
}
|
|
|
|
return left + "@" + right, nil
|
|
}
|
|
|
|
// A Header is a mail header.
|
|
type Header struct {
|
|
message.Header
|
|
}
|
|
|
|
// HeaderFromMap creates a header from a map of header fields.
|
|
//
|
|
// This function is provided for interoperability with the standard library.
|
|
// If possible, ReadHeader should be used instead to avoid loosing information.
|
|
// The map representation looses the ordering of the fields, the capitalization
|
|
// of the header keys, and the whitespace of the original header.
|
|
func HeaderFromMap(m map[string][]string) Header {
|
|
return Header{message.HeaderFromMap(m)}
|
|
}
|
|
|
|
// AddressList parses the named header field as a list of addresses. If the
|
|
// header field is missing, it returns nil.
|
|
//
|
|
// This can be used on From, Sender, Reply-To, To, Cc and Bcc header fields.
|
|
func (h *Header) AddressList(key string) ([]*Address, error) {
|
|
v := h.Get(key)
|
|
if v == "" {
|
|
return nil, nil
|
|
}
|
|
return ParseAddressList(v)
|
|
}
|
|
|
|
// SetAddressList formats the named header field to the provided list of
|
|
// addresses.
|
|
//
|
|
// This can be used on From, Sender, Reply-To, To, Cc and Bcc header fields.
|
|
func (h *Header) SetAddressList(key string, addrs []*Address) {
|
|
if len(addrs) > 0 {
|
|
h.Set(key, formatAddressList(addrs))
|
|
} else {
|
|
h.Del(key)
|
|
}
|
|
}
|
|
|
|
// Date parses the Date header field. If the header field is missing, it
|
|
// returns the zero time.
|
|
func (h *Header) Date() (time.Time, error) {
|
|
v := h.Get("Date")
|
|
if v == "" {
|
|
return time.Time{}, nil
|
|
}
|
|
return mail.ParseDate(v)
|
|
}
|
|
|
|
// SetDate formats the Date header field.
|
|
func (h *Header) SetDate(t time.Time) {
|
|
if !t.IsZero() {
|
|
h.Set("Date", t.Format(dateLayout))
|
|
} else {
|
|
h.Del("Date")
|
|
}
|
|
}
|
|
|
|
// Subject parses the Subject header field. If there is an error, the raw field
|
|
// value is returned alongside the error.
|
|
func (h *Header) Subject() (string, error) {
|
|
return h.Text("Subject")
|
|
}
|
|
|
|
// SetSubject formats the Subject header field.
|
|
func (h *Header) SetSubject(s string) {
|
|
h.SetText("Subject", s)
|
|
}
|
|
|
|
// MessageID parses the Message-ID field. It returns the message identifier,
|
|
// without the angle brackets. If the message doesn't have a Message-ID header
|
|
// field, it returns an empty string.
|
|
func (h *Header) MessageID() (string, error) {
|
|
v := h.Get("Message-Id")
|
|
if v == "" {
|
|
return "", nil
|
|
}
|
|
|
|
p := headerParser{v}
|
|
return p.parseMsgID()
|
|
}
|
|
|
|
// MsgIDList parses a list of message identifiers. It returns message
|
|
// identifiers without angle brackets. If the header field is missing, it
|
|
// returns nil.
|
|
//
|
|
// This can be used on In-Reply-To and References header fields.
|
|
func (h *Header) MsgIDList(key string) ([]string, error) {
|
|
v := h.Get(key)
|
|
if v == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
p := headerParser{v}
|
|
var l []string
|
|
for !p.empty() {
|
|
msgID, err := p.parseMsgID()
|
|
if err != nil {
|
|
return l, err
|
|
}
|
|
l = append(l, msgID)
|
|
}
|
|
|
|
return l, nil
|
|
}
|
|
|
|
// GenerateMessageID wraps GenerateMessageIDWithHostname and therefore uses the
|
|
// hostname of the local machine. This is done to not break existing software.
|
|
// Wherever possible better use GenerateMessageIDWithHostname, because the local
|
|
// hostname of a machine tends to not be unique nor a FQDN which especially
|
|
// brings problems with spam filters.
|
|
func (h *Header) GenerateMessageID() error {
|
|
var err error
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return h.GenerateMessageIDWithHostname(hostname)
|
|
}
|
|
|
|
// GenerateMessageIDWithHostname generates an RFC 2822-compliant Message-Id
|
|
// based on the informational draft "Recommendations for generating Message
|
|
// IDs", it takes an hostname as argument, so that software using this library
|
|
// could use a hostname they know to be unique
|
|
func (h *Header) GenerateMessageIDWithHostname(hostname string) error {
|
|
now := uint64(time.Now().UnixNano())
|
|
|
|
nonceByte := make([]byte, 8)
|
|
if _, err := rand.Read(nonceByte); err != nil {
|
|
return err
|
|
}
|
|
nonce := binary.BigEndian.Uint64(nonceByte)
|
|
|
|
msgID := fmt.Sprintf("%s.%s@%s", base36(now), base36(nonce), hostname)
|
|
h.SetMessageID(msgID)
|
|
return nil
|
|
}
|
|
|
|
func base36(input uint64) string {
|
|
return strings.ToUpper(strconv.FormatUint(input, 36))
|
|
}
|
|
|
|
// SetMessageID sets the Message-ID field. id is the message identifier,
|
|
// without the angle brackets.
|
|
func (h *Header) SetMessageID(id string) {
|
|
if id != "" {
|
|
h.Set("Message-Id", "<"+id+">")
|
|
} else {
|
|
h.Del("Message-Id")
|
|
}
|
|
}
|
|
|
|
// SetMsgIDList formats a list of message identifiers. Message identifiers
|
|
// don't include angle brackets.
|
|
//
|
|
// This can be used on In-Reply-To and References header fields.
|
|
func (h *Header) SetMsgIDList(key string, l []string) {
|
|
if len(l) > 0 {
|
|
h.Set(key, "<"+strings.Join(l, "> <")+">")
|
|
} else {
|
|
h.Del(key)
|
|
}
|
|
}
|
|
|
|
// Copy creates a stand-alone copy of the header.
|
|
func (h *Header) Copy() Header {
|
|
return Header{h.Header.Copy()}
|
|
}
|