package imap
type Capability string
const (
IMAP4rev1 Capability = `IMAP4rev1`
StartTLS Capability = `STARTTLS`
IDLE Capability = `IDLE`
UNSELECT Capability = `UNSELECT`
UIDPLUS Capability = `UIDPLUS`
MOVE Capability = `MOVE`
ID Capability = `ID`
AUTHPLAIN Capability = `AUTH=PLAIN`
)
func IsCapabilityAvailableBeforeAuth(c Capability) bool {
switch c {
case IMAP4rev1, StartTLS, IDLE, ID, AUTHPLAIN:
return true
case UNSELECT, UIDPLUS, MOVE:
return false
}
return false
}
package imap
import (
"net/mail"
"strings"
"github.com/ProtonMail/gluon/rfc5322"
"github.com/ProtonMail/gluon/rfc822"
"github.com/sirupsen/logrus"
)
func Envelope(header *rfc822.Header) (string, error) {
builder := strings.Builder{}
writer := singleParListWriter{b: &builder}
paramList := newParamListWithoutGroup()
if err := envelope(header, ¶mList, &writer); err != nil {
return "", err
}
return builder.String(), nil
}
func envelope(header *rfc822.Header, c *paramList, writer parListWriter) error {
fields := c.newChildList(writer)
defer fields.finish(writer)
fields.
addString(writer, header.Get("Date")).
addString(writer, header.Get("Subject"))
if v, ok := header.GetChecked("From"); !ok {
fields.addString(writer, "")
} else {
fields.addAddresses(writer, tryParseAddressList(v))
}
if v, ok := header.GetChecked("Sender"); ok {
fields.addAddresses(writer, tryParseAddressList(v))
} else if v, ok := header.GetChecked("From"); ok {
fields.addAddresses(writer, tryParseAddressList(v))
} else {
fields.addString(writer, "")
}
if v, ok := header.GetChecked("Reply-To"); ok {
fields.addAddresses(writer, tryParseAddressList(v))
} else if v, ok := header.GetChecked("From"); ok {
fields.addAddresses(writer, tryParseAddressList(v))
} else {
fields.addString(writer, "")
}
if v, ok := header.GetChecked("To"); !ok {
fields.addString(writer, "")
} else {
fields.addAddresses(writer, tryParseAddressList(v))
}
if v, ok := header.GetChecked("Cc"); !ok {
fields.addString(writer, "")
} else {
fields.addAddresses(writer, tryParseAddressList(v))
}
if v, ok := header.GetChecked("Bcc"); !ok {
fields.addString(writer, "")
} else {
fields.addAddresses(writer, tryParseAddressList(v))
}
fields.addString(writer, header.Get("In-Reply-To"))
fields.addString(writer, header.Get("Message-Id"))
return nil
}
func tryParseAddressList(val string) []*mail.Address {
addr, err := rfc5322.ParseAddressList(val)
if err != nil {
logrus.WithError(err).Error("Failed to parse address")
return []*mail.Address{{Name: val}}
}
return addr
}
package imap
import (
"strings"
"github.com/bradenaw/juniper/xslices"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
const (
FlagSeen = `\Seen`
FlagAnswered = `\Answered`
FlagFlagged = `\Flagged`
FlagDeleted = `\Deleted`
FlagDraft = `\Draft`
FlagRecent = `\Recent` // Read-only!.
XFlagDollarForwarded = "$Forwarded" // Non-Standard flag
XFlagForwarded = "Forwarded" // Non-Standard flag
)
const (
FlagSeenLowerCase = `\seen`
FlagAnsweredLowerCase = `\answered`
FlagFlaggedLowerCase = `\flagged`
FlagDeletedLowerCase = `\deleted`
FlagDraftLowerCase = `\draft`
FlagRecentLowerCase = `\recent` // Read-only!.
XFlagDollarForwardedLowerCase = "$forwarded"
XFlagForwardedLowerCase = "forwarded"
)
var ForwardFlagList = []string{
XFlagDollarForwarded,
XFlagForwarded,
}
var ForwardFlagListLowerCase = []string{
XFlagDollarForwardedLowerCase,
XFlagForwardedLowerCase,
}
// FlagSet represents a set of IMAP flags. Flags are case-insensitive and no duplicates are allowed.
type FlagSet map[string]string
// NewFlagSet creates a flag set containing the specified flags.
func NewFlagSet(flags ...string) FlagSet {
fs := NewFlagSetWithCapacity(len(flags))
for _, item := range flags {
fs.add(item)
}
return fs
}
func NewFlagSetWithCapacity(capacity int) FlagSet {
return make(FlagSet, capacity)
}
// NewFlagSetFromSlice creates a flag set containing the flags from a slice.
func NewFlagSetFromSlice(flags []string) FlagSet {
return NewFlagSet(flags...)
}
// Len returns the number of flags in the flag set.
func (fs FlagSet) Len() int {
return len(fs)
}
// ToSlice Returns the list of flags in the set as a sorted string slice. The returned list is a hard copy of the internal
// slice to avoid direct modifications of the FlagSet value that would break the uniqueness and case insensitivity rules.
func (fs FlagSet) ToSlice() []string {
flags := maps.Values(fs)
slices.Sort(flags)
return flags
}
// ToSliceUnsorted is the same as ToSlice, but does not sort the returned value.
func (fs FlagSet) ToSliceUnsorted() []string {
return maps.Values(fs)
}
// Contains returns true if and only if the flag is in the set.
func (fs FlagSet) Contains(flag string) bool {
_, ok := fs[strings.ToLower(flag)]
return ok
}
// ContainsUnchecked returns true if and only if the flag is in the set. The flag is not converted to lower case. This
// is useful for cases where we need to check flags in bulk.
func (fs FlagSet) ContainsUnchecked(flag string) bool {
_, ok := fs[flag]
return ok
}
// ContainsAny returns true if and only if any of the flags are in the set.
func (fs FlagSet) ContainsAny(flags ...string) bool {
return xslices.IndexFunc(flags, func(f string) bool {
return fs.Contains(f)
}) >= 0
}
// ContainsAnyUnchecked returns true if and only if any of the flags are in the set. The flag list is not converted to
// lower case.
func (fs FlagSet) ContainsAnyUnchecked(flags ...string) bool {
return xslices.IndexFunc(flags, func(f string) bool {
return fs.ContainsUnchecked(f)
}) >= 0
}
// ContainsAll returns true if and only if all of the flags are in the set.
func (fs FlagSet) ContainsAll(flags ...string) bool {
return xslices.IndexFunc(flags, func(f string) bool {
return !fs.Contains(f)
}) < 0
}
// Equals returns true if and only if the two sets are equal (same number of elements and each element of fs is also in otherFs).
func (fs FlagSet) Equals(otherFs FlagSet) bool {
if fs.Len() != otherFs.Len() {
return false
}
for key := range fs {
if _, ok := otherFs[key]; !ok {
return false
}
}
return true
}
// Add adds new flags to the flag set. The function returns false iff no flags was actually added because they're already in the set.
// The case of existing elements is preserved.
func (fs FlagSet) Add(flags ...string) FlagSet {
f := fs.Clone()
f.add(flags...)
return f
}
func (fs FlagSet) AddToSelf(flags ...string) {
fs.add(flags...)
}
func (fs FlagSet) AddFlagSet(set FlagSet) FlagSet {
return fs.Add(maps.Values(set)...)
}
func (fs FlagSet) AddFlagSetToSelf(set FlagSet) {
fs.add(maps.Values(set)...)
}
func (fs FlagSet) add(flags ...string) {
for _, flag := range flags {
flagLower := strings.ToLower(flag)
if fs.ContainsUnchecked(flagLower) {
continue
}
fs[flagLower] = flag
}
}
// Set ensures the flagset either contains or does not contain the given flag.
func (fs FlagSet) Set(flag string, on bool) FlagSet {
if on {
return fs.Add(flag)
} else {
return fs.Remove(flag)
}
}
// SetOnSelf ensures the flagset either contains or does not contain the given flag.
func (fs FlagSet) SetOnSelf(flag string, on bool) {
if on {
fs.AddToSelf(flag)
} else {
fs.RemoveFromSelf(flag)
}
}
func (fs FlagSet) Remove(flags ...string) FlagSet {
f := fs.Clone()
f.remove(flags...)
return f
}
func (fs FlagSet) RemoveFlagSet(set FlagSet) FlagSet {
return fs.Remove(maps.Values(set)...)
}
func (fs FlagSet) RemoveFromSelf(flags ...string) {
fs.remove(flags...)
}
func (fs FlagSet) RemoveFlagSetFromSelf(set FlagSet) {
fs.Remove(maps.Values(set)...)
}
func (fs FlagSet) remove(flags ...string) {
for _, flag := range flags {
flagLower := strings.ToLower(flag)
if !fs.ContainsUnchecked(flagLower) {
continue
}
delete(fs, flagLower)
}
}
// Clone creates a hard copy of the flag set.
func (fs FlagSet) Clone() FlagSet {
return NewFlagSetFromSlice(fs.ToSlice())
}
package imap
import (
"context"
"encoding/gob"
"fmt"
"runtime"
"strings"
"github.com/ProtonMail/gluon/version"
)
const (
IDKeyName = "name"
IDKeyVersion = "version"
IDKeyOS = "os"
IdKeyOSVersion = "os-version"
IDKeyVendor = "vendor"
IDKeySupportURL = "support-url"
IDKeyAddress = "address"
IDKeyDate = "date"
IDKeyCommand = "command"
IDKeyArguments = "arguments"
IDKeyEnvironment = "environment"
IMAPIDConnMetadataKey = "rfc2971-id"
)
// IMAPID represents the RFC 2971 IMAP IMAPID Extension. This information can be retrieved by the connector at the context
// level. To do so please use the provided GetIMAPIDFromContext() function.
type IMAPID struct {
Name string
Version string
OS string
OSVersion string
Vendor string
SupportURL string
Address string
Date string
Command string
Arguments string
Environment string
Other map[string]string
}
func NewIMAPID() IMAPID {
return IMAPID{
Name: "Unknown",
Version: "Unknown",
OS: "Unknown",
OSVersion: "Unknown",
Vendor: "Unknown",
SupportURL: "",
Address: "",
Date: "",
Command: "",
Arguments: "",
Environment: "",
Other: make(map[string]string),
}
}
func (id *IMAPID) String() string {
var values []string
writeIfNotEmpty := func(key string, value string) {
if len(value) != 0 {
values = append(values, fmt.Sprintf(`"%v" "%v"`, key, value))
}
}
writeIfNotEmpty(IDKeyName, id.Name)
writeIfNotEmpty(IDKeyVersion, id.Version)
writeIfNotEmpty(IDKeyOS, id.OS)
writeIfNotEmpty(IdKeyOSVersion, id.OSVersion)
writeIfNotEmpty(IDKeyVendor, id.Vendor)
writeIfNotEmpty(IDKeySupportURL, id.SupportURL)
writeIfNotEmpty(IDKeyAddress, id.Address)
writeIfNotEmpty(IDKeyDate, id.Date)
writeIfNotEmpty(IDKeyCommand, id.Command)
writeIfNotEmpty(IDKeyArguments, id.Arguments)
writeIfNotEmpty(IDKeyEnvironment, id.Environment)
for k, v := range id.Other {
writeIfNotEmpty(k, v)
}
return fmt.Sprintf("(%v)", strings.Join(values, " "))
}
func NewIMAPIDFromKeyMap(m map[string]string) IMAPID {
id := NewIMAPID()
paramMap := map[string]*string{
IDKeyName: &id.Name,
IDKeyVersion: &id.Version,
IDKeyOS: &id.OS,
IDKeyVendor: &id.Vendor,
IDKeySupportURL: &id.SupportURL,
IDKeyAddress: &id.Address,
IDKeyDate: &id.Date,
IDKeyCommand: &id.Command,
IDKeyArguments: &id.Arguments,
IDKeyEnvironment: &id.Environment,
}
for k, v := range m {
if idv, ok := paramMap[k]; ok {
*idv = v
} else {
id.Other[k] = v
}
}
return id
}
func NewIMAPIDFromVersionInfo(info version.Info) IMAPID {
return IMAPID{
Name: info.Name,
Version: info.Version.String(),
Vendor: info.Vendor,
SupportURL: info.SupportURL,
OS: runtime.GOOS,
}
}
func GetIMAPIDFromContext(ctx context.Context) (IMAPID, bool) {
if v := ctx.Value(imapIDContextKey); v != nil {
if id, ok := v.(IMAPID); ok {
return id, true
}
}
return IMAPID{}, false
}
func NewContextWithIMAPID(ctx context.Context, id IMAPID) context.Context {
return context.WithValue(ctx, imapIDContextKey, id)
}
type imapIDContextType struct{}
var imapIDContextKey imapIDContextType
func init() {
gob.Register(&IMAPID{})
}
package imap
import (
"time"
)
type Message struct {
ID MessageID
Flags FlagSet
Date time.Time
}
type Header []Field
type Field struct {
Key, Value string
}
func (m *Message) HasFlag(wantFlag string) bool {
return m.Flags.Contains(wantFlag)
}
package imap
import (
"net/mail"
"strconv"
"strings"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
type parListWriter interface {
writeString(string)
writeByte(byte)
}
type singleParListWriter struct {
b *strings.Builder
}
func (s *singleParListWriter) writeString(v string) {
s.b.WriteString(v)
}
func (s *singleParListWriter) writeByte(v byte) {
s.b.WriteByte(v)
}
type dualParListWriter struct {
b1 *strings.Builder
b2 *strings.Builder
}
func (d *dualParListWriter) writeString(v string) {
d.b1.WriteString(v)
d.b2.WriteString(v)
}
func (d *dualParListWriter) writeByte(v byte) {
d.b1.WriteByte(v)
d.b2.WriteByte(v)
}
func (d *dualParListWriter) toSingleWriterFrom1st() parListWriter {
return &singleParListWriter{b: d.b1}
}
func (d *dualParListWriter) toSingleWriterFrom2nd() parListWriter {
return &singleParListWriter{b: d.b2}
}
type paramList struct {
firstItem bool
}
func newParamListWithGroup(writer parListWriter) paramList {
writer.writeByte('(')
return paramList{
firstItem: true,
}
}
func newParamListWithoutGroup() paramList {
return paramList{
firstItem: true,
}
}
func (c *paramList) newChildList(writer parListWriter) paramList {
c.firstItem = false
return newParamListWithGroup(writer)
}
func (c *paramList) finish(writer parListWriter) {
writer.writeByte(')')
}
func (c *paramList) onWrite(writer parListWriter) {
if !c.firstItem {
writer.writeByte(' ')
}
c.firstItem = false
}
func (c *paramList) addString(writer parListWriter, v string) *paramList {
c.onWrite(writer)
var str string
if len(v) == 0 {
str = "NIL"
} else {
str = strconv.Quote(v)
}
writer.writeString(str)
return c
}
func (c *paramList) addNumber(writer parListWriter, v int) *paramList {
c.onWrite(writer)
str := strconv.Itoa(v)
writer.writeString(str)
return c
}
func (c *paramList) addMap(writer parListWriter, v map[string]string) *paramList {
c.onWrite(writer)
keys := maps.Keys(v)
slices.Sort(keys)
child := c.newChildList(writer)
for _, key := range keys {
child.addString(writer, key).addString(writer, v[key])
}
child.finish(writer)
return c
}
func (c *paramList) addAddresses(writer parListWriter, v []*mail.Address) *paramList {
c.onWrite(writer)
child := c.newChildList(writer)
for _, addr := range v {
var user, domain string
if split := strings.Split(addr.Address, "@"); len(split) == 2 {
user, domain = split[0], split[1]
}
fields := child.newChildList(writer)
fields.
addString(writer, addr.Name).
addString(writer, "").
addString(writer, user).
addString(writer, domain)
fields.finish(writer)
}
child.finish(writer)
return c
}
package imap
import (
"fmt"
"strconv"
"strings"
"github.com/bradenaw/juniper/xslices"
"golang.org/x/exp/slices"
)
type SeqVal struct {
Begin, End SeqID
}
func (seqval SeqVal) canCombine(val SeqID) bool {
return val == SeqID(uint32(seqval.End)+1)
}
func (seqval SeqVal) String() string {
if seqval.End > seqval.Begin {
return fmt.Sprintf("%v:%v", seqval.Begin, seqval.End)
}
return strconv.FormatUint(uint64(seqval.End), 10)
}
type SeqSet []SeqVal
func NewSeqSetFromUID(set []UID) SeqSet {
return NewSeqSet(xslices.Map(set, func(t UID) SeqID {
return SeqID(t)
}))
}
func NewSeqSet(set []SeqID) SeqSet {
slices.Sort(set)
var res SeqSet
for _, val := range set {
if n := len(res); n > 0 {
if res[n-1].canCombine(val) {
res[n-1].End = val
} else {
res = append(res, SeqVal{Begin: val, End: val})
}
} else {
res = append(res, SeqVal{Begin: val, End: val})
}
}
return res
}
func (set SeqSet) String() string {
var res []string
for _, val := range set {
res = append(res, val.String())
}
return strings.Join(res, ",")
}
package imap
import (
"fmt"
"strconv"
"github.com/google/uuid"
)
type MailboxID string
type MessageID string
func (l MailboxID) ShortID() string {
return ShortID(string(l))
}
func (m MessageID) ShortID() string {
return ShortID(string(m))
}
type InternalMessageID struct {
uuid.UUID
}
type InternalMailboxID uint64
func (i InternalMailboxID) ShortID() string {
return strconv.FormatUint(uint64(i), 10)
}
func (i InternalMessageID) ShortID() string {
return ShortID(i.String())
}
func (i InternalMailboxID) String() string {
return strconv.FormatUint(uint64(i), 10)
}
func (i InternalMessageID) String() string {
return i.UUID.String()
}
func NewInternalMessageID() InternalMessageID {
return InternalMessageID{UUID: uuid.New()}
}
func InternalMessageIDFromString(id string) (InternalMessageID, error) {
num, err := uuid.Parse(id)
if err != nil {
return InternalMessageID{}, fmt.Errorf("invalid message id:%w", err)
}
return InternalMessageID{UUID: num}, nil
}
type UID uint32
func (u UID) Add(v uint32) UID {
return UID(uint32(u) + v)
}
type SeqID uint32
package imap
import (
"bytes"
"strings"
"github.com/ProtonMail/gluon/rfc822"
)
func Structure(section *rfc822.Section) (string, string, error) {
bodyBuilder := strings.Builder{}
structureBuilder := strings.Builder{}
writer := dualParListWriter{b1: &bodyBuilder, b2: &structureBuilder}
c := newParamListWithGroup(&writer)
if err := structure(section, &c, &writer); err != nil {
return "", "", err
}
c.finish(&writer)
body := bodyBuilder.String()
structure := structureBuilder.String()
return body, structure, nil
}
func structure(section *rfc822.Section, fields *paramList, writer *dualParListWriter) error {
children, err := section.Children()
if err != nil {
return err
}
if len(children) == 0 {
return singlePartStructure(section, fields, writer)
}
if err := childStructures(section, fields, writer); err != nil {
return err
}
header, err := section.ParseHeader()
if err != nil {
return err
}
_, mimeSubType, mimeParams, err := getMIMEInfo(section)
if err != nil {
return err
}
fields.addString(writer, mimeSubType)
extWriter := writer.toSingleWriterFrom2nd()
fields.addMap(extWriter, mimeParams)
addDispInfo(fields, extWriter, header)
fields.addString(extWriter, header.Get("Content-Language")).
addString(extWriter, header.Get("Content-Location"))
return nil
}
func singlePartStructure(section *rfc822.Section, fields *paramList, writer *dualParListWriter) error {
header, err := section.ParseHeader()
if err != nil {
return err
}
mimeType, mimeSubType, mimeParams, err := getMIMEInfo(section)
if err != nil {
return err
}
fields.
addString(writer, mimeType).
addString(writer, mimeSubType).
addMap(writer, mimeParams).
addString(writer, header.Get("Content-Id")).
addString(writer, header.Get("Content-Description")).
addString(writer, header.Get("Content-Transfer-Encoding")).
addNumber(writer, len(section.Body()))
if mimeType == "message" && mimeSubType == "rfc822" {
child := rfc822.Parse(section.Body())
header, err := child.ParseHeader()
if err != nil {
return err
}
writer.writeByte(' ')
if err := envelope(header, fields, writer); err != nil {
return err
}
cstruct := fields.newChildList(writer)
if err := structure(child, &cstruct, writer); err != nil {
return err
}
cstruct.finish(writer)
}
if mimeType == "text" || (mimeType == "message" && mimeSubType == "rfc822") {
fields.addNumber(writer, countLines(section.Body()))
}
extWriter := writer.toSingleWriterFrom2nd()
fields.addString(extWriter, header.Get("Content-MD5"))
addDispInfo(fields, extWriter, header)
fields.addString(extWriter, header.Get("Content-Language")).
addString(extWriter, header.Get("Content-Location"))
return nil
}
func childStructures(section *rfc822.Section, c *paramList, writer *dualParListWriter) error {
children, err := section.Children()
if err != nil {
return err
}
for _, child := range children {
cl := c.newChildList(writer)
if err := structure(child, &cl, writer); err != nil {
return err
}
cl.finish(writer)
}
return nil
}
func getMIMEInfo(section *rfc822.Section) (string, string, map[string]string, error) {
mimeType, mimeParams, err := section.ContentType()
if err != nil {
return "", "", nil, err
}
return mimeType.Type(), mimeType.SubType(), mimeParams, nil
}
func addDispInfo(c *paramList, writer parListWriter, header *rfc822.Header) {
if contentDisp, contentDispParams, err := rfc822.ParseMediaType(header.Get("Content-Disposition")); err == nil {
writer.writeByte(' ')
fields := c.newChildList(writer)
fields.addString(writer, contentDisp).addMap(writer, contentDispParams)
fields.finish(writer)
} else {
c.addString(writer, "")
}
}
func countLines(b []byte) int {
lines := 0
remaining := b
separator := []byte{'\n'}
for len(remaining) != 0 {
index := bytes.Index(remaining, separator)
if index < 0 {
lines++
break
}
lines++
remaining = remaining[index+1:]
}
return lines
}
package imap
import (
"fmt"
"sync/atomic"
"time"
)
type UIDValidityGenerator interface {
Generate() (UID, error)
}
type EpochUIDValidityGenerator struct {
epochStart time.Time
lastUID uint32
}
func NewEpochUIDValidityGenerator(epochStart time.Time) *EpochUIDValidityGenerator {
return &EpochUIDValidityGenerator{
epochStart: epochStart,
}
}
func DefaultEpochUIDValidityGenerator() *EpochUIDValidityGenerator {
return NewEpochUIDValidityGenerator(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC))
}
func (e *EpochUIDValidityGenerator) Generate() (UID, error) {
timeStamp := uint64(time.Now().Sub(e.epochStart).Seconds())
if timeStamp > uint64(0xFFFFFFFF) {
return 0, fmt.Errorf("failed to generate uid validity, interval exceeded maximum capacity")
}
timeStampU32 := uint32(timeStamp)
// This loops is here to ensure that two successive calls to Generate that happen during the same second
// can still generate unique values. To avoid waiting another second until the values are different,
// we keep bumping the last generated value until it is greater than the last generated value.
for {
lastGenerated := atomic.LoadUint32(&e.lastUID)
// Not enough time elapsed between the last time
if lastGenerated >= timeStampU32 {
if timeStampU32 == 0xFFFFFFFF {
return 0, fmt.Errorf("failed to generate uid validity, interval exceeded maximum capacity")
}
timeStampU32 += 1
continue
}
if !atomic.CompareAndSwapUint32(&e.lastUID, lastGenerated, timeStampU32) {
continue
}
return UID(timeStampU32), nil
}
}
type IncrementalUIDValidityGenerator struct {
counter uint32
}
func (i *IncrementalUIDValidityGenerator) Generate() (UID, error) {
return UID(atomic.AddUint32(&i.counter, 1)), nil
}
func (i *IncrementalUIDValidityGenerator) GetValue() UID {
return UID(atomic.LoadUint32(&i.counter))
}
func NewIncrementalUIDValidityGenerator() *IncrementalUIDValidityGenerator {
return &IncrementalUIDValidityGenerator{}
}
type FixedUIDValidityGenerator struct {
Value UID
}
func (f FixedUIDValidityGenerator) Generate() (UID, error) {
return f.Value, nil
}
func NewFixedUIDValidityGenerator(value UID) *FixedUIDValidityGenerator {
return &FixedUIDValidityGenerator{Value: value}
}
package imap
type Update interface {
Waiter
String() string
_isUpdate()
}
type updateBase struct{}
func (updateBase) _isUpdate() {}
package imap
import (
"fmt"
"strings"
)
type MailboxCreated struct {
updateBase
*updateWaiter
Mailbox Mailbox
}
func NewMailboxCreated(mailbox Mailbox) *MailboxCreated {
return &MailboxCreated{
updateWaiter: newUpdateWaiter(),
Mailbox: mailbox,
}
}
func (u *MailboxCreated) String() string {
return fmt.Sprintf(
"MailboxCreated: Mailbox.ID = %v, Mailbox.Name = %v",
u.Mailbox.ID.ShortID(),
ShortID(strings.Join(u.Mailbox.Name, "/")),
)
}
package imap
import (
"fmt"
"strings"
)
type MailboxUpdatedOrCreated struct {
updateBase
*updateWaiter
Mailbox Mailbox
}
func NewMailboxUpdatedOrCreated(mailbox Mailbox) *MailboxUpdatedOrCreated {
return &MailboxUpdatedOrCreated{
updateWaiter: newUpdateWaiter(),
Mailbox: mailbox,
}
}
func (u *MailboxUpdatedOrCreated) String() string {
return fmt.Sprintf("MailboxUpdatedOrCreated: Mailbox.ID = %v, Mailbox.Name = %v",
u.Mailbox.ID.ShortID(),
ShortID(strings.Join(u.Mailbox.Name, "/")),
)
}
package imap
import (
"fmt"
)
type MailboxDeleted struct {
updateBase
*updateWaiter
MailboxID MailboxID
}
func NewMailboxDeleted(mailboxID MailboxID) *MailboxDeleted {
return &MailboxDeleted{
updateWaiter: newUpdateWaiter(),
MailboxID: mailboxID,
}
}
func (u *MailboxDeleted) String() string {
return fmt.Sprintf("MailboxDeleted: MailboxID = %v", u.MailboxID.ShortID())
}
type MailboxDeletedSilent struct {
updateBase
*updateWaiter
MailboxID MailboxID
}
func NewMailboxDeletedSilent(mailboxID MailboxID) *MailboxDeletedSilent {
return &MailboxDeletedSilent{
updateWaiter: newUpdateWaiter(),
MailboxID: mailboxID,
}
}
func (u *MailboxDeletedSilent) String() string {
return fmt.Sprintf("MailboxDeletedSilent: MailboxID = %v", u.MailboxID.ShortID())
}
package imap
import (
"fmt"
)
type MailboxIDChanged struct {
updateBase
*updateWaiter
InternalID InternalMailboxID
RemoteID MailboxID
}
func NewMailboxIDChanged(internalID InternalMailboxID, remoteID MailboxID) *MailboxIDChanged {
return &MailboxIDChanged{
updateWaiter: newUpdateWaiter(),
InternalID: internalID,
RemoteID: remoteID,
}
}
func (u *MailboxIDChanged) String() string {
return fmt.Sprintf("MailboxIDChanged: InternalID = %v, RemoteID = %v", u.InternalID.ShortID(), u.RemoteID.ShortID())
}
package imap
import (
"fmt"
"strings"
)
type MailboxUpdated struct {
updateBase
*updateWaiter
MailboxID MailboxID
MailboxName []string
}
func NewMailboxUpdated(mailboxID MailboxID, mailboxName []string) *MailboxUpdated {
return &MailboxUpdated{
updateWaiter: newUpdateWaiter(),
MailboxID: mailboxID,
MailboxName: mailboxName,
}
}
func (u *MailboxUpdated) String() string {
return fmt.Sprintf(
"MailboxUpdated: MailboxID = %v, MailboxName = %v",
u.MailboxID.ShortID(),
ShortID(strings.Join(u.MailboxName, "/")),
)
}
package imap
import (
"fmt"
"github.com/ProtonMail/gluon/rfc822"
"github.com/bradenaw/juniper/xslices"
)
type ParsedMessage struct {
Body string
Structure string
Envelope string
}
func NewParsedMessage(literal []byte) (*ParsedMessage, error) {
root := rfc822.Parse(literal)
body, structure, err := Structure(root)
if err != nil {
return nil, fmt.Errorf("failed to build message body and structure: %w", err)
}
header, err := root.ParseHeader()
if err != nil {
return nil, fmt.Errorf("failed to parser message header: %w", err)
}
envelope, err := Envelope(header)
if err != nil {
return nil, fmt.Errorf("failed to build message envelope: %w", err)
}
return &ParsedMessage{
Body: body,
Structure: structure,
Envelope: envelope,
}, nil
}
type MessagesCreated struct {
updateBase
*updateWaiter
Messages []*MessageCreated
// IgnoreUnknownMailboxIDs will allow message creation when one or more MailboxIDs are not yet known when set to true.
IgnoreUnknownMailboxIDs bool
}
type MessageCreated struct {
Message Message
Literal []byte
MailboxIDs []MailboxID
ParsedMessage *ParsedMessage
}
func NewMessagesCreated(ignoreUnknownMailboxIDs bool, updates ...*MessageCreated) *MessagesCreated {
return &MessagesCreated{
updateWaiter: newUpdateWaiter(),
Messages: updates,
IgnoreUnknownMailboxIDs: ignoreUnknownMailboxIDs,
}
}
func (u *MessagesCreated) String() string {
return fmt.Sprintf("MessagesCreated: MessageCount=%v Messages=%v",
len(u.Messages),
xslices.Map(u.Messages, func(m *MessageCreated) string {
return fmt.Sprintf("ID:%v Mailboxes:%v Flags:%s",
m.Message.ID.ShortID(),
xslices.Map(m.MailboxIDs, func(mboxID MailboxID) string {
return mboxID.ShortID()
}),
m.Message.Flags.ToSlice(),
)
}),
)
}
package imap
import (
"fmt"
)
type MessageDeleted struct {
updateBase
*updateWaiter
MessageID MessageID
}
func NewMessagesDeleted(messageID MessageID) *MessageDeleted {
return &MessageDeleted{
updateWaiter: newUpdateWaiter(),
MessageID: messageID,
}
}
func (u *MessageDeleted) String() string {
return fmt.Sprintf("MessageDeleted ID=%v", u.MessageID.ShortID())
}
package imap
import (
"fmt"
)
type MessageFlagsUpdated struct {
updateBase
*updateWaiter
MessageID MessageID
Flags FlagSet
}
func NewMessageFlagsUpdated(messageID MessageID, flags FlagSet) *MessageFlagsUpdated {
return &MessageFlagsUpdated{
updateWaiter: newUpdateWaiter(),
MessageID: messageID,
Flags: flags,
}
}
func (u *MessageFlagsUpdated) String() string {
return fmt.Sprintf(
"MessageFlagsUpdated: MessageID = %v, Flags = %v",
u.MessageID.ShortID(),
u.Flags.ToSlice(),
)
}
package imap
import (
"fmt"
)
type MessageIDChanged struct {
updateBase
*updateWaiter
InternalID InternalMessageID
RemoteID MessageID
}
func NewMessageIDChanged(internalID InternalMessageID, remoteID MessageID) *MessageIDChanged {
return &MessageIDChanged{
updateWaiter: newUpdateWaiter(),
InternalID: internalID,
RemoteID: remoteID,
}
}
func (u *MessageIDChanged) String() string {
return fmt.Sprintf("MessageID changed: InternalID = %v, RemoteID = %v", u.InternalID.ShortID(), u.RemoteID.ShortID())
}
package imap
import (
"fmt"
"github.com/bradenaw/juniper/xslices"
)
type MessageMailboxesUpdated struct {
updateBase
*updateWaiter
MessageID MessageID
MailboxIDs []MailboxID
Flags FlagSet
}
func NewMessageMailboxesUpdated(messageID MessageID, mailboxIDs []MailboxID, flags FlagSet) *MessageMailboxesUpdated {
return &MessageMailboxesUpdated{
updateWaiter: newUpdateWaiter(),
MessageID: messageID,
MailboxIDs: mailboxIDs,
Flags: flags,
}
}
func (u *MessageMailboxesUpdated) String() string {
return fmt.Sprintf(
"MessageMailboxesUpdated: MessageID = %v, MailboxIDs = %v, Flags = %v",
u.MessageID.ShortID(),
xslices.Map(u.MailboxIDs, func(id MailboxID) string { return id.ShortID() }),
u.Flags.ToSlice(),
)
}
package imap
import (
"fmt"
"github.com/bradenaw/juniper/xslices"
)
// MessageUpdated replaces the previous behavior of MessageDelete followed by MessageCreate.
// If the message does exist, it is updated.
// If the message does not exist, it can optionally be created.
// Furthermore, it guarantees that the operation is executed atomically.
type MessageUpdated struct {
updateBase
*updateWaiter
Message Message
Literal []byte
MailboxIDs []MailboxID
ParsedMessage *ParsedMessage
AllowCreate bool
IgnoreUnknownMailboxIDs bool
}
func NewMessageUpdated(
message Message,
literal []byte,
mailboxIDs []MailboxID,
parsedMessage *ParsedMessage,
allowCreate, ignoreUnkownMailboxIDs bool,
) *MessageUpdated {
return &MessageUpdated{
updateWaiter: newUpdateWaiter(),
Message: message,
Literal: literal,
MailboxIDs: mailboxIDs,
ParsedMessage: parsedMessage,
AllowCreate: allowCreate,
IgnoreUnknownMailboxIDs: ignoreUnkownMailboxIDs,
}
}
func (u *MessageUpdated) String() string {
return fmt.Sprintf("MessageUpdated: ID:%v Mailboxes:%v Flags:%s AllowCreate:%v IgnoreUnkownMailboxIDs:%v",
u.Message.ID.ShortID(),
xslices.Map(u.MailboxIDs, func(mboxID MailboxID) string {
return mboxID.ShortID()
}),
u.Message.Flags.ToSlice(),
u.AllowCreate,
u.IgnoreUnknownMailboxIDs,
)
}
package imap
type Noop struct {
updateBase
*updateWaiter
}
func NewNoop() *Noop {
return &Noop{
updateWaiter: newUpdateWaiter(),
}
}
func (u *Noop) String() string {
return "Noop"
}
package imap
import "fmt"
type UIDValidityBumped struct {
updateBase
*updateWaiter
}
func NewUIDValidityBumped() *UIDValidityBumped {
return &UIDValidityBumped{
updateWaiter: newUpdateWaiter(),
}
}
func (u *UIDValidityBumped) String() string {
return fmt.Sprintf("UIDValidityBumped")
}
package imap
import (
"context"
)
type Waiter interface {
// Wait waits until the update has been marked as done.
Wait() (error, bool)
// WaitContext waits until the update has been marked as done or the context is cancelled.
WaitContext(context.Context) (error, bool)
// Done marks the update as done and report an error (if any).
Done(error)
}
type updateWaiter struct {
waitCh chan error
}
func newUpdateWaiter() *updateWaiter {
return &updateWaiter{
waitCh: make(chan error, 1),
}
}
func (w *updateWaiter) Wait() (error, bool) {
err, ok := <-w.waitCh
return err, ok
}
func (w *updateWaiter) WaitContext(ctx context.Context) (error, bool) {
select {
case <-ctx.Done():
return nil, false
case err, ok := <-w.waitCh:
return err, ok
}
}
func (w *updateWaiter) Done(err error) {
if err != nil {
w.waitCh <- err
}
close(w.waitCh)
}
package imap
// ShortID return a string containing a short version of the given ID. Use only for debug display.
func ShortID(id string) string {
const l = 36
if len(id) < l {
return id
}
return id[0:l] + "..."
}
package rfc5322
import (
"net/mail"
"github.com/ProtonMail/gluon/rfcparser"
)
// 3.4. Address Specification
func parseAddressList(p *Parser) ([]*mail.Address, error) {
// address-list = (address *("," address)) / obs-addr-list
// *([CFWS] ",") address *("," [address / CFWS])
// We extended this rule to allow ';' as separator
var result []*mail.Address
isSep := func(tokenType rfcparser.TokenType) bool {
return tokenType == rfcparser.TokenTypeComma || tokenType == rfcparser.TokenTypeSemicolon
}
// *([CFWS] ",")
for {
if _, err := tryParseCFWS(p.parser); err != nil {
return nil, err
}
if ok, err := p.parser.MatchesWith(isSep); err != nil {
return nil, err
} else if !ok {
break
}
}
var groupConsumedSemiColon bool
// Address
{
addr, gConsumedSemiColon, err := parseAddress(p)
if err != nil {
return nil, err
}
groupConsumedSemiColon = gConsumedSemiColon
result = append(result, addr...)
}
// *("," [address / CFWS])
for {
if ok, err := p.parser.MatchesWith(isSep); err != nil {
return nil, err
} else if !ok { // see `parseAddress` comment about why this is necessary.
if !groupConsumedSemiColon || p.parser.CurrentToken().TType == rfcparser.TokenTypeEOF {
break
}
}
if ok, err := tryParseCFWS(p.parser); err != nil {
return nil, err
} else if ok {
// Only continue if the next input is EOF or comma or we can run into issues with parsing
// the `',' address` rules.
if p.parser.Check(rfcparser.TokenTypeEOF) || p.parser.CheckWith(isSep) {
continue
}
}
// Check there is still input to be processed before continuing.
if p.parser.Check(rfcparser.TokenTypeEOF) {
break
}
// address
addr, consumedSemiColon, err := parseAddress(p)
if err != nil {
return nil, err
}
groupConsumedSemiColon = consumedSemiColon
result = append(result, addr...)
}
return result, nil
}
// The boolean parameter represents whether a group consumed a ';' separator. This is necessary to disambiguate
// an address list where we have the sequence ` g:<address>;<address>` since we also allow groups to have optional
// `;` terminators.
func parseAddress(p *Parser) ([]*mail.Address, bool, error) {
// address = mailbox / group
// name-addr = [display-name] angle-addr
// group = display-name ":" [group-list] ";" [CFWS]
//
if _, err := tryParseCFWS(p.parser); err != nil {
return nil, false, err
}
// check addr-spec standalone
if p.parser.Check(rfcparser.TokenTypeLess) {
addr, err := parseAngleAddr(p.parser)
if err != nil {
return nil, false, err
}
return []*mail.Address{{
Name: "",
Address: addr,
}}, false, nil
}
parserState := p.SaveState()
if address, err := parseMailbox(p); err == nil {
return []*mail.Address{
address,
}, false, nil
}
p.RestoreState(parserState)
group, didConsumeSemicolon, err := parseGroup(p)
if err != nil {
return nil, false, err
}
return group, didConsumeSemicolon, nil
}
func parseGroup(p *Parser) ([]*mail.Address, bool, error) {
// nolint:dupword
// group = display-name ":" [group-list] ";" [CFWS]
// group-list = mailbox-list / CFWS / obs-group-list
// obs-group-list = 1*([CFWS] ",") [CFWS]
//
// nolint:dupword
// mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list
// obs-mbox-list = *([CFWS] ",") mailbox *("," [mailbox / CFWS])
//
// This version has been relaxed so that the ';' is optional. and that a group can be wrapped in `"`
hasQuotes, err := p.parser.Matches(rfcparser.TokenTypeDQuote)
if err != nil {
return nil, false, err
}
if _, err := parseDisplayName(p.parser); err != nil {
return nil, false, err
}
if err := p.parser.Consume(rfcparser.TokenTypeColon, "expected ':' for group start"); err != nil {
return nil, false, err
}
var didConsumeSemicolon bool
var result []*mail.Address
if ok, err := p.parser.Matches(rfcparser.TokenTypeSemicolon); err != nil {
return nil, false, err
} else if !ok {
// *([CFWS] ",")
for {
if _, err := tryParseCFWS(p.parser); err != nil {
return nil, false, err
}
if ok, err := p.parser.Matches(rfcparser.TokenTypeComma); err != nil {
return nil, false, err
} else if !ok {
break
}
}
// This section can optionally be one of the following: mailbox-list / CFWS / obs-group-list. So if
// we run out of input, we see semicolon or a double quote we should skip trying to parse this bit.
if !(p.parser.Check(rfcparser.TokenTypeEOF) ||
p.parser.Check(rfcparser.TokenTypeSemicolon) ||
p.parser.Check(rfcparser.TokenTypeDQuote)) {
// Mailbox
var parsedFirstMailbox bool
{
parserState := p.SaveState()
mailbox, err := parseMailbox(p)
if err != nil {
p.RestoreState(parserState)
} else {
parsedFirstMailbox = true
result = append(result, mailbox)
}
}
// *("," [mailbox / CFWS])
if parsedFirstMailbox {
for {
if ok, err := p.parser.Matches(rfcparser.TokenTypeComma); err != nil {
return nil, false, err
} else if !ok {
break
}
if ok, err := tryParseCFWS(p.parser); err != nil {
return nil, false, err
} else if ok {
continue
}
// Mailbox
mailbox, err := parseMailbox(p)
if err != nil {
return nil, false, err
}
result = append(result, mailbox)
}
} else {
// If we did not parse a mailbox then we must parse CWFS
if err := parseCFWS(p.parser); err != nil {
return nil, false, err
}
}
}
consumedSemicolon, err := p.parser.Matches(rfcparser.TokenTypeSemicolon)
if err != nil {
return nil, false, err
}
didConsumeSemicolon = consumedSemicolon
} else {
didConsumeSemicolon = true
}
if _, err := tryParseCFWS(p.parser); err != nil {
return nil, false, err
}
if hasQuotes {
if err := p.parser.Consume(rfcparser.TokenTypeDQuote, `expected '"' for group end`); err != nil {
return nil, false, err
}
}
return result, didConsumeSemicolon, nil
}
func parseMailbox(p *Parser) (*mail.Address, error) {
// mailbox = name-addr / addr-spec
parserState := p.SaveState()
if addr, err := parseNameAddr(p.parser); err == nil {
return addr, nil
}
p.RestoreState(parserState)
addr, err := parseAddrSpec(p.parser)
if err != nil {
return nil, err
}
return &mail.Address{
Address: addr,
}, nil
}
func parseNameAddr(p *rfcparser.Parser) (*mail.Address, error) {
// name-addr = [display-name] angle-addr
if _, err := tryParseCFWS(p); err != nil {
return nil, err
}
// Only has angle-addr component.
if p.Check(rfcparser.TokenTypeLess) {
address, err := parseAngleAddr(p)
if err != nil {
return nil, err
}
return &mail.Address{Address: address}, nil
}
displayName, err := parseDisplayName(p)
if err != nil {
return nil, err
}
address, err := parseAngleAddr(p)
if err != nil {
return nil, err
}
return &mail.Address{Address: address, Name: displayName}, nil
}
func parseAngleAddr(p *rfcparser.Parser) (string, error) {
// angle-addr = [CFWS] "<" addr-spec ">" [CFWS] /
// obs-angle-addr
//
// obs-angle-addr = [CFWS] "<" obs-route addr-spec ">" [CFWS]
//
// obs-route = obs-domain-list ":"
//
// obs-domain-list = *(CFWS / ",") "@" domain
// *("," [CFWS] ["@" domain])
//
// This version has been extended so that add-rspec is optional
if _, err := tryParseCFWS(p); err != nil {
return "", err
}
if err := p.Consume(rfcparser.TokenTypeLess, "expected < for angle-addr start"); err != nil {
return "", err
}
if ok, err := p.Matches(rfcparser.TokenTypeGreater); err != nil {
return "", err
} else if ok {
return "", nil
}
for {
if ok, err := tryParseCFWS(p); err != nil {
return "", err
} else if !ok {
if ok, err := p.Matches(rfcparser.TokenTypeComma); err != nil {
return "", err
} else if !ok {
break
}
}
}
if ok, err := p.Matches(rfcparser.TokenTypeAt); err != nil {
return "", err
} else if ok {
if _, err := parseDomain(p); err != nil {
return "", err
}
for {
if ok, err := p.Matches(rfcparser.TokenTypeComma); err != nil {
return "", err
} else if !ok {
break
}
if _, err := tryParseCFWS(p); err != nil {
return "", err
}
if ok, err := p.Matches(rfcparser.TokenTypeAt); err != nil {
return "", err
} else if ok {
if _, err := parseDomain(p); err != nil {
return "", err
}
}
}
if err := p.Consume(rfcparser.TokenTypeColon, "expected ':' for obs-route end"); err != nil {
return "", err
}
}
addr, err := parseAddrSpec(p)
if err != nil {
return "", err
}
if err := p.Consume(rfcparser.TokenTypeGreater, "expected > for angle-addr end"); err != nil {
return "", err
}
if _, err := tryParseCFWS(p); err != nil {
return "", err
}
return addr, nil
}
func parseDisplayName(p *rfcparser.Parser) (string, error) {
// display-name = phrase
phrase, err := parsePhrase(p)
if err != nil {
return "", err
}
return joinWithSpacingRules(phrase), nil
}
func parseAddrSpec(p *rfcparser.Parser) (string, error) {
// addr-spec = local-part "@" domain
// This version adds an option port extension : COLON ATOM
localPart, err := parseLocalPart(p)
if err != nil {
return "", err
}
if err := p.Consume(rfcparser.TokenTypeAt, "expected @ after local-part"); err != nil {
return "", err
}
domain, err := parseDomain(p)
if err != nil {
return "", err
}
if ok, err := p.Matches(rfcparser.TokenTypeColon); err != nil {
return "", err
} else if ok {
port, err := parseAtom(p)
if err != nil {
return "", err
}
return localPart + "@" + domain + ":" + port.String.Value, nil
}
return localPart + "@" + domain, nil
}
func parseLocalPart(p *rfcparser.Parser) (string, error) {
// nolint:dupword
// local-part = dot-atom / quoted-string / obs-local-part
// obs-local-part = word *("." word)
// word = atom / quoted-string
// ^ above rule can be relaxed into just the last part, dot-atom just
// Local part extended
var words []parserString
{
word, err := parseWord(p)
if err != nil {
return "", err
}
words = append(words, word)
}
for {
if ok, err := p.Matches(rfcparser.TokenTypePeriod); err != nil {
return "", err
} else if !ok {
break
}
words = append(words, parserString{
String: rfcparser.String{
Value: ".",
Offset: p.PreviousToken().Offset,
},
Type: parserStringTypeUnspaced,
})
word, err := parseWord(p)
if err != nil {
return "", err
}
words = append(words, word)
}
return joinWithSpacingRules(words), nil
}
func parseDomain(p *rfcparser.Parser) (string, error) {
// domain = dot-atom / domain-literal / obs-domain
//
// obs-domain = atom *("." atom)
//
if _, err := tryParseCFWS(p); err != nil {
return "", err
}
if ok, err := p.Matches(rfcparser.TokenTypeLBracket); err != nil {
return "", err
} else if ok {
return parseDomainLiteral(p)
}
// obs-domain can be seen as a more restrictive dot-atom so we just use that rule instead.
dotAtom, err := parseDotAtom(p)
if err != nil {
return "", err
}
return dotAtom.Value, nil
}
func parseDomainLiteral(p *rfcparser.Parser) (string, error) {
// domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
//
// [CFWS] and "[" consumed before entry
//
result := []byte{'['}
for {
if _, err := tryParseFWS(p); err != nil {
return "", err
}
if ok, err := p.MatchesWith(isDText); err != nil {
return "", err
} else if !ok {
break
}
result = append(result, p.PreviousToken().Value)
}
if _, err := tryParseFWS(p); err != nil {
return "", err
}
if err := p.Consume(rfcparser.TokenTypeRBracket, "expecetd ] for domain-literal end"); err != nil {
return "", err
}
result = append(result, ']')
if _, err := tryParseCFWS(p); err != nil {
return "", err
}
return string(result), nil
}
func isDText(tokenType rfcparser.TokenType) bool {
// dtext = %d33-90 / ; Printable US-ASCII
// %d94-126 / ; characters not including
// obs-dtext ; "[", "]", or "\"
//
// obs-dtext = obs-NO-WS-CTL / quoted-pair // <- we have not included this
//
if rfcparser.IsCTL(tokenType) ||
tokenType == rfcparser.TokenTypeLBracket ||
tokenType == rfcparser.TokenTypeRBracket ||
tokenType == rfcparser.TokenTypeBackslash ||
tokenType == rfcparser.TokenTypeEOF {
return false
}
return true
}
func joinWithSpacingRules(v []parserString) string {
result := v[0].String.Value
prevStrType := v[0].Type
for i := 1; i < len(v); i++ {
curStrType := v[i].Type
if prevStrType == parserStringTypeEncoded {
if curStrType == parserStringTypeOther {
result += " "
}
} else if prevStrType != parserStringTypeUnspaced {
if curStrType != parserStringTypeUnspaced {
result += " "
}
}
prevStrType = curStrType
result += v[i].String.Value
}
return result
}
package rfc5322
// 3.2.4. Quoted Strings
import (
"fmt"
"io"
"mime"
"github.com/ProtonMail/gluon/rfcparser"
)
func parseDotAtom(p *rfcparser.Parser) (rfcparser.String, error) {
// dot-atom = [CFWS] dot-atom-text [CFWS]
if _, err := tryParseCFWS(p); err != nil {
return rfcparser.String{}, err
}
atom, err := parseDotAtomText(p)
if err != nil {
return rfcparser.String{}, err
}
if _, err := tryParseCFWS(p); err != nil {
return rfcparser.String{}, err
}
return atom, nil
}
func parseDotAtomText(p *rfcparser.Parser) (rfcparser.String, error) {
// dot-atom-text = 1*atext *("." 1*atext)
// This version has been extended to allow for trailing '.' files.
if err := p.ConsumeWith(isAText, "expected atext char for dot-atom-text"); err != nil {
return rfcparser.String{}, err
}
atom, err := p.CollectBytesWhileMatchesWithPrevWith(isAText)
if err != nil {
return rfcparser.String{}, err
}
for {
if ok, err := p.Matches(rfcparser.TokenTypePeriod); err != nil {
return rfcparser.String{}, err
} else if !ok {
break
}
atom.Value = append(atom.Value, '.')
if p.Check(rfcparser.TokenTypePeriod) {
return rfcparser.String{}, p.MakeError("invalid token after '.'")
}
// Early exit to allow trailing '.'
if !p.CheckWith(isAText) {
break
}
if err := p.ConsumeWith(isAText, "expected atext char for dot-atom-text"); err != nil {
return rfcparser.String{}, err
}
atomNext, err := p.CollectBytesWhileMatchesWithPrevWith(isAText)
if err != nil {
return rfcparser.String{}, err
}
atom.Value = append(atom.Value, atomNext.Value...)
}
return atom.IntoString(), nil
}
func parseAtom(p *rfcparser.Parser) (parserString, error) {
// atom = [CFWS] 1*atext [CFWS]
if _, err := tryParseCFWS(p); err != nil {
return parserString{}, err
}
if err := p.ConsumeWith(isAText, "expected atext char for atom"); err != nil {
return parserString{}, err
}
atom, err := p.CollectBytesWhileMatchesWithPrevWith(isAText)
if err != nil {
return parserString{}, err
}
if _, err := tryParseCFWS(p); err != nil {
return parserString{}, err
}
return parserString{
String: atom.IntoString(),
Type: parserStringTypeOther,
}, nil
}
var CharsetReader func(charset string, input io.Reader) (io.Reader, error)
func parseEncodedAtom(p *rfcparser.Parser) (parserString, error) {
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
//
// charset = token ; see section 3
//
// encoding = token ; see section 4
//
//
if _, err := tryParseCFWS(p); err != nil {
return parserString{}, err
}
var fullWord string
startOffset := p.CurrentToken().Offset
if err := p.ConsumeBytesFold('=', '?'); err != nil {
return parserString{}, err
}
fullWord += "=?"
charset, err := p.CollectBytesWhileMatchesWith(isEncodedAtomToken)
if err != nil {
return parserString{}, err
}
fullWord += charset.IntoString().Value
if err := p.Consume(rfcparser.TokenTypeQuestion, "expected '?' after encoding charset"); err != nil {
return parserString{}, err
}
fullWord += "?"
if err := p.Consume(rfcparser.TokenTypeChar, "expected char after '?'"); err != nil {
return parserString{}, err
}
encoding := rfcparser.ByteToLower(p.PreviousToken().Value)
if encoding != 'q' && encoding != 'b' {
return parserString{}, p.MakeError("encoding should either be 'Q' or 'B'")
}
if err := p.Consume(rfcparser.TokenTypeQuestion, "expected '?' after encoding byte"); err != nil {
return parserString{}, err
}
if encoding == 'b' {
fullWord += "B"
} else {
fullWord += "Q"
}
fullWord += "?"
encodedText, err := p.CollectBytesWhileMatchesWith(isEncodedText)
if err != nil {
return parserString{}, err
}
fullWord += encodedText.IntoString().Value
if err := p.ConsumeBytesFold('?', '='); err != nil {
return parserString{}, err
}
fullWord += "?="
if _, err := tryParseCFWS(p); err != nil {
return parserString{}, err
}
decoder := mime.WordDecoder{CharsetReader: CharsetReader}
decoded, err := decoder.Decode(fullWord)
if err != nil {
return parserString{}, p.MakeErrorAtOffset(fmt.Sprintf("failed to decode encoded atom: %v", err), startOffset)
}
return parserString{
String: rfcparser.String{Value: decoded, Offset: startOffset},
Type: parserStringTypeEncoded,
}, nil
}
func isEncodedAtomToken(tokenType rfcparser.TokenType) bool {
// token = 1*<Any CHAR except SPACE, CTLs, and especials>
//
// specials = "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "
// <"> / "/" / "[" / "]" / "?" / "." / "="
if rfcparser.IsCTL(tokenType) {
return false
}
switch tokenType { //nolint:exhaustive
case rfcparser.TokenTypeEOF:
fallthrough
case rfcparser.TokenTypeError:
fallthrough
case rfcparser.TokenTypeSP:
fallthrough
case rfcparser.TokenTypeLParen:
fallthrough
case rfcparser.TokenTypeRParen:
fallthrough
case rfcparser.TokenTypeLess:
fallthrough
case rfcparser.TokenTypeGreater:
fallthrough
case rfcparser.TokenTypeAt:
fallthrough
case rfcparser.TokenTypeComma:
fallthrough
case rfcparser.TokenTypeSemicolon:
fallthrough
case rfcparser.TokenTypeColon:
fallthrough
case rfcparser.TokenTypeDQuote:
fallthrough
case rfcparser.TokenTypeSlash:
fallthrough
case rfcparser.TokenTypeLBracket:
fallthrough
case rfcparser.TokenTypeRBracket:
fallthrough
case rfcparser.TokenTypeQuestion:
fallthrough
case rfcparser.TokenTypePeriod:
fallthrough
case rfcparser.TokenTypeEqual:
return false
default:
return true
}
}
func isEncodedText(tokenType rfcparser.TokenType) bool {
// encoded-text = 1*<Any printable ASCII character other than "?"
// or SPACE>
// ; (but see "Use of encoded-words in message
// ; headers", section 5)
//
if rfcparser.IsCTL(tokenType) ||
tokenType == rfcparser.TokenTypeSP ||
tokenType == rfcparser.TokenTypeQuestion ||
tokenType == rfcparser.TokenTypeEOF ||
tokenType == rfcparser.TokenTypeError ||
tokenType == rfcparser.TokenTypeExtendedChar {
return false
}
return true
}
func isAText(tokenType rfcparser.TokenType) bool {
// atext = ALPHA / DIGIT / ; Printable US-ASCII
// "!" / "#" / ; characters not including
// "$" / "%" / ; specials. Used for atoms.
// "&" / "'" /
// "*" / "+" /
// "-" / "/" /
// "=" / "?" /
// "^" / "_" /
// "`" / "{" /
// "|" / "}" /
// "~"
switch tokenType { //nolint:exhaustive
case rfcparser.TokenTypeDigit:
fallthrough
case rfcparser.TokenTypeChar:
fallthrough
case rfcparser.TokenTypeExclamation:
fallthrough
case rfcparser.TokenTypeHash:
fallthrough
case rfcparser.TokenTypeDollar:
fallthrough
case rfcparser.TokenTypePercent:
fallthrough
case rfcparser.TokenTypeAmpersand:
fallthrough
case rfcparser.TokenTypeSQuote:
fallthrough
case rfcparser.TokenTypeAsterisk:
fallthrough
case rfcparser.TokenTypePlus:
fallthrough
case rfcparser.TokenTypeMinus:
fallthrough
case rfcparser.TokenTypeSlash:
fallthrough
case rfcparser.TokenTypeEqual:
fallthrough
case rfcparser.TokenTypeQuestion:
fallthrough
case rfcparser.TokenTypeCaret:
fallthrough
case rfcparser.TokenTypeUnderscore:
fallthrough
case rfcparser.TokenTyeBacktick:
fallthrough
case rfcparser.TokenTypeLCurly:
fallthrough
case rfcparser.TokenTypeRCurly:
fallthrough
case rfcparser.TokenTypePipe:
fallthrough
case rfcparser.TokenTypeExtendedChar: // RFC6532
fallthrough
case rfcparser.TokenTypeTilde:
return true
default:
return false
}
}
package rfc5322
import (
"bytes"
"io"
)
type BacktrackingByteScanner struct {
data []byte
offset int
}
func NewBacktrackingByteScanner(data []byte) *BacktrackingByteScanner {
return &BacktrackingByteScanner{
data: data,
}
}
type BacktrackingByteScannerScope struct {
offset int
}
func (bs *BacktrackingByteScanner) Read(dst []byte) (int, error) {
thisLen := len(bs.data)
if bs.offset >= thisLen {
return 0, io.EOF
}
dstLen := len(dst)
if bs.offset+dstLen >= thisLen {
bytesRead := thisLen - bs.offset
copy(dst, bs.data[bs.offset:])
return bytesRead, nil
}
nextOffset := bs.offset + dstLen
copy(dst, bs.data[bs.offset:nextOffset])
bs.offset = nextOffset
return dstLen, nil
}
func (bs *BacktrackingByteScanner) ReadByte() (byte, error) {
if bs.offset >= len(bs.data) {
return 0, io.EOF
}
b := bs.data[bs.offset]
bs.offset++
return b, nil
}
func (bs *BacktrackingByteScanner) ReadBytes(delim byte) ([]byte, error) {
if bs.offset >= len(bs.data) {
return nil, io.EOF
}
var result []byte
index := bytes.IndexByte(bs.data[bs.offset:], delim)
if index < 0 {
copy(result, bs.data[bs.offset:])
bs.offset = len(bs.data)
return result, nil
}
nextOffset := bs.offset + index + 1
if nextOffset >= len(bs.data) {
copy(result, bs.data[bs.offset:])
bs.offset = len(bs.data)
} else {
copy(result, bs.data[bs.offset:nextOffset])
bs.offset = nextOffset
}
return result, nil
}
func (bs *BacktrackingByteScanner) SaveState() BacktrackingByteScannerScope {
return BacktrackingByteScannerScope{offset: bs.offset}
}
func (bs *BacktrackingByteScanner) RestoreState(scope BacktrackingByteScannerScope) {
bs.offset = scope.offset
}
package rfc5322
import "github.com/ProtonMail/gluon/rfcparser"
// Section 3.2.2 White space and Comments
func tryParseCFWS(p *rfcparser.Parser) (bool, error) {
if !p.CheckWith(func(tokenType rfcparser.TokenType) bool {
return isWSP(tokenType) || tokenType == rfcparser.TokenTypeCR || tokenType == rfcparser.TokenTypeLParen
}) {
return false, nil
}
return true, parseCFWS(p)
}
func parseCFWS(p *rfcparser.Parser) error {
// CFWS = (1*([FWS] comment) [FWS]) / FWS
parsedFirstFWS, err := tryParseFWS(p)
if err != nil {
return err
}
// Handle case where it can just be FWS without comment
if !p.Check(rfcparser.TokenTypeLParen) {
if !parsedFirstFWS {
return p.MakeError("expected FWS or comment for CFWS")
}
return nil
}
if err := parseComment(p); err != nil {
return err
}
// Read remaining [FWS] comment
for {
if _, err := tryParseFWS(p); err != nil {
return err
}
if !p.Check(rfcparser.TokenTypeLParen) {
break
}
if err := parseComment(p); err != nil {
return err
}
}
if _, err := tryParseFWS(p); err != nil {
return err
}
return nil
}
func tryParseFWS(p *rfcparser.Parser) (bool, error) {
if !p.CheckWith(func(tokenType rfcparser.TokenType) bool {
return isWSP(tokenType) || tokenType == rfcparser.TokenTypeCR
}) {
return false, nil
}
return true, parseFWS(p)
}
func parseFWS(p *rfcparser.Parser) error {
// FWS = ([*WSP CRLF] 1*WSP) / obs-FWS
// ; Folding white space
// obs-FWS = 1*WSP *(CRLF 1*WSP)
//
// Parse 0 or more WSP
for {
if ok, err := p.MatchesWith(isWSP); err != nil {
return err
} else if !ok {
break
}
}
if !p.Check(rfcparser.TokenTypeCR) {
// Early exit.
return nil
}
if err := p.ConsumeNewLine(); err != nil {
return err
}
// Parse one or many WSP.
if err := p.ConsumeWith(isWSP, "expected WSP after CRLF"); err != nil {
return err
}
for {
if ok, err := p.MatchesWith(isWSP); err != nil {
return err
} else if !ok {
break
}
}
// Handle obs-FWS case where there can be multiple repeating loops
for {
if !p.Check(rfcparser.TokenTypeCR) {
break
}
if err := p.ConsumeNewLine(); err != nil {
return err
}
// Parse one or many WSP.
if err := p.ConsumeWith(isWSP, "expected WSP after CRLF"); err != nil {
return err
}
for {
if ok, err := p.MatchesWith(isWSP); err != nil {
return err
} else if !ok {
break
}
}
}
return nil
}
func parseCContent(p *rfcparser.Parser) error {
if ok, err := p.MatchesWith(isCText); err != nil {
return err
} else if ok {
return nil
}
if _, ok, err := tryParseQuotedPair(p); err != nil {
return err
} else if ok {
return nil
}
if p.Check(rfcparser.TokenTypeLParen) {
return parseComment(p)
}
return p.MakeError("unexpected ccontent token")
}
func parseComment(p *rfcparser.Parser) error {
if err := p.Consume(rfcparser.TokenTypeLParen, "expected ( for comment start"); err != nil {
return err
}
for {
if _, err := tryParseFWS(p); err != nil {
return err
}
if !p.CheckWith(func(tokenType rfcparser.TokenType) bool {
return isCText(tokenType) || tokenType == rfcparser.TokenTypeBackslash || tokenType == rfcparser.TokenTypeLParen
}) {
break
}
if err := parseCContent(p); err != nil {
return err
}
}
if _, err := tryParseFWS(p); err != nil {
return err
}
if err := p.Consume(rfcparser.TokenTypeRParen, "expected ) for comment end"); err != nil {
return err
}
return nil
}
func tryParseQuotedPair(p *rfcparser.Parser) (byte, bool, error) {
if !p.Check(rfcparser.TokenTypeBackslash) {
return 0, false, nil
}
b, err := parseQuotedPair(p)
if err != nil {
return 0, false, err
}
return b, true, nil
}
func parseQuotedPair(p *rfcparser.Parser) (byte, error) {
// quoted-pair = ("\" (VCHAR / WSP)) / obs-qp
//
// obs-qp = "\" (%d0 / obs-NO-WS-CTL / LF / CR)
//
if err := p.Consume(rfcparser.TokenTypeBackslash, "expected \\ for quoted pair start"); err != nil {
return 0, err
}
if ok, err := p.MatchesWith(isVChar); err != nil {
return 0, err
} else if ok {
return p.PreviousToken().Value, nil
}
if ok, err := p.MatchesWith(isWSP); err != nil {
return 0, err
} else if ok {
return p.PreviousToken().Value, nil
}
if ok, err := p.MatchesWith(func(tokenType rfcparser.TokenType) bool {
return isObsNoWSCTL(tokenType) ||
tokenType == rfcparser.TokenTypeCR ||
tokenType == rfcparser.TokenTypeLF ||
tokenType == rfcparser.TokenTypeZero
}); err != nil {
return 0, err
} else if ok {
return p.PreviousToken().Value, nil
}
return 0, p.MakeError("unexpected character for quoted pair")
}
func isWSP(tokenType rfcparser.TokenType) bool {
return tokenType == rfcparser.TokenTypeSP || tokenType == rfcparser.TokenTypeTab
}
func isCText(tokenType rfcparser.TokenType) bool {
// ctext = %d33-39 / ; Printable US-ASCII
// %d42-91 / ; characters not including
// %d93-126 / ; "(", ")", or "\"
// obs-ctext
//
// obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
// %d11 / ; characters that do not
// %d12 / ; include the carriage
// %d14-31 / ; return, line feed, and
// %d127 ; white space characters
//
// obs-ctext = obs-NO-WS-CTL
switch tokenType { // nolint:exhaustive
case rfcparser.TokenTypeEOF:
fallthrough
case rfcparser.TokenTypeError:
fallthrough
case rfcparser.TokenTypeLParen:
fallthrough
case rfcparser.TokenTypeRParen:
fallthrough
case rfcparser.TokenTypeCR:
fallthrough
case rfcparser.TokenTypeTab:
fallthrough
case rfcparser.TokenTypeLF:
fallthrough
case rfcparser.TokenTypeSP:
fallthrough
case rfcparser.TokenTypeBackslash:
return false
default:
return true
}
}
func isObsNoWSCTL(tokenType rfcparser.TokenType) bool {
// obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
// %d11 / ; characters that do not
// %d12 / ; include the carriage
// %d14-31 / ; return, line feed, and
// %d127 ; white space characters
switch tokenType { // nolint:exhaustive
case rfcparser.TokenTypeEOF:
fallthrough
case rfcparser.TokenTypeError:
fallthrough
case rfcparser.TokenTypeCR:
fallthrough
case rfcparser.TokenTypeTab:
fallthrough
case rfcparser.TokenTypeLF:
fallthrough
case rfcparser.TokenTypeSP:
return false
default:
return rfcparser.IsCTL(tokenType) || tokenType == rfcparser.TokenTypeDelete
}
}
func isVChar(tokenType rfcparser.TokenType) bool {
// VChar %x21-7E
if rfcparser.IsCTL(tokenType) ||
tokenType == rfcparser.TokenTypeDelete ||
tokenType == rfcparser.TokenTypeError ||
tokenType == rfcparser.TokenTypeEOF {
return false
}
return true
}
package rfc5322
import (
"fmt"
"time"
"github.com/ProtonMail/gluon/rfcparser"
)
func parseDTDateTime(p *rfcparser.Parser) (time.Time, error) {
// date-time = [ day-of-week "," ] date time [CFWS]
if _, err := tryParseCFWS(p); err != nil {
return time.Time{}, err
}
if p.Check(rfcparser.TokenTypeChar) {
if err := parseDTDayOfWeek(p); err != nil {
return time.Time{}, err
}
if err := p.Consume(rfcparser.TokenTypeComma, "expected ',' after day of the week"); err != nil {
return time.Time{}, err
}
}
year, month, day, err := parseDTDate(p)
if err != nil {
return time.Time{}, err
}
hour, min, sec, zone, err := parseDTTime(p)
if err != nil {
return time.Time{}, err
}
if _, err := tryParseCFWS(p); err != nil {
return time.Time{}, err
}
return time.Date(year, month, day, hour, min, sec, 0, zone), nil
}
func parseDTDayOfWeek(p *rfcparser.Parser) error {
// nolint:dupword
// day-of-week = ([FWS] day-name) / obs-day-of-week
// obs-day-of-week = [CFWS] day-name [CFWS]
//
if _, err := tryParseCFWS(p); err != nil {
return err
}
dayBytes, err := p.CollectBytesWhileMatches(rfcparser.TokenTypeChar)
if err != nil {
return err
}
dayStr := dayBytes.IntoString().ToLower()
_, ok := dateDaySet[dayStr.Value]
if !ok {
return p.MakeErrorAtOffset(fmt.Sprintf("invalid day name '%v'", dayStr.Value), dayBytes.Offset)
}
if _, err := tryParseCFWS(p); err != nil {
return err
}
return nil
}
// Return (year, month, day).
func parseDTDate(p *rfcparser.Parser) (int, time.Month, int, error) {
day, err := parseDTDay(p)
if err != nil {
return 0, 0, 0, err
}
month, err := parseDTMonth(p)
if err != nil {
return 0, 0, 0, err
}
year, err := parseDTYear(p)
if err != nil {
return 0, 0, 0, err
}
return year, month, day, nil
}
func parseDTDay(p *rfcparser.Parser) (int, error) {
// day = ([FWS] 1*2DIGIT FWS) / obs-day
//
// obs-day = [CFWS] 1*2DIGIT [CFWS]
//
if _, err := tryParseCFWS(p); err != nil {
return 0, err
}
if err := p.Consume(rfcparser.TokenTypeDigit, "expected digit for day value"); err != nil {
return 0, err
}
day := rfcparser.ByteToInt(p.PreviousToken().Value)
if ok, err := p.Matches(rfcparser.TokenTypeDigit); err != nil {
return 0, err
} else if ok {
day *= 10
day += rfcparser.ByteToInt(p.PreviousToken().Value)
}
if _, err := tryParseCFWS(p); err != nil {
return 0, err
}
return day, nil
}
func parseDTMonth(p *rfcparser.Parser) (time.Month, error) {
// month = "Jan" / "Feb" / "Mar" / "Apr" /
// "May" / "Jun" / "Jul" / "Aug" /
// "Sep" / "Oct" / "Nov" / "Dec"
//
month := make([]byte, 3)
for i := 0; i < 3; i++ {
if err := p.Consume(rfcparser.TokenTypeChar, "unexpected character for date month"); err != nil {
return 0, err
}
month[i] = p.PreviousToken().Value
}
v, ok := dateMonthToTimeMonth[string(month)]
if !ok {
return 0, p.MakeError(fmt.Sprintf("invalid date month '%v'", string(month)))
}
return v, nil
}
func parseDTYear(p *rfcparser.Parser) (int, error) {
// year = (FWS 4*DIGIT FWS) / obs-year
//
// obs-year = [CFWS] 2*DIGIT [CFWS]
//
if _, err := tryParseCFWS(p); err != nil {
return 0, err
}
year, err := p.ParseNumberN(2)
if err != nil {
return 0, err
}
if p.Check(rfcparser.TokenTypeDigit) {
yearPart2, err := p.ParseNumberN(2)
if err != nil {
return 0, err
}
year *= 100
year += yearPart2
} else {
if year > time.Now().Year()%100 {
year += 1900
} else {
year += 2000
}
}
if _, err := tryParseCFWS(p); err != nil {
return 0, err
}
return year, nil
}
func parseDTTime(p *rfcparser.Parser) (int, int, int, *time.Location, error) {
// time = time-of-day zone
//
hour, min, sec, err := parseDTTimeOfDay(p)
if err != nil {
return 0, 0, 0, nil, err
}
loc, err := parseDTZone(p)
if err != nil {
return 0, 0, 0, nil, err
}
return hour, min, sec, loc, nil
}
func parseDTTimeOfDay(p *rfcparser.Parser) (int, int, int, error) {
// time-of-day = hour ":" minute [ ":" second ]
hour, err := parseDTHour(p)
if err != nil {
return 0, 0, 0, err
}
if err := p.Consume(rfcparser.TokenTypeColon, "expected ':' after hour"); err != nil {
return 0, 0, 0, err
}
min, err := parseDTMin(p)
if err != nil {
return 0, 0, 0, err
}
var sec int
if ok, err := p.Matches(rfcparser.TokenTypeColon); err != nil {
return 0, 0, 0, err
} else if ok {
s, err := parseDTSecond(p)
if err != nil {
return 0, 0, 0, err
}
sec = s
}
return hour, min, sec, nil
}
func parseDTHour(p *rfcparser.Parser) (int, error) {
return parseDT2Digit(p)
}
func parseDTMin(p *rfcparser.Parser) (int, error) {
return parseDT2Digit(p)
}
func parseDTSecond(p *rfcparser.Parser) (int, error) {
return parseDT2Digit(p)
}
func parseDT2Digit(p *rfcparser.Parser) (int, error) {
// 2digit = 2DIGIT / obs-second
//
// obs-2digit = [CFWS] 2DIGIT [CFWS]
//
if _, err := tryParseCFWS(p); err != nil {
return 0, err
}
num, err := p.ParseNumberN(2)
if err != nil {
return 0, err
}
if _, err := tryParseCFWS(p); err != nil {
return 0, err
}
return num, nil
}
func parseDTZone(p *rfcparser.Parser) (*time.Location, error) {
// zone = (FWS ( "+" / "-" ) 4DIGIT) / obs-zone
//
// obs-zone = "UT" / "GMT" / ; Universal Time
// ; North American UT
// ; offsets
// "EST" / "EDT" / ; Eastern: - 5/ - 4
// "CST" / "CDT" / ; Central: - 6/ - 5
// "MST" / "MDT" / ; Mountain: - 7/ - 6
// "PST" / "PDT" / ; Pacific: - 8/ - 7
// ;
if _, err := tryParseCFWS(p); err != nil {
return nil, err
}
if !(p.Check(rfcparser.TokenTypeDigit) || p.Check(rfcparser.TokenTypeMinus) || p.Check(rfcparser.TokenTypePlus) || p.Check(rfcparser.TokenTypeChar)) {
return time.UTC, nil
}
multiplier := 1
if ok, err := p.Matches(rfcparser.TokenTypePlus); err != nil {
return nil, err
} else if !ok {
if ok, err := p.Matches(rfcparser.TokenTypeMinus); err != nil {
return nil, err
} else if ok {
multiplier = -1
} else if !(p.Check(rfcparser.TokenTypeDigit) || p.Check(rfcparser.TokenTypeChar)) {
return nil, p.MakeError("expected either '+' or '-' on time zone start")
}
}
// New format.
if p.Check(rfcparser.TokenTypeDigit) {
zoneHour, err := p.ParseNumberN(2)
if err != nil {
return nil, err
}
zoneMinute, err := p.ParseNumberN(2)
if err != nil {
return nil, err
}
zone := (zoneHour*3600 + zoneMinute*60) * multiplier
return time.FixedZone("zone", zone), nil
}
// Old Format
value, err := p.CollectBytesWhileMatches(rfcparser.TokenTypeChar)
if err != nil {
return nil, err
}
valueStr := value.IntoString().ToLower()
loc, ok := obsZoneToLocation[valueStr.Value]
if !ok {
return nil, p.MakeErrorAtOffset(fmt.Sprintf("unknown time zone '%v'", valueStr), value.Offset)
}
if _, err := tryParseCFWS(p); err != nil {
return nil, err
}
return loc, nil
}
var obsZoneToLocation = map[string]*time.Location{
"ut": time.FixedZone("ut", 0),
"gmt": time.FixedZone("gmt", 0),
"utc": time.FixedZone("utc", 0),
"est": time.FixedZone("est", -5*60*60),
"edt": time.FixedZone("edt", -4*60*60),
"cst": time.FixedZone("cst", -6*60*60),
"cdt": time.FixedZone("cdt", -5*60*60),
"mst": time.FixedZone("mst", -7*60*60),
"mdt": time.FixedZone("mdt", -6*60*60),
"pst": time.FixedZone("pst", -8*60*60),
"pdt": time.FixedZone("pdt", -7*60*60),
}
var dateMonthToTimeMonth = map[string]time.Month{
"Jan": time.January,
"Feb": time.February,
"Mar": time.March,
"Apr": time.April,
"May": time.May,
"Jun": time.June,
"Jul": time.July,
"Aug": time.August,
"Sep": time.September,
"Oct": time.October,
"Nov": time.November,
"Dec": time.December,
}
var dateDaySet = map[string]struct{}{
"mon": {},
"tue": {},
"wed": {},
"thu": {},
"fri": {},
"sat": {},
"sun": {},
}
package rfc5322
import (
"github.com/ProtonMail/gluon/rfcparser"
)
// 3.2.5. Miscellaneous Tokens
func parseWord(p *rfcparser.Parser) (parserString, error) {
// word = atom / quoted-string
if _, err := tryParseCFWS(p); err != nil {
return parserString{}, err
}
if p.Check(rfcparser.TokenTypeEqual) {
return parseEncodedAtom(p)
}
if p.Check(rfcparser.TokenTypeDQuote) {
return parseQuotedString(p)
}
result, err := parseAtom(p)
if err != nil {
return parserString{}, err
}
return result, nil
}
func parsePhrase(p *rfcparser.Parser) ([]parserString, error) {
// nolint:dupword
// phrase = 1*word / obs-phrase
// obs-phrase = word *(word / "." / CFWS)
// This version has been extended to allow '@' to appear in obs-phrase
word, err := parseWord(p)
if err != nil {
return nil, err
}
var result = []parserString{word}
isSep := func(tokenType rfcparser.TokenType) bool {
return tokenType == rfcparser.TokenTypePeriod || tokenType == rfcparser.TokenTypeAt
}
for {
// check period case
if ok, err := p.MatchesWith(isSep); err != nil {
return nil, err
} else if ok {
prevToken := p.PreviousToken()
result = append(result, parserString{
String: rfcparser.String{
Value: string(prevToken.Value),
Offset: prevToken.Offset,
},
Type: parserStringTypeUnspaced,
})
continue
}
if _, err := tryParseCFWS(p); err != nil {
return nil, err
}
if !(p.CheckWith(isAText) || p.Check(rfcparser.TokenTypeDQuote)) {
break
}
nextWord, err := parseWord(p)
if err != nil {
return nil, err
}
result = append(result, nextWord)
}
return result, nil
}
package rfc5322
import (
"net/mail"
"time"
"github.com/ProtonMail/gluon/rfcparser"
)
type Parser struct {
source *BacktrackingByteScanner
scanner *rfcparser.Scanner
parser *rfcparser.Parser
}
type parserStringType int
const (
parserStringTypeOther parserStringType = iota
parserStringTypeUnspaced
parserStringTypeEncoded
)
type parserString struct {
String rfcparser.String
Type parserStringType
}
func ParseAddress(input string) ([]*mail.Address, error) {
if len(input) == 0 {
return nil, nil
}
source := NewBacktrackingByteScanner([]byte(input))
scanner := rfcparser.NewScannerWithReader(source)
parser := rfcparser.NewParser(scanner)
p := Parser{
source: source,
scanner: scanner,
parser: parser,
}
if err := p.parser.Advance(); err != nil {
return nil, err
}
addr, _, err := parseAddress(&p)
return addr, err
}
func ParseAddressList(input string) ([]*mail.Address, error) {
if len(input) == 0 {
return nil, nil
}
source := NewBacktrackingByteScanner([]byte(input))
scanner := rfcparser.NewScannerWithReader(source)
parser := rfcparser.NewParser(scanner)
p := Parser{
source: source,
scanner: scanner,
parser: parser,
}
if err := p.parser.Advance(); err != nil {
return nil, err
}
return parseAddressList(&p)
}
func ParseDateTime(input string) (time.Time, error) {
source := NewBacktrackingByteScanner([]byte(input))
scanner := rfcparser.NewScannerWithReader(source)
parser := rfcparser.NewParser(scanner)
p := Parser{
source: source,
scanner: scanner,
parser: parser,
}
if err := p.parser.Advance(); err != nil {
return time.Time{}, err
}
return parseDTDateTime(p.parser)
}
type ParserState struct {
scanner BacktrackingByteScannerScope
parser rfcparser.ParserState
}
func (p *Parser) SaveState() ParserState {
scannerScope := p.source.SaveState()
return ParserState{
scanner: scannerScope,
parser: p.parser.SaveState(),
}
}
func (p *Parser) RestoreState(s ParserState) {
p.source.RestoreState(s.scanner)
p.parser.RestoreState(s.parser)
}
package rfc5322
// 3.2.4. Quoted Strings
import "github.com/ProtonMail/gluon/rfcparser"
func parseQuotedString(p *rfcparser.Parser) (parserString, error) {
var result rfcparser.Bytes
result.Offset = p.CurrentToken().Offset
if _, err := tryParseCFWS(p); err != nil {
return parserString{}, err
}
if err := p.Consume(rfcparser.TokenTypeDQuote, "expected \" for quoted string start"); err != nil {
return parserString{}, err
}
for {
if ok, err := tryParseFWS(p); err != nil {
return parserString{}, err
} else if ok {
result.Value = append(result.Value, ' ')
}
if !(p.CheckWith(isQText) || p.Check(rfcparser.TokenTypeBackslash)) {
break
}
if p.CheckWith(isQText) {
b, err := parseQContent(p)
if err != nil {
return parserString{}, err
}
result.Value = append(result.Value, b)
} else {
b, err := parseQuotedPair(p)
if err != nil {
return parserString{}, err
}
result.Value = append(result.Value, b)
}
}
if ok, err := tryParseFWS(p); err != nil {
return parserString{}, err
} else if ok {
result.Value = append(result.Value, ' ')
}
if err := p.Consume(rfcparser.TokenTypeDQuote, "expected \" for quoted string end"); err != nil {
return parserString{}, err
}
if _, err := tryParseCFWS(p); err != nil {
return parserString{}, err
}
return parserString{
String: result.IntoString(),
Type: parserStringTypeOther,
}, nil
}
func parseQContent(p *rfcparser.Parser) (byte, error) {
if ok, err := p.MatchesWith(isQText); err != nil {
return 0, err
} else if ok {
return p.PreviousToken().Value, nil
}
return parseQuotedPair(p)
}
func isQText(tokenType rfcparser.TokenType) bool {
// qtext = %d33 / ; Printable US-ASCII
// %d35-91 / ; characters not including
// %d93-126 / ; "\" or the quote character
// obs-qtext
//
// obs-qtext = obs-NO-WS-CTL
//
if (rfcparser.IsCTL(tokenType) && !isObsNoWSCTL(tokenType)) ||
tokenType == rfcparser.TokenTypeDQuote ||
tokenType == rfcparser.TokenTypeBackslash ||
tokenType == rfcparser.TokenTypeSP ||
tokenType == rfcparser.TokenTypeEOF ||
tokenType == rfcparser.TokenTypeError {
return false
}
return true
}
package rfc822
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"io"
"mime/quotedprintable"
"strings"
"github.com/ProtonMail/gluon/rfc5322"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
// GetMessageHash returns the hash of the given message.
// This takes into account:
// - the Subject header,
// - the From/To/Cc headers,
// - the Content-Type header of each (leaf) part,
// - the Content-Disposition header of each (leaf) part,
// - the (decoded) body of each part.
func GetMessageHash(b []byte) (string, error) {
section := Parse(b)
header, err := section.ParseHeader()
if err != nil {
return "", err
}
h := sha256.New()
if _, err := h.Write([]byte(header.Get("Subject"))); err != nil {
return "", err
}
if _, err := h.Write([]byte(getAddresses(header.Get("From")))); err != nil {
return "", err
}
if _, err := h.Write([]byte(getAddresses(header.Get("To")))); err != nil {
return "", err
}
if _, err := h.Write([]byte(getAddresses(header.Get("Cc")))); err != nil {
return "", err
}
if _, err := h.Write([]byte(getAddresses(header.Get("Reply-To")))); err != nil {
return "", err
}
if _, err := h.Write([]byte(getAddresses(header.Get("In-Reply-To")))); err != nil {
return "", err
}
if err := section.Walk(func(section *Section) error {
children, err := section.Children()
if err != nil {
return err
} else if len(children) > 0 {
return nil
}
header, err := section.ParseHeader()
if err != nil {
return err
}
contentType := header.Get("Content-Type")
mimeType, values, err := ParseMIMEType(contentType)
if err != nil {
logrus.Warnf("Message contains invalid mime type: %v", contentType)
} else {
if _, err := h.Write([]byte(mimeType)); err != nil {
return err
}
keys := maps.Keys(values)
slices.Sort(keys)
for _, k := range keys {
if strings.EqualFold(k, "boundary") {
continue
}
if _, err := h.Write([]byte(k)); err != nil {
return err
}
if _, err := h.Write([]byte(values[k])); err != nil {
return err
}
}
}
if _, err := h.Write([]byte(header.Get("Content-Disposition"))); err != nil {
return err
}
body := section.Body()
if err := hashBody(h, body, mimeType, header.Get("Content-Transfer-Encoding")); err != nil {
return err
}
return nil
}); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
}
func hashBody(writer io.Writer, body []byte, mimeType MIMEType, encoding string) error {
if mimeType != TextHTML && mimeType != TextPlain {
body = bytes.ReplaceAll(body, []byte{'\r'}, nil)
body = bytes.TrimSpace(body)
_, err := writer.Write(body)
return err
}
// We need to remove the transfer encoding from the text part as it is possible the that encoding sent to SMTP
// is different than the one sent to the IMAP client.
var decoded []byte
switch strings.ToLower(encoding) {
case "quoted-printable":
d, err := io.ReadAll(quotedprintable.NewReader(bytes.NewReader(body)))
if err != nil {
return err
}
decoded = d
case "base64":
d, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, bytes.NewReader(body)))
if err != nil {
return err
}
decoded = d
default:
decoded = body
}
decoded = bytes.ReplaceAll(decoded, []byte{'\r'}, nil)
decoded = bytes.TrimSpace(decoded)
_, err := writer.Write(decoded)
return err
}
func getAddresses(fieldAddr string) string {
var addresses string
addrList, err := rfc5322.ParseAddressList(fieldAddr)
if err != nil {
return fieldAddr
}
for _, addr := range addrList {
addresses += addr.Address
}
return addresses
}
package rfc822
import (
"bytes"
"errors"
"fmt"
"io"
"net/textproto"
"strings"
)
type headerEntry struct {
parsedHeaderEntry
mapKey string
merged string
prev *headerEntry
next *headerEntry
}
func (he *headerEntry) getMerged(data []byte) string {
if len(he.merged) == 0 {
he.merged = mergeMultiline(he.getValue(data))
}
return he.merged
}
type Header struct {
keys map[string][]*headerEntry
firstEntry *headerEntry
lastEntry *headerEntry
data []byte
}
// NewEmptyHeader returns an empty header that can be filled with values.
func NewEmptyHeader() *Header {
h, err := NewHeader([]byte{'\r', '\n'})
// The above code should never fail, but just in case.
if err != nil {
panic(err)
}
return h
}
func NewHeader(data []byte) (*Header, error) {
h := &Header{
keys: make(map[string][]*headerEntry),
data: data,
}
parser := newHeaderParser(data)
for {
entry, err := parser.next()
if err != nil {
if errors.Is(err, io.EOF) {
break
} else {
return nil, err
}
}
hentry := &headerEntry{
parsedHeaderEntry: entry,
merged: "",
next: nil,
}
if entry.hasKey() {
hashKey := strings.ToLower(string(entry.getKey(data)))
hentry.mapKey = hashKey
if v, ok := h.keys[hashKey]; !ok {
h.keys[hashKey] = []*headerEntry{hentry}
} else {
h.keys[hashKey] = append(v, hentry)
}
}
if h.firstEntry == nil {
h.firstEntry = hentry
h.lastEntry = hentry
} else {
h.lastEntry.next = hentry
hentry.prev = h.lastEntry
h.lastEntry = hentry
}
}
return h, nil
}
func (h *Header) Raw() []byte {
return h.data
}
func (h *Header) Has(key string) bool {
_, ok := h.keys[strings.ToLower(key)]
return ok
}
func (h *Header) GetChecked(key string) (string, bool) {
v, ok := h.keys[strings.ToLower(key)]
if !ok {
return "", false
}
return v[0].getMerged(h.data), true
}
func (h *Header) Get(key string) string {
v, ok := h.keys[strings.ToLower(key)]
if !ok {
return ""
}
return v[0].getMerged(h.data)
}
func (h *Header) GetLine(key string) []byte {
v, ok := h.keys[strings.ToLower(key)]
if !ok {
return nil
}
return v[0].getAll(h.data)
}
func (h *Header) getLines() [][]byte {
var res [][]byte
for e := h.firstEntry; e != nil; e = e.next {
res = append(res, h.data[e.keyStart:e.valueEnd])
}
return res
}
func (h *Header) GetRaw(key string) []byte {
v, ok := h.keys[strings.ToLower(key)]
if !ok {
return nil
}
return v[0].getValue(h.data)
}
func (h *Header) Set(key, val string) {
// We can only add entries to the front of the header.
key = textproto.CanonicalMIMEHeaderKey(key)
mapKey := strings.ToLower(key)
keyBytes := []byte(key)
entryBytes := joinLine([]byte(key), []byte(val))
newHeaderEntry := &headerEntry{
parsedHeaderEntry: parsedHeaderEntry{
keyStart: 0,
keyEnd: len(keyBytes),
valueStart: len(keyBytes) + 2,
valueEnd: len(entryBytes),
},
mapKey: mapKey,
}
if v, ok := h.keys[mapKey]; !ok {
h.keys[mapKey] = []*headerEntry{newHeaderEntry}
} else {
h.keys[mapKey] = append([]*headerEntry{newHeaderEntry}, v...)
}
if h.firstEntry == nil {
h.data = entryBytes
h.firstEntry = newHeaderEntry
} else {
insertOffset := h.firstEntry.keyStart
newHeaderEntry.next = h.firstEntry
h.firstEntry.prev = newHeaderEntry
h.firstEntry = newHeaderEntry
var buffer bytes.Buffer
if insertOffset != 0 {
if _, err := buffer.Write(h.data[0:insertOffset]); err != nil {
panic("failed to write to byte buffer")
}
}
if _, err := buffer.Write(entryBytes); err != nil {
panic("failed to write to byte buffer")
}
if _, err := buffer.Write(h.data[insertOffset:]); err != nil {
panic("failed to write to byte buffer")
}
h.data = buffer.Bytes()
h.applyOffset(newHeaderEntry.next, len(entryBytes))
}
}
func (h *Header) Del(key string) {
mapKey := strings.ToLower(key)
v, ok := h.keys[mapKey]
if !ok {
return
}
he := v[0]
if len(v) == 1 {
delete(h.keys, mapKey)
} else {
h.keys[mapKey] = v[1:]
}
if he.prev != nil {
he.prev.next = he.next
}
if he.next != nil {
he.next.prev = he.prev
}
dataLen := he.valueEnd - he.keyStart
h.data = append(h.data[0:he.keyStart], h.data[he.valueEnd:]...)
h.applyOffset(he.next, -dataLen)
}
func (h *Header) Fields(fields []string) []byte {
wantFields := make(map[string]struct{})
for _, field := range fields {
wantFields[strings.ToLower(field)] = struct{}{}
}
var res []byte
for e := h.firstEntry; e != nil; e = e.next {
if len(bytes.TrimSpace(e.getAll(h.data))) == 0 {
res = append(res, e.getAll(h.data)...)
continue
}
if !e.hasKey() {
continue
}
_, ok := wantFields[e.mapKey]
if !ok {
continue
}
res = append(res, e.getAll(h.data)...)
}
return res
}
func (h *Header) FieldsNot(fields []string) []byte {
wantFieldsNot := make(map[string]struct{})
for _, field := range fields {
wantFieldsNot[strings.ToLower(field)] = struct{}{}
}
var res []byte
for e := h.firstEntry; e != nil; e = e.next {
if len(bytes.TrimSpace(e.getAll(h.data))) == 0 {
res = append(res, e.getAll(h.data)...)
continue
}
if !e.hasKey() {
continue
}
_, ok := wantFieldsNot[e.mapKey]
if ok {
continue
}
res = append(res, e.getAll(h.data)...)
}
return res
}
func (h *Header) Entries(fn func(key, val string)) {
for e := h.firstEntry; e != nil; e = e.next {
if !e.hasKey() {
continue
}
fn(string(e.getKey(h.data)), e.getMerged(h.data))
}
}
func (h *Header) applyOffset(start *headerEntry, offset int) {
for e := start; e != nil; e = e.next {
e.applyOffset(offset)
}
}
// SetHeaderValue is a helper method that sets a header value in a message literal.
// It does not check whether the existing value already exists.
func SetHeaderValue(literal []byte, key, val string) ([]byte, error) {
reader, size, err := SetHeaderValueNoMemCopy(literal, key, val)
if err != nil {
return nil, err
}
var b bytes.Buffer
b.Grow(size)
if _, err := b.ReadFrom(reader); err != nil {
return nil, err
}
return b.Bytes(), nil
}
// SetHeaderValueNoMemCopy is the same as SetHeaderValue, except it does not allocate memory to modify the input literal.
// Instead, it returns an io.MultiReader that combines the sub-slices in the correct order. This enables us to only
// allocate memory for the new header field while re-using the old literal.
func SetHeaderValueNoMemCopy(literal []byte, key, val string) (io.Reader, int, error) {
rawHeader, body := Split(literal)
parser := newHeaderParser(rawHeader)
var (
foundFirstEntry bool
parsedHeaderEntry parsedHeaderEntry
)
// find first header entry.
for {
entry, err := parser.next()
if err != nil {
if errors.Is(err, io.EOF) {
break
} else {
return nil, 0, err
}
}
if entry.hasKey() {
foundFirstEntry = true
parsedHeaderEntry = entry
break
}
}
key = textproto.CanonicalMIMEHeaderKey(key)
data := joinLine([]byte(key), []byte(val))
if !foundFirstEntry {
return io.MultiReader(bytes.NewReader(rawHeader), bytes.NewReader(data), bytes.NewReader(body)), len(rawHeader) + len(data) + len(body), nil
}
part1 := literal[0:parsedHeaderEntry.keyStart]
part2 := literal[parsedHeaderEntry.keyStart:]
return io.MultiReader(
bytes.NewReader(part1),
bytes.NewReader(data),
bytes.NewReader(part2),
), len(part1) + len(part2) + len(data), nil
}
// GetHeaderValue is a helper method that queries a header value in a message literal.
func GetHeaderValue(literal []byte, key string) (string, error) {
rawHeader, _ := Split(literal)
parser := newHeaderParser(rawHeader)
for {
entry, err := parser.next()
if err != nil {
if errors.Is(err, io.EOF) {
break
} else {
return "", err
}
}
if !entry.hasKey() {
continue
}
if !strings.EqualFold(key, string(entry.getKey(rawHeader))) {
continue
}
return mergeMultiline(entry.getValue(rawHeader)), nil
}
return "", nil
}
// EraseHeaderValue removes the header from a literal.
func EraseHeaderValue(literal []byte, key string) ([]byte, error) {
rawHeader, _ := Split(literal)
parser := newHeaderParser(rawHeader)
var (
foundEntry bool
parsedHeaderEntry parsedHeaderEntry
)
for {
entry, err := parser.next()
if err != nil {
if errors.Is(err, io.EOF) {
break
} else {
return nil, err
}
}
if !entry.hasKey() {
continue
}
if !strings.EqualFold(key, string(entry.getKey(rawHeader))) {
continue
}
foundEntry = true
parsedHeaderEntry = entry
break
}
result := make([]byte, 0, len(literal))
if !foundEntry {
result = append(result, literal...)
} else {
result = append(result, literal[0:parsedHeaderEntry.keyStart]...)
result = append(result, literal[parsedHeaderEntry.valueEnd:]...)
}
return result, nil
}
var (
ErrNonASCIIHeaderKey = fmt.Errorf("header key contains invalid characters")
ErrKeyNotFound = fmt.Errorf("invalid header key")
ErrParseHeader = fmt.Errorf("failed to parse header")
)
func mergeMultiline(line []byte) string {
remaining := line
var builder strings.Builder
for len(remaining) != 0 {
index := bytes.Index(remaining, []byte{'\n'})
if index < 0 {
builder.Write(bytes.TrimSpace(remaining))
break
}
var section []byte
if index >= 1 && remaining[index-1] == '\r' {
section = remaining[0 : index-1]
} else {
section = remaining[0:index]
}
remaining = remaining[index+1:]
if len(section) != 0 {
builder.Write(bytes.TrimSpace(section))
if len(remaining) != 0 {
builder.WriteRune(' ')
}
}
}
return builder.String()
}
func splitLine(line []byte) [][]byte {
result := bytes.SplitN(line, []byte(`:`), 2)
if len(result) > 1 && len(result[1]) > 0 && result[1][0] == ' ' {
result[1] = result[1][1:]
}
return result
}
// TODO: Don't assume line ending is \r\n. Bad.
func joinLine(key, val []byte) []byte {
return []byte(string(key) + ": " + string(val) + "\r\n")
}
package rfc822
import (
"fmt"
"io"
)
type headerParser struct {
header []byte
offset int
}
func newHeaderParser(header []byte) headerParser {
return headerParser{header: header}
}
// next will keep parsing until it collects a new entry. io.EOF is returned when there is nothing left to parse.
func (hp *headerParser) next() (parsedHeaderEntry, error) {
headerLen := len(hp.header)
if hp.offset >= headerLen {
return parsedHeaderEntry{}, io.EOF
}
result := parsedHeaderEntry{
keyStart: hp.offset,
keyEnd: -1,
valueStart: -1,
valueEnd: -1,
}
// Detect key, have to handle prelude case where there is no header information or last empty new line.
{
for hp.offset < headerLen {
if hp.header[hp.offset] == ':' {
prevOffset := hp.offset
hp.offset++
if hp.offset < headerLen {
result.keyEnd = prevOffset
validateHeaderField := func(h parsedHeaderEntry) error {
for i := h.keyStart; i < h.keyEnd; i++ {
if v := hp.header[i]; v < 33 || v > 126 {
return ErrNonASCIIHeaderKey
}
}
return nil
}
switch {
case isWSP(hp.header[hp.offset]):
if err := validateHeaderField(result); err != nil {
return parsedHeaderEntry{}, err
}
case hp.header[hp.offset] == '\r':
// ensure next char is '\n'
hp.offset++
if hp.offset < headerLen && hp.header[hp.offset] != '\n' {
return parsedHeaderEntry{}, fmt.Errorf("expected \\n after \\r: %w", ErrParseHeader)
}
fallthrough
case hp.header[hp.offset] == '\n':
hp.offset++
if err := validateHeaderField(result); err != nil {
return parsedHeaderEntry{}, err
}
// If the next char it's not a space, it's an empty header field.
if hp.offset < headerLen && !isWSP(hp.header[hp.offset]) {
result.valueStart = result.keyEnd
result.valueEnd = result.keyEnd
return result, nil
}
case hp.header[hp.offset] == ':':
return parsedHeaderEntry{}, fmt.Errorf("unexpected char '%v', for header field value: %w", string(hp.header[hp.offset]), ErrParseHeader)
default:
if err := validateHeaderField(result); err != nil {
return parsedHeaderEntry{}, err
}
}
break
}
} else if hp.header[hp.offset] == '\n' {
hp.offset++
result.keyEnd = result.keyStart
result.valueStart = result.keyStart
result.valueEnd = hp.offset
return result, nil
} else {
hp.offset++
}
}
if result.keyEnd == -1 {
return parsedHeaderEntry{}, ErrKeyNotFound
}
}
// collect value.
searchOffset := hp.offset
for searchOffset < headerLen && isWSP(hp.header[searchOffset]) {
searchOffset++
}
if searchOffset < headerLen {
result.valueStart = searchOffset
} else {
result.valueStart = headerLen
}
for searchOffset < headerLen {
b := hp.header[searchOffset]
if b == '\r' {
searchOffset++
if searchOffset >= headerLen {
return parsedHeaderEntry{}, io.ErrUnexpectedEOF
}
if hp.header[searchOffset] != '\n' {
return parsedHeaderEntry{}, fmt.Errorf(`expected \n after \n`)
}
searchOffset++
// If the next character after new line is a space, it's a fold
if searchOffset < headerLen && isWSP(hp.header[searchOffset]) {
continue
}
result.valueEnd = searchOffset
break
} else if b == '\n' {
searchOffset++
// If the next character after new line is a space, it's a fold
if searchOffset < headerLen && isWSP(hp.header[searchOffset]) {
continue
}
result.valueEnd = searchOffset
break
} else {
searchOffset++
}
}
hp.offset = searchOffset
// handle case where we may have reached EOF without concluding any previous processing.
if result.valueEnd == -1 && searchOffset >= headerLen {
result.valueEnd = headerLen
}
return result, nil
}
func isWSP(b byte) bool {
return b == ' ' || b == '\t'
}
type parsedHeaderEntry struct {
keyStart int
keyEnd int
valueStart int
valueEnd int
}
func (p *parsedHeaderEntry) hasKey() bool {
return p.keyStart != p.keyEnd
}
func (p *parsedHeaderEntry) getKey(header []byte) []byte {
return header[p.keyStart:p.keyEnd]
}
func (p *parsedHeaderEntry) getValue(header []byte) []byte {
return header[p.valueStart:p.valueEnd]
}
func (p *parsedHeaderEntry) getAll(header []byte) []byte {
return header[p.keyStart:p.valueEnd]
}
func (p *parsedHeaderEntry) applyOffset(offset int) {
p.keyStart += offset
p.keyEnd += offset
p.valueStart += offset
p.valueEnd += offset
}
package rfc822
import (
"mime"
"strings"
"unicode"
)
// ParseMediaType parses a MIME media type.
var ParseMediaType = mime.ParseMediaType
type MIMEType string
const (
TextPlain MIMEType = "text/plain"
TextHTML MIMEType = "text/html"
MultipartMixed MIMEType = "multipart/mixed"
MultipartRelated MIMEType = "multipart/related"
MessageRFC822 MIMEType = "message/rfc822"
)
func (mimeType MIMEType) IsMultiPart() bool {
return strings.HasPrefix(string(mimeType), "multipart/")
}
func (mimeType MIMEType) Type() string {
if split := strings.SplitN(string(mimeType), "/", 2); len(split) == 2 {
return split[0]
}
return ""
}
func (mimeType MIMEType) SubType() string {
if split := strings.SplitN(string(mimeType), "/", 2); len(split) == 2 {
return split[1]
}
return ""
}
func ParseMIMEType(val string) (MIMEType, map[string]string, error) {
if val == "" {
val = string(TextPlain)
}
sanitized := strings.Map(func(r rune) rune {
if r > unicode.MaxASCII {
return -1
}
return r
}, val)
mimeType, mimeParams, err := ParseMediaType(sanitized)
if err != nil {
return "", nil, err
}
return MIMEType(mimeType), mimeParams, nil
}
package rfc822
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"mime/quotedprintable"
"github.com/sirupsen/logrus"
)
var ErrNoSuchPart = errors.New("no such parts exists")
type Section struct {
identifier []int
literal []byte
parsedHeader *Header
header int
body int
end int
children []*Section
}
func Parse(literal []byte) *Section {
literalCopy := make([]byte, len(literal))
copy(literalCopy, literal)
return parse(literalCopy, []int{}, 0, len(literal))
}
func (section *Section) Identifier() []int {
return section.identifier
}
func (section *Section) ContentType() (MIMEType, map[string]string, error) {
header, err := section.ParseHeader()
if err != nil {
return "", nil, err
}
contentType := header.Get("Content-Type")
mimeType, values, err := ParseMIMEType(contentType)
if err != nil {
logrus.Warnf("Message contains invalid mime type: %v", contentType)
return "", nil, nil //nolint:nilerr
}
return mimeType, values, nil
}
func (section *Section) Header() []byte {
return section.literal[section.header:section.body]
}
func (section *Section) ParseHeader() (*Header, error) {
if section.parsedHeader == nil {
h, err := NewHeader(section.Header())
if err != nil {
return nil, err
}
section.parsedHeader = h
}
return section.parsedHeader, nil
}
func (section *Section) Body() []byte {
return section.literal[section.body:section.end]
}
func (section *Section) DecodedBody() ([]byte, error) {
header, err := section.ParseHeader()
if err != nil {
return nil, err
}
switch header.Get("Content-Transfer-Encoding") {
case "base64":
return base64Decode(section.Body())
case "quoted-printable":
return quotedPrintableDecode(section.Body())
default:
return section.Body(), nil
}
}
func (section *Section) Literal() []byte {
return section.literal[section.header:section.end]
}
func (section *Section) Children() ([]*Section, error) {
if len(section.children) == 0 {
if err := section.load(); err != nil {
return nil, err
}
}
return section.children, nil
}
func (section *Section) Part(identifier ...int) (*Section, error) {
if len(identifier) > 0 {
children, err := section.Children()
if err != nil {
return nil, err
}
if identifier[0] <= 0 || identifier[0]-1 > len(children) {
return nil, ErrNoSuchPart
}
if len(children) != 0 {
childIndex := identifier[0] - 1
if childIndex >= len(children) {
return nil, fmt.Errorf("invalid part index")
}
return children[identifier[0]-1].Part(identifier[1:]...)
}
}
return section, nil
}
func (section *Section) Walk(f func(*Section) error) error {
if err := f(section); err != nil {
return err
}
children, err := section.Children()
if err != nil {
return err
}
for _, child := range children {
if err := child.Walk(f); err != nil {
return err
}
}
return nil
}
func (section *Section) load() error {
contentType, contentParams, err := section.ContentType()
if err != nil {
return err
}
if MIMEType(contentType) == MessageRFC822 {
child := parse(
section.literal[section.body:section.end],
section.identifier,
0,
section.end-section.body,
)
if err := child.load(); err != nil {
return err
}
section.children = append(section.children, child.children...)
} else if contentType.IsMultiPart() {
scanner, err := NewByteScanner(section.literal[section.body:section.end], []byte(contentParams["boundary"]))
if err != nil {
return err
}
res := scanner.ScanAll()
for idx, res := range res {
child := parse(
section.literal,
append(section.identifier, idx+1),
section.body+res.Offset,
section.body+res.Offset+len(res.Data),
)
section.children = append(section.children, child)
}
}
return nil
}
func Split(b []byte) ([]byte, []byte) {
remaining := b
splitIndex := int(0)
separator := []byte{'\n'}
for len(remaining) != 0 {
index := bytes.Index(remaining, separator)
if index < 0 {
splitIndex += len(remaining)
break
}
splitIndex += index + 1
if len(bytes.Trim(remaining[0:index], "\r\n")) == 0 {
break
}
remaining = remaining[index+1:]
}
return b[0:splitIndex], b[splitIndex:]
}
func parse(literal []byte, identifier []int, begin, end int) *Section {
header, _ := Split(literal[begin:end])
parsedHeader, err := NewHeader(header)
if err != nil {
header = nil
parsedHeader = nil
}
return &Section{
identifier: identifier,
literal: literal,
parsedHeader: parsedHeader,
header: begin,
body: begin + len(header),
end: end,
}
}
func base64Decode(b []byte) ([]byte, error) {
res := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
n, err := base64.StdEncoding.Decode(res, b)
if err != nil {
return nil, err
}
return res[0:n], nil
}
func quotedPrintableDecode(b []byte) ([]byte, error) {
return io.ReadAll(quotedprintable.NewReader(bytes.NewReader(b)))
}
package rfc822
import (
"bytes"
)
type Part struct {
Data []byte
Offset int
}
type ByteScanner struct {
data []byte
startBoundary []byte
progress int
}
func NewByteScanner(data []byte, boundary []byte) (*ByteScanner, error) {
scanner := &ByteScanner{
data: data,
startBoundary: append([]byte{'-', '-'}, boundary...),
}
scanner.readToBoundary()
return scanner, nil
}
func (s *ByteScanner) ScanAll() []Part {
var parts []Part
for {
offset := s.progress
data, more := s.readToBoundary()
if data != nil {
parts = append(parts, Part{Data: data, Offset: offset})
}
if !more {
return parts
}
}
}
func indexOfNewLineAfterBoundary(data []byte) int {
dataLen := len(data)
if dataLen == 0 {
return -1
}
if dataLen == 1 && data[0] == '\n' {
return 0
}
// consume extra '\r's
index := 0
for ; index < dataLen && data[index] == '\r'; index++ {
}
if index < dataLen && data[index] == '\n' {
return index
}
return -1
}
func (s *ByteScanner) getPreviousLineBreakIndex(offset int) int {
if s.progress == offset {
return 0
} else if s.data[offset-1] == '\n' {
if offset-s.progress >= 2 && s.data[offset-2] == '\r' {
return 2
}
return 1
}
return -1
}
// readToBoundary returns the slice matching to the boundary and whether this is the start or the end of said boundary.
func (s *ByteScanner) readToBoundary() ([]byte, bool) {
boundarySuffix := []byte{'-', '-'}
boundarySuffixLen := len(boundarySuffix)
boundaryLen := len(s.startBoundary)
dataLen := len(s.data)
searchStart := s.progress
for s.progress < dataLen {
remaining := s.data[s.progress:]
index := bytes.Index(remaining, s.startBoundary)
if index < 0 {
s.progress = len(s.data)
return remaining, false
}
// Matched the pattern, now we need to check if the previous line break is available or not. It can also not be
// available if the pattern just happens to match exactly at the offset search.
prevNewLineOffset := s.getPreviousLineBreakIndex(s.progress + index)
if prevNewLineOffset != -1 {
// Since we matched the pattern we can check whether this is a starting or terminating pattern.
if s.progress+index+boundaryLen+boundarySuffixLen <= dataLen &&
bytes.Equal(remaining[index+boundaryLen:index+boundaryLen+boundarySuffixLen], boundarySuffix) {
lineEndIndex := index + boundaryLen + boundarySuffixLen
afterBoundary := remaining[lineEndIndex:]
var newLineStartIndex int
// It can happen that this boundary is at the end of the file/message with no new line.
if len(afterBoundary) != 0 {
newLineStartIndex = indexOfNewLineAfterBoundary(afterBoundary)
// If there is no new line this can't be a boundary pattern. RFC 1341 states that tey are
// immediately followed by either \r\n or \n.
if newLineStartIndex < 0 {
s.progress += index + boundaryLen + boundarySuffixLen
continue
}
} else {
newLineStartIndex = 0
}
result := s.data[searchStart : s.progress+index-prevNewLineOffset]
s.progress += index + boundaryLen + boundarySuffixLen + newLineStartIndex + 1
return result, false
} else {
// Check for new line.
lineEndIndex := index + boundaryLen
afterBoundary := remaining[lineEndIndex:]
newLineStart := indexOfNewLineAfterBoundary(afterBoundary)
// If there is no new line this can't be a boundary pattern. RFC 1341 states that tey are
// immediately followed by either \r\n or \n.
if newLineStart < 0 {
s.progress += index + boundaryLen
continue
}
result := s.data[searchStart : s.progress+index-prevNewLineOffset]
s.progress += index + boundaryLen + newLineStart + 1
return result, true
}
}
s.progress += index + boundaryLen
}
return nil, false
}
package rfc822
import (
"bytes"
"fmt"
"io"
)
type MultipartWriter struct {
w io.Writer
boundary string
}
func NewMultipartWriter(w io.Writer, boundary string) *MultipartWriter {
return &MultipartWriter{w: w, boundary: boundary}
}
func (w *MultipartWriter) AddPart(fn func(io.Writer) error) error {
buf := new(bytes.Buffer)
if err := fn(buf); err != nil {
return err
}
if _, err := fmt.Fprintf(w.w, "--%v\r\n%v\r\n", w.boundary, buf.String()); err != nil {
return err
}
return nil
}
func (w *MultipartWriter) Done() error {
if _, err := fmt.Fprintf(w.w, "--%v--\r\n", w.boundary); err != nil {
return err
}
return nil
}