Update vendor/

This commit is contained in:
2018-06-28 01:09:56 +01:00
parent 3e5ab5bb0a
commit 21c6e571d8
108 changed files with 121110 additions and 1144 deletions

View File

@@ -1,5 +1,6 @@
language: go
sudo: false
go:
- 1.8
- 1.9
script: bash <(curl -sL https://gist.github.com/emersion/49d4dda535497002639626bd9e16480c/raw/codecov-go.sh)
after_script: bash <(curl -s https://codecov.io/bash)

View File

@@ -6,7 +6,6 @@
[![Go Report
Card](https://goreportcard.com/badge/github.com/emersion/go-imap)](https://goreportcard.com/report/github.com/emersion/go-imap)
[![Unstable](https://img.shields.io/badge/stability-unstable-yellow.svg)](https://github.com/emersion/stability-badges#unstable)
[![Gitter chat](https://badges.gitter.im/goimap/Lobby.svg)](https://gitter.im/goimap/Lobby)
An [IMAP4rev1](https://tools.ietf.org/html/rfc3501) library written in Go. It
can be used to build a client and/or a server.
@@ -96,7 +95,7 @@ func main() {
messages := make(chan *imap.Message, 10)
done = make(chan error, 1)
go func() {
done <- c.Fetch(seqset, []string{imap.EnvelopeMsgAttr}, messages)
done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages)
}()
log.Println("Last 4 messages:")

View File

@@ -0,0 +1,2 @@
// Package backendutil provides utility functions to implement IMAP backends.
package backendutil

View File

@@ -0,0 +1,68 @@
package backendutil
import (
"bytes"
"errors"
"io"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
)
var errNoSuchPart = errors.New("backendutil: no such message body part")
// FetchBodySection extracts a body section from a message.
func FetchBodySection(e *message.Entity, section *imap.BodySectionName) (imap.Literal, error) {
// First, find the requested part using the provided path
for i := len(section.Path) - 1; i >= 0; i-- {
n := section.Path[i]
mr := e.MultipartReader()
if mr == nil {
return nil, errNoSuchPart
}
for j := 1; j <= n; j++ {
p, err := mr.NextPart()
if err == io.EOF {
return nil, errNoSuchPart
} else if err != nil {
return nil, err
}
if j == n {
e = p
break
}
}
}
// Then, write the requested data to a buffer
b := new(bytes.Buffer)
// Write the header
mw, err := message.CreateWriter(b, e.Header)
if err != nil {
return nil, err
}
defer mw.Close()
// If the header hasn't been requested, discard it
if section.Specifier == imap.TextSpecifier {
b.Reset()
}
// Write the body, if requested
switch section.Specifier {
case imap.EntireSpecifier, imap.TextSpecifier:
if _, err := io.Copy(mw, e.Body); err != nil {
return nil, err
}
}
var l imap.Literal = b
if section.Partial != nil {
l = bytes.NewReader(section.ExtractPartial(b.Bytes()))
}
return l, nil
}

View File

@@ -0,0 +1,60 @@
package backendutil
import (
"io"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
)
// FetchBodyStructure computes a message's body structure from its content.
func FetchBodyStructure(e *message.Entity, extended bool) (*imap.BodyStructure, error) {
bs := new(imap.BodyStructure)
mediaType, mediaParams, _ := e.Header.ContentType()
typeParts := strings.SplitN(mediaType, "/", 2)
bs.MIMEType = typeParts[0]
if len(typeParts) == 2 {
bs.MIMESubType = typeParts[1]
}
bs.Params = mediaParams
bs.Id = e.Header.Get("Content-Id")
bs.Description = e.Header.Get("Content-Description")
bs.Encoding = e.Header.Get("Content-Encoding")
// TODO: bs.Size
if mr := e.MultipartReader(); mr != nil {
var parts []*imap.BodyStructure
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
pbs, err := FetchBodyStructure(p, extended)
if err != nil {
return nil, err
}
parts = append(parts, pbs)
}
bs.Parts = parts
}
// TODO: bs.Envelope, bs.BodyStructure
// TODO: bs.Lines
if extended {
bs.Extended = true
bs.Disposition, bs.DispositionParams, _ = e.Header.ContentDisposition()
// TODO: bs.Language, bs.Location
// TODO: bs.MD5
}
return bs, nil
}

View File

@@ -0,0 +1,50 @@
package backendutil
import (
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
)
func headerAddressList(h mail.Header, key string) ([]*imap.Address, error) {
addrs, err := h.AddressList(key)
list := make([]*imap.Address, len(addrs))
for i, a := range addrs {
parts := strings.SplitN(a.Address, "@", 2)
mailbox := parts[0]
var hostname string
if len(parts) == 2 {
hostname = parts[1]
}
list[i] = &imap.Address{
PersonalName: a.Name,
MailboxName: mailbox,
HostName: hostname,
}
}
return list, err
}
// FetchEnvelope returns a message's envelope from its header.
func FetchEnvelope(h message.Header) (*imap.Envelope, error) {
mh := mail.Header{h}
env := new(imap.Envelope)
env.Date, _ = mh.Date()
env.Subject, _ = mh.Subject()
env.From, _ = headerAddressList(mh, "From")
env.Sender, _ = headerAddressList(mh, "Sender")
env.ReplyTo, _ = headerAddressList(mh, "Reply-To")
env.To, _ = headerAddressList(mh, "To")
env.Cc, _ = headerAddressList(mh, "Cc")
env.Bcc, _ = headerAddressList(mh, "Bcc")
env.InReplyTo = mh.Get("In-Reply-To")
env.MessageId = mh.Get("Message-Id")
return env, nil
}

View File

@@ -0,0 +1,40 @@
package backendutil
import (
"github.com/emersion/go-imap"
)
// UpdateFlags executes a flag operation on the flag set current.
func UpdateFlags(current []string, op imap.FlagsOp, flags []string) []string {
switch op {
case imap.SetFlags:
// TODO: keep \Recent if it is present
return flags
case imap.AddFlags:
// Check for duplicates
for _, flag := range current {
for i, addFlag := range flags {
if addFlag == flag {
flags = append(flags[:i], flags[i+1:]...)
break
}
}
}
return append(current, flags...)
case imap.RemoveFlags:
// Iterate through flags from the last one to the first one, to be able to
// delete some of them.
for i := len(current) - 1; i >= 0; i-- {
flag := current[i]
for _, removeFlag := range flags {
if removeFlag == flag {
current = append(current[:i], current[i+1:]...)
break
}
}
}
return current
}
return current
}

View File

@@ -0,0 +1,225 @@
package backendutil
import (
"bytes"
"fmt"
"io"
"strings"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
)
func matchString(s, substr string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}
func bufferBody(e *message.Entity) (*bytes.Buffer, error) {
b := new(bytes.Buffer)
if _, err := io.Copy(b, e.Body); err != nil {
return nil, err
}
e.Body = b
return b, nil
}
func matchBody(e *message.Entity, substr string) (bool, error) {
if s, ok := e.Body.(fmt.Stringer); ok {
return matchString(s.String(), substr), nil
}
b, err := bufferBody(e)
if err != nil {
return false, err
}
return matchString(b.String(), substr), nil
}
type lengther interface {
Len() int
}
func bodyLen(e *message.Entity) (int, error) {
if l, ok := e.Body.(lengther); ok {
return l.Len(), nil
}
b, err := bufferBody(e)
if err != nil {
return 0, err
}
return b.Len(), nil
}
// Match returns true if a message matches the provided criteria. Sequence
// number, UID, flag and internal date contrainsts are not checked.
func Match(e *message.Entity, c *imap.SearchCriteria) (bool, error) {
// TODO: support encoded header fields for Bcc, Cc, From, To
// TODO: add header size for Larger and Smaller
h := mail.Header{e.Header}
if !c.SentBefore.IsZero() || !c.SentSince.IsZero() {
t, err := h.Date()
if err != nil {
return false, err
}
t = t.Round(24 * time.Hour)
if !c.SentBefore.IsZero() && !t.Before(c.SentBefore) {
return false, nil
}
if !c.SentSince.IsZero() && !t.After(c.SentSince) {
return false, nil
}
}
for key, wantValues := range c.Header {
values, ok := e.Header[key]
for _, wantValue := range wantValues {
if wantValue == "" && !ok {
return false, nil
}
if wantValue != "" {
ok := false
for _, v := range values {
if matchString(v, wantValue) {
ok = true
break
}
}
if !ok {
return false, nil
}
}
}
}
for _, body := range c.Body {
if ok, err := matchBody(e, body); err != nil || !ok {
return false, err
}
}
for _, text := range c.Text {
// TODO: also match header fields
if ok, err := matchBody(e, text); err != nil || !ok {
return false, err
}
}
if c.Larger > 0 || c.Smaller > 0 {
n, err := bodyLen(e)
if err != nil {
return false, err
}
if c.Larger > 0 && uint32(n) < c.Larger {
return false, nil
}
if c.Smaller > 0 && uint32(n) > c.Smaller {
return false, nil
}
}
for _, not := range c.Not {
ok, err := Match(e, not)
if err != nil || ok {
return false, err
}
}
for _, or := range c.Or {
ok1, err := Match(e, or[0])
if err != nil {
return ok1, err
}
ok2, err := Match(e, or[1])
if err != nil || (!ok1 && !ok2) {
return false, err
}
}
return true, nil
}
func matchFlags(flags map[string]bool, c *imap.SearchCriteria) bool {
for _, f := range c.WithFlags {
if !flags[f] {
return false
}
}
for _, f := range c.WithoutFlags {
if flags[f] {
return false
}
}
for _, not := range c.Not {
if matchFlags(flags, not) {
return false
}
}
for _, or := range c.Or {
if !matchFlags(flags, or[0]) && !matchFlags(flags, or[1]) {
return false
}
}
return true
}
// MatchFlags returns true if a flag list matches the provided criteria.
func MatchFlags(flags []string, c *imap.SearchCriteria) bool {
flagsMap := make(map[string]bool)
for _, f := range flags {
flagsMap[f] = true
}
return matchFlags(flagsMap, c)
}
// MatchSeqNumAndUid returns true if a sequence number and a UID matches the
// provided criteria.
func MatchSeqNumAndUid(seqNum uint32, uid uint32, c *imap.SearchCriteria) bool {
if c.SeqNum != nil && !c.SeqNum.Contains(seqNum) {
return false
}
if c.Uid != nil && !c.Uid.Contains(uid) {
return false
}
for _, not := range c.Not {
if MatchSeqNumAndUid(seqNum, uid, not) {
return false
}
}
for _, or := range c.Or {
if !MatchSeqNumAndUid(seqNum, uid, or[0]) && !MatchSeqNumAndUid(seqNum, uid, or[1]) {
return false
}
}
return true
}
// MatchDate returns true if a date matches the provided criteria.
func MatchDate(date time.Time, c *imap.SearchCriteria) bool {
date = date.Round(24 * time.Hour)
if !c.Since.IsZero() && !date.After(c.Since) {
return false
}
if !c.Before.IsZero() && !date.Before(c.Before) {
return false
}
for _, not := range c.Not {
if MatchDate(date, not) {
return false
}
}
for _, or := range c.Or {
if !MatchDate(date, or[0]) && !MatchDate(date, or[1]) {
return false
}
}
return true
}

View File

@@ -15,11 +15,11 @@ type Mailbox interface {
// Info returns this mailbox info.
Info() (*imap.MailboxInfo, error)
// Status returns this mailbox status. The fields Name, Flags and
// PermanentFlags in the returned MailboxStatus must be always populated. This
// function does not affect the state of any messages in the mailbox. See RFC
// 3501 section 6.3.10 for a list of items that can be requested.
Status(items []string) (*imap.MailboxStatus, error)
// Status returns this mailbox status. The fields Name, Flags, PermanentFlags
// and UnseenSeqNum in the returned MailboxStatus must be always populated.
// This function does not affect the state of any messages in the mailbox. See
// RFC 3501 section 6.3.10 for a list of items that can be requested.
Status(items []imap.StatusItem) (*imap.MailboxStatus, error)
// SetSubscribed adds or removes the mailbox to the server's set of "active"
// or "subscribed" mailboxes.
@@ -38,7 +38,7 @@ type Mailbox interface {
// 3501 section 6.4.5 for a list of items that can be requested.
//
// Messages must be sent to ch. When the function returns, ch must be closed.
ListMessages(uid bool, seqset *imap.SeqSet, items []string, ch chan<- *imap.Message) error
ListMessages(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error
// SearchMessages searches messages. The returned list must contain UIDs if
// uid is set to true, or sequence numbers otherwise.

View File

@@ -6,35 +6,47 @@ import (
// Update contains user and mailbox information about an unilateral backend
// update.
type Update struct {
type Update interface {
// The user targeted by this update. If empty, all connected users will
// be notified.
Username string
Username() string
// The mailbox targeted by this update. If empty, the update targets all
// mailboxes.
Mailbox string
Mailbox() string
// Done returns a channel that is closed when the update has been broadcast to
// all clients.
Done() chan struct{}
}
// A channel that will be closed once the update has been processed.
// NewUpdate creates a new update.
func NewUpdate(username, mailbox string) Update {
return &update{
username: username,
mailbox: mailbox,
}
}
type update struct {
username string
mailbox string
done chan struct{}
}
// Done returns a channel that is closed when the update has been broadcast to
// all clients.
func (u *Update) Done() <-chan struct{} {
func (u *update) Username() string {
return u.username
}
func (u *update) Mailbox() string {
return u.mailbox
}
func (u *update) Done() chan struct{} {
if u.done == nil {
u.done = make(chan struct{})
}
return u.done
}
// DoneUpdate marks an update as done.
// TODO: remove this function
func DoneUpdate(u *Update) {
if u.done != nil {
close(u.done)
}
}
// StatusUpdate is a status update. See RFC 3501 section 7.1 for a list of
// status responses.
type StatusUpdate struct {
@@ -60,50 +72,21 @@ type ExpungeUpdate struct {
SeqNum uint32
}
// Updater is a Backend that implements Updater is able to send unilateral
// backend updates. Backends not implementing this interface don't correctly
// send unilateral updates, for instance if a user logs in from two connections
// and deletes a message from one of them, the over is not aware that such a
// mesage has been deleted. More importantly, backends implementing Updater can
// notify the user for external updates such as new message notifications.
type Updater interface {
// BackendUpdater is a Backend that implements Updater is able to send
// unilateral backend updates. Backends not implementing this interface don't
// correctly send unilateral updates, for instance if a user logs in from two
// connections and deletes a message from one of them, the over is not aware
// that such a mesage has been deleted. More importantly, backends implementing
// Updater can notify the user for external updates such as new message
// notifications.
type BackendUpdater interface {
// Updates returns a set of channels where updates are sent to.
Updates() <-chan interface{}
Updates() <-chan Update
}
// UpdaterMailbox is a Mailbox that implements UpdaterMailbox is able to poll
// updates for new messages or message status updates during a period of
// inactivity.
type UpdaterMailbox interface {
// MailboxPoller is a Mailbox that is able to poll updates for new messages or
// message status updates during a period of inactivity.
type MailboxPoller interface {
// Poll requests mailbox updates.
Poll() error
}
// WaitUpdates returns a channel that's closed when all provided updates have
// been dispatched to all clients. It panics if one of the provided value is
// not an update.
func WaitUpdates(updates ...interface{}) <-chan struct{} {
done := make(chan struct{})
var chs []<-chan struct{}
for _, u := range updates {
uu, ok := u.(interface {
Done() <-chan struct{}
})
if !ok {
panic("imap: cannot wait for update: provided value is not a valid update")
}
chs = append(chs, uu.Done())
}
go func() {
// Wait for all updates to be sent
for _, ch := range chs {
<-ch
}
close(done)
}()
return done
}

View File

@@ -5,38 +5,6 @@ import (
"strings"
)
// IMAP4rev1 commands.
const (
Capability string = "CAPABILITY"
Noop = "NOOP"
Logout = "LOGOUT"
StartTLS = "STARTTLS"
Authenticate = "AUTHENTICATE"
Login = "LOGIN"
Select = "SELECT"
Examine = "EXAMINE"
Create = "CREATE"
Delete = "DELETE"
Rename = "RENAME"
Subscribe = "SUBSCRIBE"
Unsubscribe = "UNSUBSCRIBE"
List = "LIST"
Lsub = "LSUB"
Status = "STATUS"
Append = "APPEND"
Check = "CHECK"
Close = "CLOSE"
Expunge = "EXPUNGE"
Search = "SEARCH"
Fetch = "FETCH"
Store = "STORE"
Copy = "COPY"
Uid = "UID"
)
// A value that can be converted to a command.
type Commander interface {
Command() *Command

View File

@@ -19,13 +19,13 @@ type Append struct {
func (cmd *Append) Command() *imap.Command {
var args []interface{}
mailbox, _ := utf7.Encoder.String(cmd.Mailbox)
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
args = append(args, mailbox)
if cmd.Flags != nil {
flags := make([]interface{}, len(cmd.Flags))
for i, flag := range cmd.Flags {
flags[i] = flag
flags[i] = imap.Atom(flag)
}
args = append(args, flags)
}
@@ -37,7 +37,7 @@ func (cmd *Append) Command() *imap.Command {
args = append(args, cmd.Message)
return &imap.Command{
Name: imap.Append,
Name: "APPEND",
Arguments: args,
}
}
@@ -48,9 +48,9 @@ func (cmd *Append) Parse(fields []interface{}) (err error) {
}
// Parse mailbox name
if mailbox, ok := fields[0].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if mailbox, err = utf7.Decoder.String(mailbox); err != nil {
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)
@@ -81,11 +81,9 @@ func (cmd *Append) Parse(fields []interface{}) (err error) {
// Parse date
if len(fields) > 0 {
date, ok := fields[0].(string)
if !ok {
if date, ok := fields[0].(string); !ok {
return errors.New("Date must be a string")
}
if cmd.Date, err = time.Parse(imap.DateTimeLayout, date); err != nil {
} else if cmd.Date, err = time.Parse(imap.DateTimeLayout, date); err != nil {
return err
}
}

View File

@@ -27,7 +27,7 @@ type Authenticate struct {
func (cmd *Authenticate) Command() *imap.Command {
return &imap.Command{
Name: imap.Authenticate,
Name: "AUTHENTICATE",
Arguments: []interface{}{cmd.Mechanism},
}
}
@@ -62,7 +62,7 @@ func (cmd *Authenticate) Handle(mechanisms map[string]sasl.Server, conn Authenti
}
encoded := base64.StdEncoding.EncodeToString(challenge)
cont := &imap.ContinuationResp{Info: encoded}
cont := &imap.ContinuationReq{Info: encoded}
if err := conn.WriteResp(cont); err != nil {
return err
}

View File

@@ -9,7 +9,7 @@ type Capability struct{}
func (c *Capability) Command() *imap.Command {
return &imap.Command{
Name: imap.Capability,
Name: "CAPABILITY",
}
}

View File

@@ -9,7 +9,7 @@ type Check struct{}
func (cmd *Check) Command() *imap.Command {
return &imap.Command{
Name: imap.Check,
Name: "CHECK",
}
}

View File

@@ -9,7 +9,7 @@ type Close struct{}
func (cmd *Close) Command() *imap.Command {
return &imap.Command{
Name: imap.Close,
Name: "CLOSE",
}
}

View File

@@ -14,10 +14,10 @@ type Copy struct {
}
func (cmd *Copy) Command() *imap.Command {
mailbox, _ := utf7.Encoder.String(cmd.Mailbox)
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: imap.Copy,
Name: "COPY",
Arguments: []interface{}{cmd.SeqSet, mailbox},
}
}
@@ -29,15 +29,15 @@ func (cmd *Copy) Parse(fields []interface{}) error {
if seqSet, ok := fields[0].(string); !ok {
return errors.New("Invalid sequence set")
} else if seqSet, err := imap.NewSeqSet(seqSet); err != nil {
} else if seqSet, err := imap.ParseSeqSet(seqSet); err != nil {
return err
} else {
cmd.SeqSet = seqSet
}
if mailbox, ok := fields[1].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if mailbox, err := utf7.Decoder.String(mailbox); err != nil {
if mailbox, err := imap.ParseString(fields[1]); err != nil {
return err
} else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)

View File

@@ -13,10 +13,10 @@ type Create struct {
}
func (cmd *Create) Command() *imap.Command {
mailbox, _ := utf7.Encoder.String(cmd.Mailbox)
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: imap.Create,
Name: "CREATE",
Arguments: []interface{}{mailbox},
}
}
@@ -26,9 +26,9 @@ func (cmd *Create) Parse(fields []interface{}) error {
return errors.New("No enough arguments")
}
if mailbox, ok := fields[0].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if mailbox, err := utf7.Decoder.String(mailbox); err != nil {
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)

View File

@@ -13,10 +13,10 @@ type Delete struct {
}
func (cmd *Delete) Command() *imap.Command {
mailbox, _ := utf7.Encoder.String(cmd.Mailbox)
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: imap.Delete,
Name: "DELETE",
Arguments: []interface{}{mailbox},
}
}
@@ -26,9 +26,9 @@ func (cmd *Delete) Parse(fields []interface{}) error {
return errors.New("No enough arguments")
}
if mailbox, ok := fields[0].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if mailbox, err := utf7.Decoder.String(mailbox); err != nil {
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)

View File

@@ -8,7 +8,7 @@ import (
type Expunge struct{}
func (cmd *Expunge) Command() *imap.Command {
return &imap.Command{Name: imap.Expunge}
return &imap.Command{Name: "EXPUNGE"}
}
func (cmd *Expunge) Parse(fields []interface{}) error {

View File

@@ -10,21 +10,21 @@ import (
// Fetch is a FETCH command, as defined in RFC 3501 section 6.4.5.
type Fetch struct {
SeqSet *imap.SeqSet
Items []string
Items []imap.FetchItem
}
func (cmd *Fetch) Command() *imap.Command {
items := make([]interface{}, len(cmd.Items))
for i, item := range cmd.Items {
if section, err := imap.NewBodySectionName(item); err == nil {
if section, err := imap.ParseBodySectionName(item); err == nil {
items[i] = section
} else {
items[i] = item
items[i] = string(item)
}
}
return &imap.Command{
Name: imap.Fetch,
Name: "FETCH",
Arguments: []interface{}{cmd.SeqSet, items},
}
}
@@ -34,33 +34,22 @@ func (cmd *Fetch) Parse(fields []interface{}) error {
return errors.New("No enough arguments")
}
seqset, ok := fields[0].(string)
if !ok {
return errors.New("Sequence set must be a string")
}
var err error
if cmd.SeqSet, err = imap.NewSeqSet(seqset); err != nil {
if seqset, ok := fields[0].(string); !ok {
return errors.New("Sequence set must be an atom")
} else if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil {
return err
}
switch items := fields[1].(type) {
case string: // A macro or a single item
switch strings.ToUpper(items) {
case "ALL":
cmd.Items = []string{"FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE"}
case "FAST":
cmd.Items = []string{"FLAGS", "INTERNALDATE", "RFC822.SIZE"}
case "FULL":
cmd.Items = []string{"FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE", "BODY"}
default:
cmd.Items = []string{strings.ToUpper(items)}
}
cmd.Items = imap.FetchItem(strings.ToUpper(items)).Expand()
case []interface{}: // A list of items
cmd.Items = make([]string, len(items))
for i, v := range items {
item, _ := v.(string)
cmd.Items[i] = strings.ToUpper(item)
cmd.Items = make([]imap.FetchItem, 0, len(items))
for _, v := range items {
itemStr, _ := v.(string)
item := imap.FetchItem(strings.ToUpper(itemStr))
cmd.Items = append(cmd.Items, item.Expand()...)
}
default:
return errors.New("Items must be either a string or a list")

View File

@@ -17,13 +17,14 @@ type List struct {
}
func (cmd *List) Command() *imap.Command {
name := imap.List
name := "LIST"
if cmd.Subscribed {
name = imap.Lsub
name = "LSUB"
}
ref, _ := utf7.Encoder.String(cmd.Reference)
mailbox, _ := utf7.Encoder.String(cmd.Mailbox)
enc := utf7.Encoding.NewEncoder()
ref, _ := enc.String(cmd.Reference)
mailbox, _ := enc.String(cmd.Mailbox)
return &imap.Command{
Name: name,
@@ -36,18 +37,20 @@ func (cmd *List) Parse(fields []interface{}) error {
return errors.New("No enough arguments")
}
if mailbox, ok := fields[0].(string); !ok {
return errors.New("Reference name must be a string")
} else if mailbox, err := utf7.Decoder.String(mailbox); err != nil {
dec := utf7.Encoding.NewDecoder()
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err := dec.String(mailbox); err != nil {
return err
} else {
// TODO: canonical mailbox path
cmd.Reference = imap.CanonicalMailboxName(mailbox)
}
if mailbox, ok := fields[1].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if mailbox, err := utf7.Decoder.String(mailbox); err != nil {
if mailbox, err := imap.ParseString(fields[1]); err != nil {
return err
} else if mailbox, err := dec.String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)

View File

@@ -14,7 +14,7 @@ type Login struct {
func (cmd *Login) Command() *imap.Command {
return &imap.Command{
Name: imap.Login,
Name: "LOGIN",
Arguments: []interface{}{cmd.Username, cmd.Password},
}
}
@@ -24,12 +24,12 @@ func (cmd *Login) Parse(fields []interface{}) error {
return errors.New("Not enough arguments")
}
var ok bool
if cmd.Username, ok = fields[0].(string); !ok {
return errors.New("Username is not a string")
var err error
if cmd.Username, err = imap.ParseString(fields[0]); err != nil {
return err
}
if cmd.Password, ok = fields[1].(string); !ok {
return errors.New("Password is not a string")
if cmd.Password, err = imap.ParseString(fields[1]); err != nil {
return err
}
return nil

View File

@@ -9,7 +9,7 @@ type Logout struct{}
func (c *Logout) Command() *imap.Command {
return &imap.Command{
Name: imap.Logout,
Name: "LOGOUT",
}
}

View File

@@ -9,7 +9,7 @@ type Noop struct{}
func (c *Noop) Command() *imap.Command {
return &imap.Command{
Name: imap.Noop,
Name: "NOOP",
}
}

View File

@@ -14,11 +14,12 @@ type Rename struct {
}
func (cmd *Rename) Command() *imap.Command {
existingName, _ := utf7.Encoder.String(cmd.Existing)
newName, _ := utf7.Encoder.String(cmd.New)
enc := utf7.Encoding.NewEncoder()
existingName, _ := enc.String(cmd.Existing)
newName, _ := enc.String(cmd.New)
return &imap.Command{
Name: imap.Rename,
Name: "RENAME",
Arguments: []interface{}{existingName, newName},
}
}
@@ -28,17 +29,19 @@ func (cmd *Rename) Parse(fields []interface{}) error {
return errors.New("No enough arguments")
}
if existingName, ok := fields[0].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if existingName, err := utf7.Decoder.String(existingName); err != nil {
dec := utf7.Encoding.NewDecoder()
if existingName, err := imap.ParseString(fields[0]); err != nil {
return err
} else if existingName, err := dec.String(existingName); err != nil {
return err
} else {
cmd.Existing = imap.CanonicalMailboxName(existingName)
}
if newName, ok := fields[1].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if newName, err := utf7.Decoder.String(newName); err != nil {
if newName, err := imap.ParseString(fields[1]); err != nil {
return err
} else if newName, err := dec.String(newName); err != nil {
return err
} else {
cmd.New = imap.CanonicalMailboxName(newName)

View File

@@ -22,7 +22,7 @@ func (cmd *Search) Command() *imap.Command {
args = append(args, cmd.Criteria.Format()...)
return &imap.Command{
Name: imap.Search,
Name: "SEARCH",
Arguments: args,
}
}

View File

@@ -15,12 +15,12 @@ type Select struct {
}
func (cmd *Select) Command() *imap.Command {
name := imap.Select
name := "SELECT"
if cmd.ReadOnly {
name = imap.Examine
name = "EXAMINE"
}
mailbox, _ := utf7.Encoder.String(cmd.Mailbox)
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: name,
@@ -33,9 +33,9 @@ func (cmd *Select) Parse(fields []interface{}) error {
return errors.New("No enough arguments")
}
if mailbox, ok := fields[0].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if mailbox, err := utf7.Decoder.String(mailbox); err != nil {
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)

View File

@@ -9,7 +9,7 @@ type StartTLS struct{}
func (cmd *StartTLS) Command() *imap.Command {
return &imap.Command{
Name: imap.StartTLS,
Name: "STARTTLS",
}
}

View File

@@ -11,19 +11,19 @@ import (
// Status is a STATUS command, as defined in RFC 3501 section 6.3.10.
type Status struct {
Mailbox string
Items []string
Items []imap.StatusItem
}
func (cmd *Status) Command() *imap.Command {
mailbox, _ := utf7.Encoder.String(cmd.Mailbox)
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
items := make([]interface{}, len(cmd.Items))
for i, f := range cmd.Items {
items[i] = f
for i, item := range cmd.Items {
items[i] = string(item)
}
return &imap.Command{
Name: imap.Status,
Name: "STATUS",
Arguments: []interface{}{mailbox, items},
}
}
@@ -33,21 +33,24 @@ func (cmd *Status) Parse(fields []interface{}) error {
return errors.New("No enough arguments")
}
if mailbox, ok := fields[0].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if mailbox, err := utf7.Decoder.String(mailbox); err != nil {
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)
}
if items, ok := fields[1].([]interface{}); !ok {
return errors.New("Items must be a list")
} else {
cmd.Items = make([]string, len(items))
for i, v := range items {
item, _ := v.(string)
cmd.Items[i] = strings.ToUpper(item)
items, ok := fields[1].([]interface{})
if !ok {
return errors.New("STATUS command parameter is not a list")
}
cmd.Items = make([]imap.StatusItem, len(items))
for i, f := range items {
if s, ok := f.(string); !ok {
return errors.New("Got a non-string field in a STATUS command parameter")
} else {
cmd.Items[i] = imap.StatusItem(strings.ToUpper(s))
}
}

View File

@@ -10,18 +10,18 @@ import (
// Store is a STORE command, as defined in RFC 3501 section 6.4.6.
type Store struct {
SeqSet *imap.SeqSet
Item string
Item imap.StoreItem
Value interface{}
}
func (cmd *Store) Command() *imap.Command {
return &imap.Command{
Name: imap.Store,
Arguments: []interface{}{cmd.SeqSet, cmd.Item, cmd.Value},
Name: "STORE",
Arguments: []interface{}{cmd.SeqSet, string(cmd.Item), cmd.Value},
}
}
func (cmd *Store) Parse(fields []interface{}) (err error) {
func (cmd *Store) Parse(fields []interface{}) error {
if len(fields) < 3 {
return errors.New("No enough arguments")
}
@@ -30,16 +30,18 @@ func (cmd *Store) Parse(fields []interface{}) (err error) {
if !ok {
return errors.New("Invalid sequence set")
}
if cmd.SeqSet, err = imap.NewSeqSet(seqset); err != nil {
var err error
if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil {
return err
}
if cmd.Item, ok = fields[1].(string); !ok {
if item, ok := fields[1].(string); !ok {
return errors.New("Item name must be a string")
} else {
cmd.Item = imap.StoreItem(strings.ToUpper(item))
}
cmd.Item = strings.ToUpper(cmd.Item)
// TODO: could be fields[2:] according to RFC 3501 page 91 "store-att-flags"
cmd.Value = fields[2]
return
return nil
}

View File

@@ -13,29 +13,25 @@ type Subscribe struct {
}
func (cmd *Subscribe) Command() *imap.Command {
mailbox, _ := utf7.Encoder.String(cmd.Mailbox)
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: imap.Subscribe,
Name: "SUBSCRIBE",
Arguments: []interface{}{mailbox},
}
}
func (cmd *Subscribe) Parse(fields []interface{}) (err error) {
func (cmd *Subscribe) Parse(fields []interface{}) error {
if len(fields) < 0 {
return errors.New("No enogh arguments")
return errors.New("No enough arguments")
}
mailbox, ok := fields[0].(string)
if !ok {
return errors.New("Mailbox name must be a string")
}
if cmd.Mailbox, err = utf7.Decoder.String(mailbox); err != nil {
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
}
return
return nil
}
// An UNSUBSCRIBE command.
@@ -45,27 +41,23 @@ type Unsubscribe struct {
}
func (cmd *Unsubscribe) Command() *imap.Command {
mailbox, _ := utf7.Encoder.String(cmd.Mailbox)
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: imap.Unsubscribe,
Name: "UNSUBSCRIBE",
Arguments: []interface{}{mailbox},
}
}
func (cmd *Unsubscribe) Parse(fields []interface{}) (err error) {
func (cmd *Unsubscribe) Parse(fields []interface{}) error {
if len(fields) < 0 {
return errors.New("No enogh arguments")
}
mailbox, ok := fields[0].(string)
if !ok {
return errors.New("Mailbox name must be a string")
}
if cmd.Mailbox, err = utf7.Decoder.String(mailbox); err != nil {
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
}
return
return nil
}

View File

@@ -20,7 +20,7 @@ func (cmd *Uid) Command() *imap.Command {
args = append(args, inner.Arguments...)
return &imap.Command{
Name: imap.Uid,
Name: "UID",
Arguments: args,
}
}

View File

@@ -154,10 +154,7 @@ func (c *Conn) Write(b []byte) (n int, err error) {
// Flush writes any buffered data to the underlying connection.
func (c *Conn) Flush() error {
if err := c.Writer.Flush(); err != nil {
return err
}
return nil
return c.Writer.Flush()
}
// Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted

View File

@@ -1,121 +0,0 @@
package imap
import (
"sync"
)
// A response that can be either accepted or rejected by a handler.
type RespHandle struct {
Resp interface{}
Accepts chan bool
}
// Accept this response. This means that the handler will process it.
func (h *RespHandle) Accept() {
h.Accepts <- true
}
// Reject this response. The handler cannot process it.
func (h *RespHandle) Reject() {
h.Accepts <- false
}
// Accept this response if it has the specified name. If not, reject it.
func (h *RespHandle) AcceptNamedResp(name string) (fields []interface{}, accepted bool) {
res, ok := h.Resp.(*Resp)
if !ok || len(res.Fields) == 0 {
h.Reject()
return
}
n, ok := res.Fields[0].(string)
if !ok || n != name {
h.Reject()
return
}
h.Accept()
fields = res.Fields[1:]
accepted = true
return
}
// Delivers responses to handlers.
type RespHandler chan *RespHandle
// Handles responses from a handler.
type RespHandlerFrom interface {
HandleFrom(hdlr RespHandler) error
}
// A RespHandlerFrom that forwards responses to multiple RespHandler.
type MultiRespHandler struct {
handlers []RespHandler
locker sync.Locker
}
func NewMultiRespHandler() *MultiRespHandler {
return &MultiRespHandler{
locker: &sync.Mutex{},
}
}
func (mh *MultiRespHandler) HandleFrom(ch RespHandler) error {
for rh := range ch {
mh.locker.Lock()
accepted := false
for i := len(mh.handlers) - 1; i >= 0; i-- {
hdlr := mh.handlers[i]
rh := &RespHandle{
Resp: rh.Resp,
Accepts: make(chan bool),
}
hdlr <- rh
if accepted = <-rh.Accepts; accepted {
break
}
}
mh.locker.Unlock()
if accepted {
rh.Accept()
} else {
rh.Reject()
}
}
mh.locker.Lock()
for _, hdlr := range mh.handlers {
close(hdlr)
}
mh.handlers = nil
mh.locker.Unlock()
return nil
}
func (mh *MultiRespHandler) Add(hdlr RespHandler) {
if hdlr == nil {
return
}
mh.locker.Lock()
mh.handlers = append(mh.handlers, hdlr)
mh.locker.Unlock()
}
func (mh *MultiRespHandler) Del(hdlr RespHandler) {
mh.locker.Lock()
for i, h := range mh.handlers {
if h == hdlr {
close(hdlr)
mh.handlers = append(mh.handlers[:i], mh.handlers[i+1:]...)
}
}
mh.locker.Unlock()
}

View File

@@ -2,9 +2,60 @@
package imap
import (
"errors"
"io"
"strings"
)
// A StatusItem is a mailbox status data item that can be retrieved with a
// STATUS command. See RFC 3501 section 6.3.10.
type StatusItem string
const (
StatusMessages StatusItem = "MESSAGES"
StatusRecent = "RECENT"
StatusUidNext = "UIDNEXT"
StatusUidValidity = "UIDVALIDITY"
StatusUnseen = "UNSEEN"
)
// A FetchItem is a message data item that can be fetched.
type FetchItem string
// List of items that can be fetched.
const (
// Macros
FetchAll FetchItem = "ALL"
FetchFast = "FAST"
FetchFull = "FULL"
// Items
FetchBody = "BODY"
FetchBodyStructure = "BODYSTRUCTURE"
FetchEnvelope = "ENVELOPE"
FetchFlags = "FLAGS"
FetchInternalDate = "INTERNALDATE"
FetchRFC822 = "RFC822"
FetchRFC822Header = "RFC822.HEADER"
FetchRFC822Size = "RFC822.SIZE"
FetchRFC822Text = "RFC822.TEXT"
FetchUid = "UID"
)
// Expand expands the item if it's a macro.
func (item FetchItem) Expand() []FetchItem {
switch item {
case FetchAll:
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope}
case FetchFast:
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size}
case FetchFull:
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope, FetchBody}
default:
return []FetchItem{item}
}
}
// FlagsOp is an operation that will be applied on message flags.
type FlagsOp string
@@ -17,9 +68,36 @@ const (
RemoveFlags = "-FLAGS"
)
// SilentOp can be appended to a FlagsOp to prevent the operation from
// silentOp can be appended to a FlagsOp to prevent the operation from
// triggering unilateral message updates.
const SilentOp = ".SILENT"
const silentOp = ".SILENT"
// A StoreItem is a message data item that can be updated.
type StoreItem string
// FormatFlagsOp returns the StoreItem that executes the flags operation op.
func FormatFlagsOp(op FlagsOp, silent bool) StoreItem {
s := string(op)
if silent {
s += silentOp
}
return StoreItem(s)
}
// ParseFlagsOp parses a flags operation from StoreItem.
func ParseFlagsOp(item StoreItem) (op FlagsOp, silent bool, err error) {
itemStr := string(item)
silent = strings.HasSuffix(itemStr, silentOp)
if silent {
itemStr = strings.TrimSuffix(itemStr, silentOp)
}
op = FlagsOp(itemStr)
if op != SetFlags && op != AddFlags && op != RemoveFlags {
err = errors.New("Unsupported STORE operation")
}
return
}
// CharsetReader, if non-nil, defines a function to generate charset-conversion
// readers, converting from the provided charset into UTF-8. Charsets are always

View File

@@ -2,6 +2,7 @@ package imap
import (
"errors"
"fmt"
"strings"
"sync"
@@ -53,22 +54,36 @@ func (info *MailboxInfo) Parse(fields []interface{}) error {
return errors.New("Mailbox info needs at least 3 fields")
}
info.Attributes, _ = ParseStringList(fields[0])
var err error
if info.Attributes, err = ParseStringList(fields[0]); err != nil {
return err
}
info.Delimiter, _ = fields[1].(string)
var ok bool
if info.Delimiter, ok = fields[1].(string); !ok {
return errors.New("Mailbox delimiter must be a string")
}
name, _ := fields[2].(string)
info.Name, _ = utf7.Decoder.String(name)
info.Name = CanonicalMailboxName(info.Name)
if name, err := ParseString(fields[2]); err != nil {
return err
} else if name, err := utf7.Encoding.NewDecoder().String(name); err != nil {
return err
} else {
info.Name = CanonicalMailboxName(name)
}
return nil
}
// Format mailbox info to fields.
func (info *MailboxInfo) Format() []interface{} {
name, _ := utf7.Encoder.String(info.Name)
name, _ := utf7.Encoding.NewEncoder().String(info.Name)
attrs := make([]interface{}, len(info.Attributes))
for i, attr := range info.Attributes {
attrs[i] = Atom(attr)
}
// Thunderbird doesn't understand delimiters if not quoted
return []interface{}{FormatStringList(info.Attributes), Quoted(info.Delimiter), name}
return []interface{}{attrs, Quoted(info.Delimiter), name}
}
// TODO: optimize this
@@ -125,19 +140,6 @@ func (info *MailboxInfo) Match(reference, pattern string) bool {
return info.match(name, pattern)
}
// Mailbox status items.
const (
MailboxFlags = "FLAGS"
MailboxPermanentFlags = "PERMANENTFLAGS"
// Defined in RFC 3501 section 6.3.10.
MailboxMessages = "MESSAGES"
MailboxRecent = "RECENT"
MailboxUnseen = "UNSEEN"
MailboxUidNext = "UIDNEXT"
MailboxUidValidity = "UIDVALIDITY"
)
// A mailbox status.
type MailboxStatus struct {
// The mailbox name.
@@ -147,7 +149,7 @@ type MailboxStatus struct {
// The mailbox items that are currently filled in. This map's values
// should not be used directly, they must only be used by libraries
// implementing extensions of the IMAP protocol.
Items map[string]interface{}
Items map[StatusItem]interface{}
// The Items map may be accessed in different goroutines. Protect
// concurrent writes.
@@ -157,6 +159,8 @@ type MailboxStatus struct {
Flags []string
// The mailbox permanent flags.
PermanentFlags []string
// The sequence number of the first unseen message in the mailbox.
UnseenSeqNum uint32
// The number of messages in this mailbox.
Messages uint32
@@ -172,10 +176,10 @@ type MailboxStatus struct {
}
// Create a new mailbox status that will contain the specified items.
func NewMailboxStatus(name string, items []string) *MailboxStatus {
func NewMailboxStatus(name string, items []StatusItem) *MailboxStatus {
status := &MailboxStatus{
Name: name,
Items: make(map[string]interface{}),
Items: make(map[StatusItem]interface{}),
}
for _, k := range items {
@@ -186,30 +190,30 @@ func NewMailboxStatus(name string, items []string) *MailboxStatus {
}
func (status *MailboxStatus) Parse(fields []interface{}) error {
status.Items = make(map[string]interface{})
status.Items = make(map[StatusItem]interface{})
var k string
var k StatusItem
for i, f := range fields {
if i%2 == 0 {
var ok bool
if k, ok = f.(string); !ok {
return errors.New("Key is not a string")
if kstr, ok := f.(string); !ok {
return fmt.Errorf("cannot parse mailbox status: key is not a string, but a %T", f)
} else {
k = StatusItem(strings.ToUpper(kstr))
}
k = strings.ToUpper(k)
} else {
status.Items[k] = nil
var err error
switch k {
case MailboxMessages:
case StatusMessages:
status.Messages, err = ParseNumber(f)
case MailboxRecent:
case StatusRecent:
status.Recent, err = ParseNumber(f)
case MailboxUnseen:
case StatusUnseen:
status.Unseen, err = ParseNumber(f)
case MailboxUidNext:
case StatusUidNext:
status.UidNext, err = ParseNumber(f)
case MailboxUidValidity:
case StatusUidValidity:
status.UidValidity, err = ParseNumber(f)
default:
status.Items[k] = f
@@ -228,19 +232,19 @@ func (status *MailboxStatus) Format() []interface{} {
var fields []interface{}
for k, v := range status.Items {
switch k {
case MailboxMessages:
case StatusMessages:
v = status.Messages
case MailboxRecent:
case StatusRecent:
v = status.Recent
case MailboxUnseen:
case StatusUnseen:
v = status.Unseen
case MailboxUidNext:
case StatusUidNext:
v = status.UidNext
case MailboxUidValidity:
case StatusUidValidity:
v = status.UidValidity
}
fields = append(fields, k, v)
fields = append(fields, string(k), v)
}
return fields
}

View File

@@ -30,30 +30,13 @@ var flags = []string{
RecentFlag,
}
// Message attributes that can be fetched, defined in RFC 3501 section 6.4.5.
// Attributes that fetches the message contents are defined with
// BodySectionName.
const (
// Non-extensible form of BODYSTRUCTURE.
BodyMsgAttr = "BODY"
// MIME body structure of the message.
BodyStructureMsgAttr = "BODYSTRUCTURE"
// The envelope structure of the message.
EnvelopeMsgAttr = "ENVELOPE"
// The flags that are set for the message.
FlagsMsgAttr = "FLAGS"
// The internal date of the message.
InternalDateMsgAttr = "INTERNALDATE"
// The RFC 822 size of the message.
SizeMsgAttr = "RFC822.SIZE"
// The unique identifier for the message.
UidMsgAttr = "UID"
)
// A PartSpecifier specifies which parts of the MIME entity should be returned.
type PartSpecifier string
// Part specifiers described in RFC 3501 page 55.
const (
// Refers to the entire part, including headers.
EntireSpecifier = ""
EntireSpecifier PartSpecifier = ""
// Refers to the header of the part. Must include the final CRLF delimiting
// the header and the body.
HeaderSpecifier = "HEADER"
@@ -61,7 +44,7 @@ const (
TextSpecifier = "TEXT"
// Refers to the MIME Internet Message Body header. Must include the final
// CRLF delimiting the header and the body.
MimeSpecifier = "MIME"
MIMESpecifier = "MIME"
)
// Returns the canonical form of a flag. Flags are case-insensitive.
@@ -83,9 +66,9 @@ func ParseParamList(fields []interface{}) (map[string]string, error) {
var k string
for i, f := range fields {
p, ok := f.(string)
if !ok {
return nil, errors.New("Parameter list contains a non-string")
p, err := ParseString(f)
if err != nil {
return nil, errors.New("Parameter list contains a non-string: " + err.Error())
}
if i%2 == 0 {
@@ -158,7 +141,7 @@ type Message struct {
// The mailbox items that are currently filled in. This map's values
// should not be used directly, they must only be used by libraries
// implementing extensions of the IMAP protocol.
Items map[string]interface{}
Items map[FetchItem]interface{}
// The message envelope.
Envelope *Envelope
@@ -178,14 +161,14 @@ type Message struct {
// The order in which items were requested. This order must be preserved
// because some bad IMAP clients (looking at you, Outlook!) refuse responses
// containing items in a different order.
itemsOrder []string
itemsOrder []FetchItem
}
// Create a new empty message that will contain the specified items.
func NewMessage(seqNum uint32, items []string) *Message {
func NewMessage(seqNum uint32, items []FetchItem) *Message {
msg := &Message{
SeqNum: seqNum,
Items: make(map[string]interface{}),
Items: make(map[FetchItem]interface{}),
Body: make(map[*BodySectionName]Literal),
itemsOrder: items,
}
@@ -199,65 +182,65 @@ func NewMessage(seqNum uint32, items []string) *Message {
// Parse a message from fields.
func (m *Message) Parse(fields []interface{}) error {
m.Items = make(map[string]interface{})
m.Items = make(map[FetchItem]interface{})
m.Body = map[*BodySectionName]Literal{}
m.itemsOrder = nil
var k string
var k FetchItem
for i, f := range fields {
if i%2 == 0 { // It's a key
var ok bool
if k, ok = f.(string); !ok {
return errors.New("Key is not a string")
if kstr, ok := f.(string); !ok {
return fmt.Errorf("cannot parse message: key is not a string, but a %T", f)
} else {
k = FetchItem(strings.ToUpper(kstr))
}
k = strings.ToUpper(k)
} else { // It's a value
m.Items[k] = nil
m.itemsOrder = append(m.itemsOrder, k)
switch k {
case BodyMsgAttr, BodyStructureMsgAttr:
case FetchBody, FetchBodyStructure:
bs, ok := f.([]interface{})
if !ok {
return errors.New("BODYSTRUCTURE is not a list")
return fmt.Errorf("cannot parse message: BODYSTRUCTURE is not a list, but a %T", f)
}
m.BodyStructure = &BodyStructure{Extended: k == BodyStructureMsgAttr}
m.BodyStructure = &BodyStructure{Extended: k == FetchBodyStructure}
if err := m.BodyStructure.Parse(bs); err != nil {
return err
}
case EnvelopeMsgAttr:
case FetchEnvelope:
env, ok := f.([]interface{})
if !ok {
return errors.New("ENVELOPE is not a list")
return fmt.Errorf("cannot parse message: ENVELOPE is not a list, but a %T", f)
}
m.Envelope = &Envelope{}
if err := m.Envelope.Parse(env); err != nil {
return err
}
case FlagsMsgAttr:
case FetchFlags:
flags, ok := f.([]interface{})
if !ok {
return errors.New("FLAGS is not a list")
return fmt.Errorf("cannot parse message: FLAGS is not a list, but a %T", f)
}
m.Flags = make([]string, len(flags))
for i, flag := range flags {
s, _ := flag.(string)
s, _ := ParseString(flag)
m.Flags[i] = CanonicalFlag(s)
}
case InternalDateMsgAttr:
case FetchInternalDate:
date, _ := f.(string)
m.InternalDate, _ = time.Parse(DateTimeLayout, date)
case SizeMsgAttr:
case FetchRFC822Size:
m.Size, _ = ParseNumber(f)
case UidMsgAttr:
case FetchUid:
m.Uid, _ = ParseNumber(f)
default:
// Likely to be a section of the body
// First check that the section name is correct
if section, err := NewBodySectionName(k); err != nil {
if section, err := ParseBodySectionName(k); err != nil {
// Not a section name, maybe an attribute defined in an IMAP extension
m.Items[k] = f
} else {
@@ -270,24 +253,28 @@ func (m *Message) Parse(fields []interface{}) error {
return nil
}
func (m *Message) formatItem(k string) []interface{} {
func (m *Message) formatItem(k FetchItem) []interface{} {
v := m.Items[k]
var kk interface{} = k
var kk interface{} = string(k)
switch strings.ToUpper(k) {
case BodyMsgAttr, BodyStructureMsgAttr:
switch k {
case FetchBody, FetchBodyStructure:
// Extension data is only returned with the BODYSTRUCTURE fetch
m.BodyStructure.Extended = k == BodyStructureMsgAttr
m.BodyStructure.Extended = k == FetchBodyStructure
v = m.BodyStructure.Format()
case EnvelopeMsgAttr:
case FetchEnvelope:
v = m.Envelope.Format()
case FlagsMsgAttr:
v = FormatStringList(m.Flags)
case InternalDateMsgAttr:
case FetchFlags:
flags := make([]interface{}, len(m.Flags))
for i, flag := range m.Flags {
flags[i] = Atom(flag)
}
v = flags
case FetchInternalDate:
v = m.InternalDate
case SizeMsgAttr:
case FetchRFC822Size:
v = m.Size
case UidMsgAttr:
case FetchUid:
v = m.Uid
default:
for section, literal := range m.Body {
@@ -307,7 +294,7 @@ func (m *Message) Format() []interface{} {
var fields []interface{}
// First send ordered items
processed := make(map[string]bool)
processed := make(map[FetchItem]bool)
for _, k := range m.itemsOrder {
if _, ok := m.Items[k]; ok {
fields = append(fields, m.formatItem(k)...)
@@ -326,9 +313,11 @@ func (m *Message) Format() []interface{} {
}
// Get the body section with the specified name. Returns nil if it's not found.
func (m *Message) GetBody(s string) Literal {
for section, body := range m.Body {
if section.value == s {
func (m *Message) GetBody(section *BodySectionName) Literal {
section = section.resp()
for s, body := range m.Body {
if section.Equal(s) {
return body
}
}
@@ -338,7 +327,7 @@ func (m *Message) GetBody(s string) Literal {
// A body section name.
// See RFC 3501 page 55.
type BodySectionName struct {
*BodyPartName
BodyPartName
// If set to true, do not implicitly set the \Seen flag.
Peek bool
@@ -347,11 +336,11 @@ type BodySectionName struct {
// octets desired.
Partial []int
value string
value FetchItem
}
func (section *BodySectionName) parse(s string) (err error) {
section.value = s
func (section *BodySectionName) parse(s string) error {
section.value = FetchItem(s)
if s == "RFC822" {
s = "BODY[]"
@@ -385,14 +374,13 @@ func (section *BodySectionName) parse(s string) (err error) {
b := bytes.NewBufferString(part + string(cr) + string(lf))
r := NewReader(b)
var fields []interface{}
if fields, err = r.ReadFields(); err != nil {
return
fields, err := r.ReadFields()
if err != nil {
return err
}
section.BodyPartName = &BodyPartName{}
if err = section.BodyPartName.parse(fields); err != nil {
return
if err := section.BodyPartName.parse(fields); err != nil {
return err
}
if len(partial) > 0 {
@@ -420,17 +408,17 @@ func (section *BodySectionName) parse(s string) (err error) {
return nil
}
func (section *BodySectionName) String() (s string) {
func (section *BodySectionName) FetchItem() FetchItem {
if section.value != "" {
return section.value
}
s = "BODY"
s := "BODY"
if section.Peek {
s += ".PEEK"
}
s += "[" + section.BodyPartName.String() + "]"
s += "[" + section.BodyPartName.string() + "]"
if len(section.Partial) > 0 {
s += "<"
@@ -444,30 +432,39 @@ func (section *BodySectionName) String() (s string) {
s += ">"
}
return
return FetchItem(s)
}
// Equal checks whether two sections are equal.
func (section *BodySectionName) Equal(other *BodySectionName) bool {
if section.Peek != other.Peek {
return false
}
if len(section.Partial) != len(other.Partial) {
return false
}
if len(section.Partial) > 0 && section.Partial[0] != other.Partial[0] {
return false
}
if len(section.Partial) > 1 && section.Partial[1] != other.Partial[1] {
return false
}
return section.BodyPartName.Equal(&other.BodyPartName)
}
func (section *BodySectionName) resp() *BodySectionName {
var reset bool
if section.Peek != false {
section.Peek = false
reset = true
resp := *section // Copy section
if resp.Peek != false {
resp.Peek = false
}
if len(section.Partial) == 2 {
section.Partial = []int{section.Partial[0]}
reset = true
if len(resp.Partial) == 2 {
resp.Partial = []int{resp.Partial[0]}
}
if reset && !strings.HasPrefix(section.value, "RFC822") {
section.value = "" // Reset cached value
}
return section
resp.value = ""
return &resp
}
// Returns a subset of the specified bytes matching the partial requested in the
// ExtractPartial returns a subset of the specified bytes matching the partial requested in the
// section name.
func (section *BodySectionName) ExtractPartial(b []byte) []byte {
if len(section.Partial) != 2 {
@@ -486,17 +483,17 @@ func (section *BodySectionName) ExtractPartial(b []byte) []byte {
return b[from:to]
}
// Parse a body section name.
func NewBodySectionName(s string) (section *BodySectionName, err error) {
section = &BodySectionName{}
err = section.parse(s)
return
// ParseBodySectionName parses a body section name.
func ParseBodySectionName(s FetchItem) (*BodySectionName, error) {
section := new(BodySectionName)
err := section.parse(string(s))
return section, err
}
// A body part name.
type BodyPartName struct {
// The specifier of the requested part.
Specifier string
Specifier PartSpecifier
// The part path. Parts indexes start at 1.
Path []int
// If Specifier is HEADER, contains header fields that will/won't be returned,
@@ -521,11 +518,13 @@ func (part *BodyPartName) parse(fields []interface{}) error {
path := strings.Split(strings.ToUpper(name), ".")
end := 0
loop:
for i, node := range path {
if node == "" || node == HeaderSpecifier || node == MimeSpecifier || node == TextSpecifier {
part.Specifier = node
switch PartSpecifier(node) {
case EntireSpecifier, HeaderSpecifier, MIMESpecifier, TextSpecifier:
part.Specifier = PartSpecifier(node)
end = i + 1
break
break loop
}
index, err := strconv.Atoi(node)
@@ -560,14 +559,14 @@ func (part *BodyPartName) parse(fields []interface{}) error {
return nil
}
func (part *BodyPartName) String() (s string) {
func (part *BodyPartName) string() string {
path := make([]string, len(part.Path))
for i, index := range part.Path {
path[i] = strconv.Itoa(index)
}
if part.Specifier != "" {
path = append(path, part.Specifier)
if part.Specifier != EntireSpecifier {
path = append(path, string(part.Specifier))
}
if part.Specifier == HeaderSpecifier && len(part.Fields) > 0 {
@@ -578,13 +577,47 @@ func (part *BodyPartName) String() (s string) {
}
}
s = strings.Join(path, ".")
s := strings.Join(path, ".")
if len(part.Fields) > 0 {
s += " (" + strings.Join(part.Fields, " ") + ")"
}
return
return s
}
// Equal checks whether two body part names are equal.
func (part *BodyPartName) Equal(other *BodyPartName) bool {
if part.Specifier != other.Specifier {
return false
}
if part.NotFields != other.NotFields {
return false
}
if len(part.Path) != len(other.Path) {
return false
}
for i, node := range part.Path {
if node != other.Path[i] {
return false
}
}
if len(part.Fields) != len(other.Fields) {
return false
}
for _, field := range part.Fields {
found := false
for _, f := range other.Fields {
if strings.EqualFold(field, f) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// An address.
@@ -605,17 +638,17 @@ func (addr *Address) Parse(fields []interface{}) error {
return errors.New("Address doesn't contain 4 fields")
}
if f, ok := fields[0].(string); ok {
addr.PersonalName, _ = decodeHeader(f)
if s, err := ParseString(fields[0]); err == nil {
addr.PersonalName, _ = decodeHeader(s)
}
if f, ok := fields[1].(string); ok {
addr.AtDomainList = f
if s, err := ParseString(fields[1]); err == nil {
addr.AtDomainList, _ = decodeHeader(s)
}
if f, ok := fields[2].(string); ok {
addr.MailboxName = f
if s, err := ParseString(fields[2]); err == nil {
addr.MailboxName, _ = decodeHeader(s)
}
if f, ok := fields[3].(string); ok {
addr.HostName = f
if s, err := ParseString(fields[3]); err == nil {
addr.HostName, _ = decodeHeader(s)
}
return nil
@@ -702,7 +735,7 @@ func (e *Envelope) Parse(fields []interface{}) error {
if date, ok := fields[0].(string); ok {
e.Date, _ = parseMessageDateTime(date)
}
if subject, ok := fields[1].(string); ok {
if subject, err := ParseString(fields[1]); err == nil {
e.Subject, _ = decodeHeader(subject)
}
if from, ok := fields[2].([]interface{}); ok {
@@ -755,9 +788,9 @@ type BodyStructure struct {
// Basic fields
// The MIME type.
MimeType string
MIMEType string
// The MIME subtype.
MimeSubType string
MIMESubType string
// The MIME parameters.
Params map[string]string
@@ -796,7 +829,7 @@ type BodyStructure struct {
Location []string
// The MD5 checksum.
Md5 string
MD5 string
}
func (bs *BodyStructure) Parse(fields []interface{}) error {
@@ -809,7 +842,7 @@ func (bs *BodyStructure) Parse(fields []interface{}) error {
switch fields[0].(type) {
case []interface{}: // A multipart body part
bs.MimeType = "multipart"
bs.MIMEType = "multipart"
end := 0
for i, fi := range fields {
@@ -829,7 +862,7 @@ func (bs *BodyStructure) Parse(fields []interface{}) error {
}
}
bs.MimeSubType, _ = fields[end].(string)
bs.MIMESubType, _ = fields[end].(string)
end++
// GMail seems to return only 3 extension data fields. Parse as many fields
@@ -873,14 +906,14 @@ func (bs *BodyStructure) Parse(fields []interface{}) error {
return errors.New("Non-multipart body part doesn't have 7 fields")
}
bs.MimeType, _ = fields[0].(string)
bs.MimeSubType, _ = fields[1].(string)
bs.MIMEType, _ = fields[0].(string)
bs.MIMESubType, _ = fields[1].(string)
params, _ := fields[2].([]interface{})
bs.Params, _ = parseHeaderParamList(params)
bs.Id, _ = fields[3].(string)
if desc, ok := fields[4].(string); ok {
if desc, err := ParseString(fields[4]); err == nil {
bs.Description, _ = decodeHeader(desc)
}
bs.Encoding, _ = fields[5].(string)
@@ -889,7 +922,7 @@ func (bs *BodyStructure) Parse(fields []interface{}) error {
end := 7
// Type-specific fields
if bs.MimeType == "message" && bs.MimeSubType == "rfc822" {
if bs.MIMEType == "message" && bs.MIMESubType == "rfc822" {
if len(fields)-end < 3 {
return errors.New("Missing type-specific fields for message/rfc822")
}
@@ -906,7 +939,7 @@ func (bs *BodyStructure) Parse(fields []interface{}) error {
end += 3
}
if bs.MimeType == "text" {
if bs.MIMEType == "text" {
if len(fields)-end < 1 {
return errors.New("Missing type-specific fields for text/*")
}
@@ -920,7 +953,7 @@ func (bs *BodyStructure) Parse(fields []interface{}) error {
if len(fields) > end {
bs.Extended = true // Contains extension data
bs.Md5, _ = fields[end].(string)
bs.MD5, _ = fields[end].(string)
end++
}
if len(fields) > end {
@@ -956,12 +989,12 @@ func (bs *BodyStructure) Parse(fields []interface{}) error {
}
func (bs *BodyStructure) Format() (fields []interface{}) {
if bs.MimeType == "multipart" {
if bs.MIMEType == "multipart" {
for _, part := range bs.Parts {
fields = append(fields, part.Format())
}
fields = append(fields, bs.MimeSubType)
fields = append(fields, bs.MIMESubType)
if bs.Extended {
extended := make([]interface{}, 4)
@@ -986,8 +1019,8 @@ func (bs *BodyStructure) Format() (fields []interface{}) {
}
} else {
fields = make([]interface{}, 7)
fields[0] = bs.MimeType
fields[1] = bs.MimeSubType
fields[0] = bs.MIMEType
fields[1] = bs.MIMESubType
fields[2] = formatHeaderParamList(bs.Params)
if bs.Id != "" {
@@ -1003,7 +1036,7 @@ func (bs *BodyStructure) Format() (fields []interface{}) {
fields[6] = bs.Size
// Type-specific fields
if bs.MimeType == "message" && bs.MimeSubType == "rfc822" {
if bs.MIMEType == "message" && bs.MIMESubType == "rfc822" {
var env interface{}
if bs.Envelope != nil {
env = bs.Envelope.Format()
@@ -1016,7 +1049,7 @@ func (bs *BodyStructure) Format() (fields []interface{}) {
fields = append(fields, env, bsbs, bs.Lines)
}
if bs.MimeType == "text" {
if bs.MIMEType == "text" {
fields = append(fields, bs.Lines)
}
@@ -1024,8 +1057,8 @@ func (bs *BodyStructure) Format() (fields []interface{}) {
if bs.Extended {
extended := make([]interface{}, 4)
if bs.Md5 != "" {
extended[0] = bs.Md5
if bs.MD5 != "" {
extended[0] = bs.MD5
}
if bs.Disposition != "" {
extended[1] = []interface{}{

View File

@@ -62,7 +62,7 @@ type reader interface {
StringReader
}
// Convert a field to a number.
// ParseNumber parses a number.
func ParseNumber(f interface{}) (uint32, error) {
// Useful for tests
if n, ok := f.(uint32); ok {
@@ -71,7 +71,7 @@ func ParseNumber(f interface{}) (uint32, error) {
s, ok := f.(string)
if !ok {
return 0, newParseError("number is not a string")
return 0, newParseError("expected a number, got a non-atom")
}
nbr, err := strconv.ParseUint(s, 10, 32)
@@ -82,18 +82,44 @@ func ParseNumber(f interface{}) (uint32, error) {
return uint32(nbr), nil
}
// ParseString parses a string, which is either a literal, a quoted string or an
// atom.
func ParseString(f interface{}) (string, error) {
if s, ok := f.(string); ok {
return s, nil
}
// Useful for tests
if q, ok := f.(Quoted); ok {
return string(q), nil
}
if a, ok := f.(Atom); ok {
return string(a), nil
}
if l, ok := f.(Literal); ok {
b := make([]byte, l.Len())
if _, err := io.ReadFull(l, b); err != nil {
return "", err
}
return string(b), nil
}
return "", newParseError("expected a string")
}
// Convert a field list to a string list.
func ParseStringList(f interface{}) ([]string, error) {
fields, ok := f.([]interface{})
if !ok {
return nil, newParseError("string list is not a list")
return nil, newParseError("expected a string list, got a non-list")
}
list := make([]string, len(fields))
for i, f := range fields {
var ok bool
if list[i], ok = f.(string); !ok {
return nil, newParseError("string list contains a non-string")
var err error
if list[i], err = ParseString(f); err != nil {
return nil, newParseError("cannot parse string in string list: " + err.Error())
}
}
return list, nil
@@ -121,7 +147,7 @@ func (r *Reader) ReadSp() error {
return err
}
if char != sp {
return newParseError("not a space")
return newParseError("expected a space")
}
return nil
}
@@ -134,6 +160,7 @@ func (r *Reader) ReadCrlf() (err error) {
}
if char != cr {
err = newParseError("line doesn't end with a CR")
return
}
if char, _, err = r.ReadRune(); err != nil {
@@ -305,6 +332,9 @@ func (r *Reader) ReadFields() (fields []interface{}, err error) {
return
}
if char == cr || char == listEnd || char == respCodeEnd {
if char == cr {
r.UnreadRune()
}
return
}
if char == listStart {
@@ -354,7 +384,7 @@ func (r *Reader) ReadLine() (fields []interface{}, err error) {
return
}
func (r *Reader) ReadRespCode() (code string, fields []interface{}, err error) {
func (r *Reader) ReadRespCode() (code StatusRespCode, fields []interface{}, err error) {
char, _, err := r.ReadRune()
if err != nil {
return
@@ -376,15 +406,16 @@ func (r *Reader) ReadRespCode() (code string, fields []interface{}, err error) {
return
}
code, ok := fields[0].(string)
codeStr, ok := fields[0].(string)
if !ok {
err = newParseError("response code doesn't start with a string atom")
return
}
if code == "" {
if codeStr == "" {
err = newParseError("response code is empty")
return
}
code = StatusRespCode(strings.ToUpper(codeStr))
fields = fields[1:]

View File

@@ -1,17 +1,104 @@
package imap
import (
"errors"
"strings"
)
// A value that can be converted to a Resp.
type Responser interface {
Response() *Resp
// Resp is an IMAP response. It is either a *DataResp, a
// *ContinuationReq or a *StatusResp.
type Resp interface {
resp()
}
// A response.
// See RFC 3501 section 2.2.2
type Resp struct {
// ReadResp reads a single response from a Reader.
func ReadResp(r *Reader) (Resp, error) {
atom, err := r.ReadAtom()
if err != nil {
return nil, err
}
tag, ok := atom.(string)
if !ok {
return nil, newParseError("response tag is not an atom")
}
if tag == "+" {
if err := r.ReadSp(); err != nil {
r.UnreadRune()
}
resp := &ContinuationReq{}
resp.Info, err = r.ReadInfo()
if err != nil {
return nil, err
}
return resp, nil
}
if err := r.ReadSp(); err != nil {
return nil, err
}
// Can be either data or status
// Try to parse a status
var fields []interface{}
if atom, err := r.ReadAtom(); err == nil {
fields = append(fields, atom)
if err := r.ReadSp(); err == nil {
if name, ok := atom.(string); ok {
status := StatusRespType(name)
switch status {
case StatusRespOk, StatusRespNo, StatusRespBad, StatusRespPreauth, StatusRespBye:
resp := &StatusResp{
Tag: tag,
Type: status,
}
char, _, err := r.ReadRune()
if err != nil {
return nil, err
}
r.UnreadRune()
if char == '[' {
// Contains code & arguments
resp.Code, resp.Arguments, err = r.ReadRespCode()
if err != nil {
return nil, err
}
}
resp.Info, err = r.ReadInfo()
if err != nil {
return nil, err
}
return resp, nil
}
}
} else {
r.UnreadRune()
}
} else {
r.UnreadRune()
}
// Not a status so it's data
resp := &DataResp{Tag: tag}
var remaining []interface{}
remaining, err = r.ReadLine()
if err != nil {
return nil, err
}
resp.Fields = append(fields, remaining...)
return resp, nil
}
// DataResp is an IMAP response containing data.
type DataResp struct {
// The response tag. Can be either "" for untagged responses, "+" for continuation
// requests or a previous command's tag.
Tag string
@@ -19,10 +106,20 @@ type Resp struct {
Fields []interface{}
}
func (r *Resp) WriteTo(w *Writer) error {
tag := r.Tag
// NewUntaggedResp creates a new untagged response.
func NewUntaggedResp(fields []interface{}) *DataResp {
return &DataResp{
Tag: "*",
Fields: fields,
}
}
func (r *DataResp) resp() {}
func (r *DataResp) WriteTo(w *Writer) error {
tag := Atom(r.Tag)
if tag == "" {
tag = "*"
tag = Atom("*")
}
fields := []interface{}{tag}
@@ -30,21 +127,15 @@ func (r *Resp) WriteTo(w *Writer) error {
return w.writeLine(fields...)
}
// Create a new untagged response.
func NewUntaggedResp(fields []interface{}) *Resp {
return &Resp{
Tag: "*",
Fields: fields,
}
}
// A continuation request.
type ContinuationResp struct {
// ContinuationReq is a continuation request response.
type ContinuationReq struct {
// The info message sent with the continuation request.
Info string
}
func (r *ContinuationResp) WriteTo(w *Writer) error {
func (r *ContinuationReq) resp() {}
func (r *ContinuationReq) WriteTo(w *Writer) error {
if err := w.writeString("+"); err != nil {
return err
}
@@ -58,99 +149,33 @@ func (r *ContinuationResp) WriteTo(w *Writer) error {
return w.writeCrlf()
}
// Read a single response from a Reader. Returns either a continuation request,
// a status response or a raw response.
func ReadResp(r *Reader) (out interface{}, err error) {
atom, err := r.ReadAtom()
if err != nil {
return
}
tag, ok := atom.(string)
if !ok {
err = errors.New("Response tag is not an atom")
// ParseNamedResp attempts to parse a named data response.
func ParseNamedResp(resp Resp) (name string, fields []interface{}, ok bool) {
data, ok := resp.(*DataResp)
if !ok || len(data.Fields) == 0 {
return
}
if tag == "+" {
if err := r.ReadSp(); err != nil {
r.UnreadRune()
}
res := &ContinuationResp{}
res.Info, err = r.ReadInfo()
if err != nil {
return
}
out = res
return
}
if err = r.ReadSp(); err != nil {
return
}
// Can be either data or status
// Try to parse a status
isStatus := false
var fields []interface{}
if atom, err = r.ReadAtom(); err == nil {
fields = append(fields, atom)
if err = r.ReadSp(); err == nil {
if name, ok := atom.(string); ok {
status := StatusRespType(name)
if status == StatusOk || status == StatusNo || status == StatusBad || status == StatusPreauth || status == StatusBye {
isStatus = true
res := &StatusResp{
Tag: tag,
Type: status,
}
var char rune
if char, _, err = r.ReadRune(); err != nil {
return
}
r.UnreadRune()
if char == '[' {
// Contains code & arguments
res.Code, res.Arguments, err = r.ReadRespCode()
if err != nil {
return
}
}
res.Info, err = r.ReadInfo()
if err != nil {
return
}
out = res
}
// Some responses (namely EXISTS and RECENT) are formatted like so:
// [num] [name] [...]
// Which is fucking stupid. But we handle that here by checking if the
// response name is a number and then rearranging it.
if len(data.Fields) > 1 {
name, ok := data.Fields[1].(string)
if ok {
if _, err := ParseNumber(data.Fields[0]); err == nil {
fields := []interface{}{data.Fields[0]}
fields = append(fields, data.Fields[2:]...)
return strings.ToUpper(name), fields, true
}
} else {
r.UnreadRune()
}
} else {
r.UnreadRune()
}
if !isStatus {
// Not a status so it's data
res := &Resp{Tag: tag}
var remaining []interface{}
remaining, err = r.ReadLine()
if err != nil {
return
}
res.Fields = append(fields, remaining...)
out = res
// IMAP commands are formatted like this:
// [name] [...]
name, ok = data.Fields[0].(string)
if !ok {
return
}
return
return strings.ToUpper(name), data.Fields[1:], true
}

View File

@@ -14,59 +14,45 @@ type Authenticate struct {
Writer *imap.Writer
}
func (r *Authenticate) HandleFrom(hdlr imap.RespHandler) (err error) {
w := r.Writer
func (r *Authenticate) writeLine(l string) error {
if _, err := r.Writer.Write([]byte(l + "\r\n")); err != nil {
return err
}
return r.Writer.Flush()
}
// Cancel auth if an error occurs
defer (func() {
if err != nil {
w.Write([]byte("*\r\n"))
w.Flush()
}
})()
func (r *Authenticate) cancel() error {
return r.writeLine("*")
}
for h := range hdlr {
cont, ok := h.Resp.(*imap.ContinuationResp)
if !ok {
h.Reject()
continue
}
h.Accept()
// Empty challenge, send initial response as stated in RFC 2222 section 5.1
if cont.Info == "" && r.InitialResponse != nil {
encoded := base64.StdEncoding.EncodeToString(r.InitialResponse)
if _, err = w.Write([]byte(encoded + "\r\n")); err != nil {
return
}
if err = w.Flush(); err != nil {
return
}
r.InitialResponse = nil
continue
}
var challenge []byte
challenge, err = base64.StdEncoding.DecodeString(cont.Info)
if err != nil {
return
}
var res []byte
res, err = r.Mechanism.Next(challenge)
if err != nil {
return
}
encoded := base64.StdEncoding.EncodeToString(res)
if _, err = w.Write([]byte(encoded + "\r\n")); err != nil {
return
}
if err = w.Flush(); err != nil {
return
}
func (r *Authenticate) Handle(resp imap.Resp) error {
cont, ok := resp.(*imap.ContinuationReq)
if !ok {
return ErrUnhandled
}
return
// Empty challenge, send initial response as stated in RFC 2222 section 5.1
if cont.Info == "" && r.InitialResponse != nil {
encoded := base64.StdEncoding.EncodeToString(r.InitialResponse)
if err := r.writeLine(encoded); err != nil {
return err
}
r.InitialResponse = nil
return nil
}
challenge, err := base64.StdEncoding.DecodeString(cont.Info)
if err != nil {
r.cancel()
return err
}
reply, err := r.Mechanism.Next(challenge)
if err != nil {
r.cancel()
return err
}
encoded := base64.StdEncoding.EncodeToString(reply)
return r.writeLine(encoded)
}

View File

@@ -10,30 +10,11 @@ type Capability struct {
Caps []string
}
func (r *Capability) HandleFrom(hdlr imap.RespHandler) (err error) {
for h := range hdlr {
caps, ok := h.AcceptNamedResp(imap.Capability)
if !ok {
continue
}
r.Caps = make([]string, len(caps))
for i, c := range caps {
r.Caps[i], _ = c.(string)
}
return
}
return
}
func (r *Capability) WriteTo(w *imap.Writer) error {
fields := []interface{}{imap.Capability}
fields := []interface{}{"CAPABILITY"}
for _, cap := range r.Caps {
fields = append(fields, cap)
}
res := &imap.Resp{Fields: fields}
return res.WriteTo(w)
return imap.NewUntaggedResp(fields).WriteTo(w)
}

View File

@@ -4,39 +4,37 @@ import (
"github.com/emersion/go-imap"
)
const expungeName = "EXPUNGE"
// An EXPUNGE response.
// See RFC 3501 section 7.4.1
type Expunge struct {
SeqNums chan uint32
}
func (r *Expunge) HandleFrom(hdlr imap.RespHandler) error {
defer close(r.SeqNums)
for h := range hdlr {
res, ok := h.Resp.(*imap.Resp)
if !ok || len(res.Fields) < 3 {
h.Reject()
continue
}
if name, ok := res.Fields[1].(string); !ok || name != imap.Expunge {
h.Reject()
continue
}
h.Accept()
seqNum, _ := imap.ParseNumber(res.Fields[0])
r.SeqNums <- seqNum
func (r *Expunge) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != expungeName {
return ErrUnhandled
}
if len(fields) == 0 {
return errNotEnoughFields
}
seqNum, err := imap.ParseNumber(fields[0])
if err != nil {
return err
}
r.SeqNums <- seqNum
return nil
}
func (r *Expunge) WriteTo(w *imap.Writer) error {
for seqNum := range r.SeqNums {
res := imap.NewUntaggedResp([]interface{}{seqNum, imap.Expunge})
if err := res.WriteTo(w); err != nil {
resp := imap.NewUntaggedResp([]interface{}{seqNum, expungeName})
if err := resp.WriteTo(w); err != nil {
return err
}
}

View File

@@ -4,49 +4,41 @@ import (
"github.com/emersion/go-imap"
)
const fetchName = "FETCH"
// A FETCH response.
// See RFC 3501 section 7.4.2
type Fetch struct {
Messages chan *imap.Message
}
func (r *Fetch) HandleFrom(hdlr imap.RespHandler) error {
defer close(r.Messages)
for h := range hdlr {
res, ok := h.Resp.(*imap.Resp)
if !ok || len(res.Fields) < 3 {
h.Reject()
continue
}
if name, ok := res.Fields[1].(string); !ok || name != imap.Fetch {
h.Reject()
continue
}
h.Accept()
seqNum, _ := imap.ParseNumber(res.Fields[0])
fields, _ := res.Fields[2].([]interface{})
msg := &imap.Message{
SeqNum: seqNum,
}
if err := msg.Parse(fields); err != nil {
return err
}
r.Messages <- msg
func (r *Fetch) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != fetchName {
return ErrUnhandled
} else if len(fields) < 1 {
return errNotEnoughFields
}
seqNum, err := imap.ParseNumber(fields[0])
if err != nil {
return err
}
msgFields, _ := fields[1].([]interface{})
msg := &imap.Message{SeqNum: seqNum}
if err := msg.Parse(msgFields); err != nil {
return err
}
r.Messages <- msg
return nil
}
func (r *Fetch) WriteTo(w *imap.Writer) error {
for msg := range r.Messages {
res := imap.NewUntaggedResp([]interface{}{msg.SeqNum, imap.Fetch, msg.Format()})
if err := res.WriteTo(w); err != nil {
resp := imap.NewUntaggedResp([]interface{}{msg.SeqNum, fetchName, msg.Format()})
if err := resp.WriteTo(w); err != nil {
return err
}
}

View File

@@ -4,6 +4,11 @@ import (
"github.com/emersion/go-imap"
)
const (
listName = "LIST"
lsubName = "LSUB"
)
// A LIST response.
// If Subscribed is set to true, LSUB will be used instead.
// See RFC 3501 section 7.2.2
@@ -14,43 +19,36 @@ type List struct {
func (r *List) Name() string {
if r.Subscribed {
return imap.Lsub
return lsubName
} else {
return imap.List
return listName
}
}
func (r *List) HandleFrom(hdlr imap.RespHandler) error {
defer close(r.Mailboxes)
name := r.Name()
for h := range hdlr {
fields, ok := h.AcceptNamedResp(name)
if !ok {
continue
}
mbox := &imap.MailboxInfo{}
if err := mbox.Parse(fields); err != nil {
return err
}
r.Mailboxes <- mbox
func (r *List) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != r.Name() {
return ErrUnhandled
}
mbox := &imap.MailboxInfo{}
if err := mbox.Parse(fields); err != nil {
return err
}
r.Mailboxes <- mbox
return nil
}
func (r *List) WriteTo(w *imap.Writer) error {
name := r.Name()
respName := r.Name()
for mbox := range r.Mailboxes {
fields := []interface{}{name}
fields := []interface{}{respName}
fields = append(fields, mbox.Format()...)
res := imap.NewUntaggedResp(fields)
if err := res.WriteTo(w); err != nil {
resp := imap.NewUntaggedResp(fields)
if err := resp.WriteTo(w); err != nil {
return err
}
}

View File

@@ -1,2 +1,28 @@
// IMAP responses defined in RFC 3501.
package responses
import (
"errors"
"github.com/emersion/go-imap"
)
// ErrUnhandled is used when a response hasn't been handled.
var ErrUnhandled = errors.New("imap: unhandled response")
var errNotEnoughFields = errors.New("imap: not enough fields in response")
// Handler handles responses.
type Handler interface {
// Handle processes a response. If the response cannot be processed,
// ErrUnhandledResp must be returned.
Handle(resp imap.Resp) error
}
// HandlerFunc is a function that handles responses.
type HandlerFunc func(resp imap.Resp) error
// Handle implements Handler.
func (f HandlerFunc) Handle(resp imap.Resp) error {
return f(resp)
}

View File

@@ -4,34 +4,38 @@ import (
"github.com/emersion/go-imap"
)
const searchName = "SEARCH"
// A SEARCH response.
// See RFC 3501 section 7.2.5
type Search struct {
Ids []uint32
}
func (r *Search) HandleFrom(hdlr imap.RespHandler) (err error) {
for h := range hdlr {
fields, ok := h.AcceptNamedResp(imap.Search)
if !ok {
continue
}
func (r *Search) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != searchName {
return ErrUnhandled
}
for _, f := range fields {
id, _ := imap.ParseNumber(f)
r.Ids = append(r.Ids, id)
r.Ids = make([]uint32, len(fields))
for i, f := range fields {
if id, err := imap.ParseNumber(f); err != nil {
return err
} else {
r.Ids[i] = id
}
}
return
return nil
}
func (r *Search) WriteTo(w *imap.Writer) (err error) {
fields := []interface{}{imap.Search}
fields := []interface{}{searchName}
for _, id := range r.Ids {
fields = append(fields, id)
}
res := imap.NewUntaggedResp(fields)
return res.WriteTo(w)
resp := imap.NewUntaggedResp(fields)
return resp.WriteTo(w)
}

View File

@@ -11,135 +11,132 @@ type Select struct {
Mailbox *imap.MailboxStatus
}
func (r *Select) HandleFrom(hdlr imap.RespHandler) (err error) {
func (r *Select) Handle(resp imap.Resp) error {
if r.Mailbox == nil {
r.Mailbox = &imap.MailboxStatus{}
r.Mailbox = &imap.MailboxStatus{Items: make(map[imap.StatusItem]interface{})}
}
mbox := r.Mailbox
mbox.Items = make(map[string]interface{})
for h := range hdlr {
switch res := h.Resp.(type) {
case *imap.Resp:
fields, ok := h.AcceptNamedResp(imap.MailboxFlags)
if !ok {
break
}
switch resp := resp.(type) {
case *imap.DataResp:
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != "FLAGS" {
return ErrUnhandled
} else if len(fields) < 1 {
return errNotEnoughFields
}
flags, _ := fields[0].([]interface{})
mbox.Flags, _ = imap.ParseStringList(flags)
flags, _ := fields[0].([]interface{})
mbox.Flags, _ = imap.ParseStringList(flags)
case *imap.StatusResp:
if len(resp.Arguments) < 1 {
return ErrUnhandled
}
var item imap.StatusItem
switch resp.Code {
case "UNSEEN":
mbox.UnseenSeqNum, _ = imap.ParseNumber(resp.Arguments[0])
case "PERMANENTFLAGS":
flags, _ := resp.Arguments[0].([]interface{})
mbox.PermanentFlags, _ = imap.ParseStringList(flags)
case "UIDNEXT":
mbox.UidNext, _ = imap.ParseNumber(resp.Arguments[0])
item = imap.StatusUidNext
case "UIDVALIDITY":
mbox.UidValidity, _ = imap.ParseNumber(resp.Arguments[0])
item = imap.StatusUidValidity
default:
return ErrUnhandled
}
if item != "" {
mbox.ItemsLocker.Lock()
mbox.Items[imap.MailboxFlags] = nil
mbox.Items[item] = nil
mbox.ItemsLocker.Unlock()
case *imap.StatusResp:
if len(res.Arguments) < 1 {
h.Accepts <- false
break
}
}
default:
return ErrUnhandled
}
return nil
}
accepted := true
switch res.Code {
case imap.MailboxUnseen:
mbox.Unseen, _ = imap.ParseNumber(res.Arguments[0])
mbox.ItemsLocker.Lock()
mbox.Items[imap.MailboxUnseen] = nil
mbox.ItemsLocker.Unlock()
case imap.MailboxPermanentFlags:
flags, _ := res.Arguments[0].([]interface{})
mbox.PermanentFlags, _ = imap.ParseStringList(flags)
mbox.ItemsLocker.Lock()
mbox.Items[imap.MailboxPermanentFlags] = nil
mbox.ItemsLocker.Unlock()
case imap.MailboxUidNext:
mbox.UidNext, _ = imap.ParseNumber(res.Arguments[0])
mbox.ItemsLocker.Lock()
mbox.Items[imap.MailboxUidNext] = nil
mbox.ItemsLocker.Unlock()
case imap.MailboxUidValidity:
mbox.UidValidity, _ = imap.ParseNumber(res.Arguments[0])
mbox.ItemsLocker.Lock()
mbox.Items[imap.MailboxUidValidity] = nil
mbox.ItemsLocker.Unlock()
default:
accepted = false
}
h.Accepts <- accepted
func (r *Select) WriteTo(w *imap.Writer) error {
mbox := r.Mailbox
if mbox.Flags != nil {
flags := make([]interface{}, len(mbox.Flags))
for i, f := range mbox.Flags {
flags[i] = imap.Atom(f)
}
res := imap.NewUntaggedResp([]interface{}{"FLAGS", flags})
if err := res.WriteTo(w); err != nil {
return err
}
}
return
}
if mbox.PermanentFlags != nil {
flags := make([]interface{}, len(mbox.PermanentFlags))
for i, f := range mbox.PermanentFlags {
flags[i] = imap.Atom(f)
}
statusRes := &imap.StatusResp{
Type: imap.StatusRespOk,
Code: imap.CodePermanentFlags,
Arguments: []interface{}{flags},
Info: "Flags permitted.",
}
if err := statusRes.WriteTo(w); err != nil {
return err
}
}
func (r *Select) WriteTo(w *imap.Writer) (err error) {
status := r.Mailbox
if mbox.UnseenSeqNum > 0 {
statusRes := &imap.StatusResp{
Type: imap.StatusRespOk,
Code: imap.CodeUnseen,
Arguments: []interface{}{mbox.UnseenSeqNum},
Info: fmt.Sprintf("Message %d is first unseen", mbox.UnseenSeqNum),
}
if err := statusRes.WriteTo(w); err != nil {
return err
}
}
for k := range r.Mailbox.Items {
switch k {
case imap.MailboxFlags:
flags := make([]interface{}, len(status.Flags))
for i, f := range status.Flags {
flags[i] = f
case imap.StatusMessages:
res := imap.NewUntaggedResp([]interface{}{mbox.Messages, "EXISTS"})
if err := res.WriteTo(w); err != nil {
return err
}
res := imap.NewUntaggedResp([]interface{}{"FLAGS", flags})
if err = res.WriteTo(w); err != nil {
return
}
case imap.MailboxPermanentFlags:
flags := make([]interface{}, len(status.PermanentFlags))
for i, f := range status.PermanentFlags {
flags[i] = f
case imap.StatusRecent:
res := imap.NewUntaggedResp([]interface{}{mbox.Recent, "RECENT"})
if err := res.WriteTo(w); err != nil {
return err
}
case imap.StatusUidNext:
statusRes := &imap.StatusResp{
Type: imap.StatusOk,
Code: imap.CodePermanentFlags,
Arguments: []interface{}{flags},
Info: "Flags permitted.",
}
if err = statusRes.WriteTo(w); err != nil {
return
}
case imap.MailboxMessages:
res := imap.NewUntaggedResp([]interface{}{status.Messages, "EXISTS"})
if err = res.WriteTo(w); err != nil {
return
}
case imap.MailboxRecent:
res := imap.NewUntaggedResp([]interface{}{status.Recent, "RECENT"})
if err = res.WriteTo(w); err != nil {
return
}
case imap.MailboxUnseen:
statusRes := &imap.StatusResp{
Type: imap.StatusOk,
Code: imap.CodeUnseen,
Arguments: []interface{}{status.Unseen},
Info: fmt.Sprintf("Message %d is first unseen", status.Unseen),
}
if err = statusRes.WriteTo(w); err != nil {
return
}
case imap.MailboxUidNext:
statusRes := &imap.StatusResp{
Type: imap.StatusOk,
Type: imap.StatusRespOk,
Code: imap.CodeUidNext,
Arguments: []interface{}{status.UidNext},
Arguments: []interface{}{mbox.UidNext},
Info: "Predicted next UID",
}
if err = statusRes.WriteTo(w); err != nil {
return
if err := statusRes.WriteTo(w); err != nil {
return err
}
case imap.MailboxUidValidity:
case imap.StatusUidValidity:
statusRes := &imap.StatusResp{
Type: imap.StatusOk,
Type: imap.StatusRespOk,
Code: imap.CodeUidValidity,
Arguments: []interface{}{status.UidValidity},
Arguments: []interface{}{mbox.UidValidity},
Info: "UIDs valid",
}
if err = statusRes.WriteTo(w); err != nil {
return
if err := statusRes.WriteTo(w); err != nil {
return err
}
}
}
return
return nil
}

View File

@@ -7,57 +7,47 @@ import (
"github.com/emersion/go-imap/utf7"
)
const statusName = "STATUS"
// A STATUS response.
// See RFC 3501 section 7.2.4
type Status struct {
Mailbox *imap.MailboxStatus
}
func (r *Status) HandleFrom(hdlr imap.RespHandler) error {
func (r *Status) Handle(resp imap.Resp) error {
if r.Mailbox == nil {
r.Mailbox = &imap.MailboxStatus{}
}
mbox := r.Mailbox
mbox.Items = nil
for h := range hdlr {
fields, ok := h.AcceptNamedResp(imap.Status)
if !ok {
continue
}
if len(fields) < 2 {
return errors.New("STATUS response expects two fields")
}
name, ok := fields[0].(string)
if !ok {
return errors.New("STATUS response expects a string as first argument")
}
mbox.Name, _ = utf7.Decoder.String(name)
mbox.Name = imap.CanonicalMailboxName(mbox.Name)
var items []interface{}
if items, ok = fields[1].([]interface{}); !ok {
return errors.New("STATUS response expects a list as second argument")
}
if err := mbox.Parse(items); err != nil {
return err
}
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != statusName {
return ErrUnhandled
} else if len(fields) < 2 {
return errNotEnoughFields
}
return nil
if name, err := imap.ParseString(fields[0]); err != nil {
return err
} else if name, err := utf7.Encoding.NewDecoder().String(name); err != nil {
return err
} else {
mbox.Name = imap.CanonicalMailboxName(name)
}
var items []interface{}
if items, ok = fields[1].([]interface{}); !ok {
return errors.New("STATUS response expects a list as second argument")
}
mbox.Items = nil
return mbox.Parse(items)
}
func (r *Status) WriteTo(w *imap.Writer) error {
mbox := r.Mailbox
fields := []interface{}{imap.Status, mbox.Name, mbox.Format()}
res := imap.NewUntaggedResp(fields)
if err := res.WriteTo(w); err != nil {
return err
}
return nil
name, _ := utf7.Encoding.NewEncoder().String(mbox.Name)
fields := []interface{}{statusName, name, mbox.Format()}
return imap.NewUntaggedResp(fields).WriteTo(w)
}

View File

@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/textproto"
"strings"
"time"
@@ -43,8 +42,8 @@ func convertField(f interface{}, charsetReader func(io.Reader) io.Reader) string
}
}
b, err := ioutil.ReadAll(r)
if err != nil {
b := make([]byte, l.Len())
if _, err := io.ReadFull(r, b); err != nil {
return ""
}
return string(b)
@@ -238,7 +237,7 @@ func (c *SearchCriteria) parseField(fields []interface{}, charsetReader func(io.
case "UID":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else if c.Uid, err = NewSeqSet(maybeString(f)); err != nil {
} else if c.Uid, err = ParseSeqSet(maybeString(f)); err != nil {
return nil, err
}
case "UNANSWERED", "UNDELETED", "UNDRAFT", "UNFLAGGED", "UNSEEN":
@@ -251,7 +250,7 @@ func (c *SearchCriteria) parseField(fields []interface{}, charsetReader func(io.
c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag(maybeString(f)))
}
default: // Try to parse a sequence set
if c.SeqNum, err = NewSeqSet(key); err != nil {
if c.SeqNum, err = ParseSeqSet(key); err != nil {
return nil, err
}
}

View File

@@ -122,8 +122,8 @@ type SeqSet struct {
Set []Seq
}
// NewSeqSet returns a new SeqSet instance after parsing the set string.
func NewSeqSet(set string) (s *SeqSet, err error) {
// ParseSeqSet returns a new SeqSet instance after parsing the set string.
func ParseSeqSet(set string) (s *SeqSet, err error) {
s = new(SeqSet)
return s, s.Add(set)
}
@@ -268,7 +268,6 @@ func (s *SeqSet) insertAt(i int, v Seq) {
s.Set = set
}
s.Set[i] = v
return
}
// search attempts to find the index of the sequence set value that contains q.

View File

@@ -24,7 +24,7 @@ func (cmd *Noop) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.Mailbox != nil {
// If a mailbox is selected, NOOP can be used to poll for server updates
if mbox, ok := ctx.Mailbox.(backend.UpdaterMailbox); ok {
if mbox, ok := ctx.Mailbox.(backend.MailboxPoller); ok {
return mbox.Poll()
}
}
@@ -38,7 +38,7 @@ type Logout struct {
func (cmd *Logout) Handle(conn Conn) error {
res := &imap.StatusResp{
Type: imap.StatusBye,
Type: imap.StatusRespBye,
Info: "Closing connection",
}

View File

@@ -29,10 +29,9 @@ func (cmd *Select) Handle(conn Conn) error {
return err
}
items := []string{
imap.MailboxFlags, imap.MailboxPermanentFlags,
imap.MailboxMessages, imap.MailboxRecent, imap.MailboxUnseen,
imap.MailboxUidNext, imap.MailboxUidValidity,
items := []imap.StatusItem{
imap.StatusMessages, imap.StatusRecent, imap.StatusUnseen,
imap.StatusUidNext, imap.StatusUidValidity,
}
status, err := mbox.Status(items)
@@ -48,12 +47,12 @@ func (cmd *Select) Handle(conn Conn) error {
return err
}
code := imap.CodeReadWrite
var code imap.StatusRespCode = imap.CodeReadWrite
if ctx.MailboxReadOnly {
code = imap.CodeReadOnly
}
return ErrStatusResp(&imap.StatusResp{
Type: imap.StatusOk,
Type: imap.StatusRespOk,
Code: code,
})
}
@@ -208,7 +207,7 @@ func (cmd *Status) Handle(conn Conn) error {
}
// Only keep items thqat have been requested
items := make(map[string]interface{})
items := make(map[imap.StatusItem]interface{})
for _, k := range cmd.Items {
items[k] = status.Items[k]
}
@@ -231,7 +230,7 @@ func (cmd *Append) Handle(conn Conn) error {
mbox, err := ctx.User.GetMailbox(cmd.Mailbox)
if err == backend.ErrNoSuchMailbox {
return ErrStatusResp(&imap.StatusResp{
Type: imap.StatusNo,
Type: imap.StatusRespNo,
Code: imap.CodeTryCreate,
Info: err.Error(),
})
@@ -246,10 +245,13 @@ func (cmd *Append) Handle(conn Conn) error {
// If APPEND targets the currently selected mailbox, send an untagged EXISTS
// Do this only if the backend doesn't send updates itself
if conn.Server().Updates == nil && ctx.Mailbox != nil && ctx.Mailbox.Name() == mbox.Name() {
status, err := mbox.Status([]string{imap.MailboxMessages})
status, err := mbox.Status([]imap.StatusItem{imap.StatusMessages})
if err != nil {
return err
}
status.Flags = nil
status.PermanentFlags = nil
status.UnseenSeqNum = 0
res := &responses.Select{Mailbox: status}
if err := conn.WriteResp(res); err != nil {

View File

@@ -36,7 +36,7 @@ func (cmd *StartTLS) Handle(conn Conn) error {
// Send an OK status response to let the client know that the TLS handshake
// can begin
return ErrStatusResp(&imap.StatusResp{
Type: imap.StatusOk,
Type: imap.StatusRespOk,
Info: "Begin TLS negotiation now",
})
}
@@ -62,7 +62,7 @@ func (cmd *StartTLS) Upgrade(conn Conn) error {
func afterAuthStatus(conn Conn) error {
return ErrStatusResp(&imap.StatusResp{
Type: imap.StatusOk,
Type: imap.StatusRespOk,
Code: imap.CodeCapability,
Arguments: imap.FormatStringList(conn.Capabilities()),
})

View File

@@ -2,7 +2,6 @@ package server
import (
"errors"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/commands"
@@ -53,12 +52,8 @@ func (cmd *Close) Handle(conn Conn) error {
ctx.Mailbox = nil
ctx.MailboxReadOnly = false
if err := mailbox.Expunge(); err != nil {
return err
}
// No need to send expunge updates here, since the mailbox is already unselected
return nil
return mailbox.Expunge()
}
type Expunge struct {
@@ -206,15 +201,10 @@ func (cmd *Store) handle(uid bool, conn Conn) error {
return ErrMailboxReadOnly
}
itemStr := cmd.Item
silent := strings.HasSuffix(itemStr, imap.SilentOp)
if silent {
itemStr = strings.TrimSuffix(itemStr, imap.SilentOp)
}
item := imap.FlagsOp(itemStr)
if item != imap.SetFlags && item != imap.AddFlags && item != imap.RemoveFlags {
return errors.New("Unsupported STORE operation")
// Only flags operations are supported
op, silent, err := imap.ParseFlagsOp(cmd.Item)
if err != nil {
return err
}
flagsList, ok := cmd.Value.([]interface{})
@@ -233,7 +223,7 @@ func (cmd *Store) handle(uid bool, conn Conn) error {
// from receiving them
// TODO: find a better way to do this, without conn.silent
*conn.silent() = silent
err = ctx.Mailbox.UpdateMessagesFlags(uid, cmd.SeqSet, item, flags)
err = ctx.Mailbox.UpdateMessagesFlags(uid, cmd.SeqSet, op, flags)
*conn.silent() = false
if err != nil {
return err
@@ -244,7 +234,7 @@ func (cmd *Store) handle(uid bool, conn Conn) error {
if conn.Server().Updates == nil && !silent {
inner := &Fetch{}
inner.SeqSet = cmd.SeqSet
inner.Items = []string{"FLAGS"}
inner.Items = []imap.FetchItem{imap.FetchFlags}
if uid {
inner.Items = append(inner.Items, "UID")
}
@@ -307,7 +297,7 @@ func (cmd *Uid) Handle(conn Conn) error {
}
return ErrStatusResp(&imap.StatusResp{
Type: imap.StatusOk,
Info: imap.Uid + " " + inner.Name + " completed",
Type: imap.StatusRespOk,
Info: "UID " + inner.Name + " completed",
})
}

View File

@@ -36,7 +36,7 @@ type Conn interface {
setTLSConn(*tls.Conn)
silent() *bool // TODO: remove this
serve() error
serve(Conn) error
commandHandler(cmd *imap.Command) (hdlr Handler, err error)
}
@@ -52,17 +52,21 @@ type Context struct {
MailboxReadOnly bool
// Responses to send to the client.
Responses chan<- imap.WriterTo
// Closed when the client is logged out.
LoggedOut <-chan struct{}
}
type conn struct {
*imap.Conn
conn Conn // With extensions overrides
s *Server
ctx *Context
l sync.Locker
tlsConn *tls.Conn
continues chan bool
responses chan imap.WriterTo
loggedOut chan struct{}
silentVal bool
}
@@ -73,6 +77,7 @@ func newConn(s *Server, c net.Conn) *conn {
w := imap.NewWriter(nil)
responses := make(chan imap.WriterTo)
loggedOut := make(chan struct{})
tlsConn, _ := c.(*tls.Conn)
@@ -84,10 +89,12 @@ func newConn(s *Server, c net.Conn) *conn {
ctx: &Context{
State: imap.ConnectingState,
Responses: responses,
LoggedOut: loggedOut,
},
tlsConn: tlsConn,
continues: continues,
responses: responses,
loggedOut: loggedOut,
}
if s.Debug != nil {
@@ -149,14 +156,7 @@ func (c *conn) Close() error {
c.ctx.User.Logout()
}
if err := c.Conn.Close(); err != nil {
return err
}
close(c.continues)
c.ctx.State = imap.LogoutState
return nil
return c.Conn.Close()
}
func (c *conn) Capabilities() []string {
@@ -187,8 +187,8 @@ func (c *conn) send() {
// Send continuation requests
go func() {
for range c.continues {
res := &imap.ContinuationResp{Info: "send literal"}
if err := res.WriteTo(c.Writer); err != nil {
resp := &imap.ContinuationReq{Info: "send literal"}
if err := resp.WriteTo(c.Writer); err != nil {
c.Server().ErrorLog.Println("cannot send continuation request: ", err)
} else if err := c.Writer.Flush(); err != nil {
c.Server().ErrorLog.Println("cannot flush connection: ", err)
@@ -199,19 +199,22 @@ func (c *conn) send() {
// Send responses
for {
// Get a response that needs to be sent
res := <-c.responses
select {
case res := <-c.responses:
// Request to send the response
c.l.Lock()
// Request to send the response
c.l.Lock()
// Send the response
if err := res.WriteTo(c.Writer); err != nil {
c.Server().ErrorLog.Println("cannot send response: ", err)
} else if err := c.Writer.Flush(); err != nil {
c.Server().ErrorLog.Println("cannot flush connection: ", err)
}
// Send the response
if err := res.WriteTo(c.Writer); err != nil {
c.Server().ErrorLog.Println("cannot send response: ", err)
} else if err := c.Writer.Flush(); err != nil {
c.Server().ErrorLog.Println("cannot flush connection: ", err)
c.l.Unlock()
case <-c.loggedOut:
return
}
c.l.Unlock()
}
}
@@ -225,7 +228,7 @@ func (c *conn) greet() error {
}
greeting := &imap.StatusResp{
Type: imap.StatusOk,
Type: imap.StatusRespOk,
Code: imap.CodeCapability,
Arguments: args,
Info: "IMAP4rev1 Service Ready",
@@ -262,7 +265,15 @@ func (c *conn) silent() *bool {
return &c.silentVal
}
func (c *conn) serve() error {
func (c *conn) serve(conn Conn) error {
c.conn = conn
defer func() {
c.ctx.State = imap.LogoutState
close(c.continues)
close(c.loggedOut)
}()
// Send greeting
if err := c.greet(); err != nil {
return err
@@ -286,7 +297,7 @@ func (c *conn) serve() error {
if err != nil {
if imap.IsParseError(err) {
res = &imap.StatusResp{
Type: imap.StatusBad,
Type: imap.StatusRespBad,
Info: err.Error(),
}
} else {
@@ -298,7 +309,7 @@ func (c *conn) serve() error {
if err := cmd.Parse(fields); err != nil {
res = &imap.StatusResp{
Tag: cmd.Tag,
Type: imap.StatusBad,
Type: imap.StatusRespBad,
Info: err.Error(),
}
} else {
@@ -307,7 +318,7 @@ func (c *conn) serve() error {
if err != nil {
res = &imap.StatusResp{
Tag: cmd.Tag,
Type: imap.StatusBad,
Type: imap.StatusRespBad,
Info: err.Error(),
}
}
@@ -323,8 +334,8 @@ func (c *conn) serve() error {
continue
}
if up != nil && res.Type == imap.StatusOk {
if err := up.Upgrade(c); err != nil {
if up != nil && res.Type == imap.StatusRespOk {
if err := up.Upgrade(c.conn); err != nil {
c.s.ErrorLog.Println("cannot upgrade connection:", err)
return err
}
@@ -356,24 +367,24 @@ func (c *conn) handleCommand(cmd *imap.Command) (res *imap.StatusResp, up Upgrad
c.l.Unlock()
defer c.l.Lock()
hdlrErr := hdlr.Handle(c)
hdlrErr := hdlr.Handle(c.conn)
if statusErr, ok := hdlrErr.(*errStatusResp); ok {
res = statusErr.resp
} else if hdlrErr != nil {
res = &imap.StatusResp{
Type: imap.StatusNo,
Type: imap.StatusRespNo,
Info: hdlrErr.Error(),
}
} else {
res = &imap.StatusResp{
Type: imap.StatusOk,
Type: imap.StatusRespOk,
}
}
if res != nil {
res.Tag = cmd.Tag
if res.Type == imap.StatusOk && res.Info == "" {
if res.Type == imap.StatusRespOk && res.Info == "" {
res.Info = cmd.Name + " completed"
}
}

View File

@@ -45,7 +45,7 @@ type Upgrader interface {
type HandlerFactory func() Handler
// A function that creates SASL servers.
type SaslServerFactory func(conn Conn) sasl.Server
type SASLServerFactory func(conn Conn) sasl.Server
// An IMAP extension.
type Extension interface {
@@ -94,7 +94,7 @@ type Server struct {
conns map[Conn]struct{}
commands map[string]HandlerFactory
auths map[string]SaslServerFactory
auths map[string]SASLServerFactory
extensions []Extension
// TCP address to listen on.
@@ -104,7 +104,7 @@ type Server struct {
// This server's backend.
Backend backend.Backend
// Backend updates that will be sent to connected clients.
Updates <-chan interface{}
Updates <-chan backend.Update
// Automatically logout clients after a duration. To do not logout users
// automatically, set this to zero. The duration MUST be at least
// MinAutoLogout (as stated in RFC 3501 section 5.4).
@@ -132,7 +132,7 @@ func New(bkd backend.Backend) *Server {
ErrorLog: log.New(os.Stderr, "imap/server: ", log.LstdFlags),
}
s.auths = map[string]SaslServerFactory{
s.auths = map[string]SASLServerFactory{
sasl.Plain: func(conn Conn) sasl.Server {
return sasl.NewPlainServer(func(identity, username, password string) error {
if identity != "" && identity != username {
@@ -153,42 +153,42 @@ func New(bkd backend.Backend) *Server {
}
s.commands = map[string]HandlerFactory{
imap.Noop: func() Handler { return &Noop{} },
imap.Capability: func() Handler { return &Capability{} },
imap.Logout: func() Handler { return &Logout{} },
"NOOP": func() Handler { return &Noop{} },
"CAPABILITY": func() Handler { return &Capability{} },
"LOGOUT": func() Handler { return &Logout{} },
imap.StartTLS: func() Handler { return &StartTLS{} },
imap.Login: func() Handler { return &Login{} },
imap.Authenticate: func() Handler { return &Authenticate{} },
"STARTTLS": func() Handler { return &StartTLS{} },
"LOGIN": func() Handler { return &Login{} },
"AUTHENTICATE": func() Handler { return &Authenticate{} },
imap.Select: func() Handler { return &Select{} },
imap.Examine: func() Handler {
"SELECT": func() Handler { return &Select{} },
"EXAMINE": func() Handler {
hdlr := &Select{}
hdlr.ReadOnly = true
return hdlr
},
imap.Create: func() Handler { return &Create{} },
imap.Delete: func() Handler { return &Delete{} },
imap.Rename: func() Handler { return &Rename{} },
imap.Subscribe: func() Handler { return &Subscribe{} },
imap.Unsubscribe: func() Handler { return &Unsubscribe{} },
imap.List: func() Handler { return &List{} },
imap.Lsub: func() Handler {
"CREATE": func() Handler { return &Create{} },
"DELETE": func() Handler { return &Delete{} },
"RENAME": func() Handler { return &Rename{} },
"SUBSCRIBE": func() Handler { return &Subscribe{} },
"UNSUBSCRIBE": func() Handler { return &Unsubscribe{} },
"LIST": func() Handler { return &List{} },
"LSUB": func() Handler {
hdlr := &List{}
hdlr.Subscribed = true
return hdlr
},
imap.Status: func() Handler { return &Status{} },
imap.Append: func() Handler { return &Append{} },
"STATUS": func() Handler { return &Status{} },
"APPEND": func() Handler { return &Append{} },
imap.Check: func() Handler { return &Check{} },
imap.Close: func() Handler { return &Close{} },
imap.Expunge: func() Handler { return &Expunge{} },
imap.Search: func() Handler { return &Search{} },
imap.Fetch: func() Handler { return &Fetch{} },
imap.Store: func() Handler { return &Store{} },
imap.Copy: func() Handler { return &Copy{} },
imap.Uid: func() Handler { return &Uid{} },
"CHECK": func() Handler { return &Check{} },
"CLOSE": func() Handler { return &Close{} },
"EXPUNGE": func() Handler { return &Expunge{} },
"SEARCH": func() Handler { return &Search{} },
"FETCH": func() Handler { return &Fetch{} },
"STORE": func() Handler { return &Store{} },
"COPY": func() Handler { return &Copy{} },
"UID": func() Handler { return &Uid{} },
}
return s
@@ -207,7 +207,11 @@ func (s *Server) Serve(l net.Listener) error {
delete(s.listeners, l)
}()
go s.listenUpdates()
updater, ok := s.Backend.(backend.BackendUpdater)
if ok {
s.Updates = updater.Updates()
go s.listenUpdates()
}
for {
c, err := l.Accept()
@@ -274,7 +278,7 @@ func (s *Server) serveConn(conn Conn) error {
delete(s.conns, conn)
}()
return conn.serve()
return conn.serve(conn)
}
// Get a command handler factory for the provided command name.
@@ -289,49 +293,32 @@ func (s *Server) Command(name string) HandlerFactory {
return s.commands[name]
}
func (s *Server) listenUpdates() (err error) {
updater, ok := s.Backend.(backend.Updater)
if !ok {
return
}
s.Updates = updater.Updates()
func (s *Server) listenUpdates() {
for {
item := <-s.Updates
update := <-s.Updates
var (
update *backend.Update
res imap.WriterTo
)
switch item := item.(type) {
var res imap.WriterTo
switch update := update.(type) {
case *backend.StatusUpdate:
update = &item.Update
res = item.StatusResp
res = update.StatusResp
case *backend.MailboxUpdate:
update = &item.Update
res = &responses.Select{Mailbox: item.MailboxStatus}
res = &responses.Select{Mailbox: update.MailboxStatus}
case *backend.MessageUpdate:
update = &item.Update
ch := make(chan *imap.Message, 1)
ch <- item.Message
ch <- update.Message
close(ch)
res = &responses.Fetch{Messages: ch}
case *backend.ExpungeUpdate:
update = &item.Update
ch := make(chan uint32, 1)
ch <- item.SeqNum
ch <- update.SeqNum
close(ch)
res = &responses.Expunge{SeqNums: ch}
default:
s.ErrorLog.Printf("unhandled update: %T\n", item)
s.ErrorLog.Printf("unhandled update: %T\n", update)
}
if update == nil || res == nil {
if res == nil {
continue
}
@@ -341,10 +328,10 @@ func (s *Server) listenUpdates() (err error) {
for conn := range s.conns {
ctx := conn.Context()
if update.Username != "" && (ctx.User == nil || ctx.User.Username() != update.Username) {
if update.Username() != "" && (ctx.User == nil || ctx.User.Username() != update.Username()) {
continue
}
if update.Mailbox != "" && (ctx.Mailbox == nil || ctx.Mailbox.Name() != update.Mailbox) {
if update.Mailbox() != "" && (ctx.Mailbox == nil || ctx.Mailbox.Name() != update.Mailbox()) {
continue
}
if *conn.silent() {
@@ -374,12 +361,11 @@ func (s *Server) listenUpdates() (err error) {
for done := 0; done < wait; done++ {
<-sends
}
close(sends)
backend.DoneUpdate(update)
close(update.Done())
}()
} else {
backend.DoneUpdate(update)
close(update.Done())
}
}
}
@@ -421,6 +407,6 @@ func (s *Server) Enable(extensions ...Extension) {
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the IMAP protocol.
func (s *Server) EnableAuth(name string, f SaslServerFactory) {
func (s *Server) EnableAuth(name string, f SASLServerFactory) {
s.auths[name] = f
}

View File

@@ -12,13 +12,13 @@ const (
// The OK response indicates an information message from the server. When
// tagged, it indicates successful completion of the associated command.
// The untagged form indicates an information-only message.
StatusOk StatusRespType = "OK"
StatusRespOk StatusRespType = "OK"
// The NO response indicates an operational error message from the
// server. When tagged, it indicates unsuccessful completion of the
// associated command. The untagged form indicates a warning; the
// command can still complete successfully.
StatusNo = "NO"
StatusRespNo = "NO"
// The BAD response indicates an error message from the server. When
// tagged, it reports a protocol-level error in the client's command;
@@ -26,32 +26,34 @@ const (
// form indicates a protocol-level error for which the associated
// command can not be determined; it can also indicate an internal
// server failure.
StatusBad = "BAD"
StatusRespBad = "BAD"
// The PREAUTH response is always untagged, and is one of three
// possible greetings at connection startup. It indicates that the
// connection has already been authenticated by external means; thus
// no LOGIN command is needed.
StatusPreauth = "PREAUTH"
StatusRespPreauth = "PREAUTH"
// The BYE response is always untagged, and indicates that the server
// is about to close the connection.
StatusBye = "BYE"
StatusRespBye = "BYE"
)
type StatusRespCode string
// Status response codes defined in RFC 3501 section 7.1.
const (
CodeAlert = "ALERT"
CodeBadCharset = "BADCHARSET"
CodeCapability = "CAPABILITY"
CodeParse = "PARSE"
CodePermanentFlags = "PERMANENTFLAGS"
CodeReadOnly = "READ-ONLY"
CodeReadWrite = "READ-WRITE"
CodeTryCreate = "TRYCREATE"
CodeUidNext = "UIDNEXT"
CodeUidValidity = "UIDVALIDITY"
CodeUnseen = "UNSEEN"
CodeAlert StatusRespCode = "ALERT"
CodeBadCharset = "BADCHARSET"
CodeCapability = "CAPABILITY"
CodeParse = "PARSE"
CodePermanentFlags = "PERMANENTFLAGS"
CodeReadOnly = "READ-ONLY"
CodeReadWrite = "READ-WRITE"
CodeTryCreate = "TRYCREATE"
CodeUidNext = "UIDNEXT"
CodeUidValidity = "UIDVALIDITY"
CodeUnseen = "UNSEEN"
)
// A status response.
@@ -59,21 +61,19 @@ const (
type StatusResp struct {
// The response tag. If empty, it defaults to *.
Tag string
// The status type.
Type StatusRespType
// The status code.
// See https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml
Code string
Code StatusRespCode
// Arguments provided with the status code.
Arguments []interface{}
// The status info.
Info string
}
func (r *StatusResp) resp() {}
// If this status is NO or BAD, returns an error with the status info.
// Otherwise, returns nil.
func (r *StatusResp) Err() error {
@@ -82,14 +82,14 @@ func (r *StatusResp) Err() error {
return errors.New("imap: connection closed during command execution")
}
if r.Type == StatusNo || r.Type == StatusBad {
if r.Type == StatusRespNo || r.Type == StatusRespBad {
return errors.New(r.Info)
}
return nil
}
func (r *StatusResp) WriteTo(w *Writer) error {
tag := r.Tag
tag := Atom(r.Tag)
if tag == "" {
tag = "*"
}

View File

@@ -5,15 +5,11 @@ import (
"unicode/utf16"
"unicode/utf8"
"golang.org/x/text/encoding"
"golang.org/x/text/transform"
)
var ErrBadUtf7 = errors.New("bad utf-7 encoding")
var Decoder = &encoding.Decoder{
Transformer: &decoder{true},
}
// ErrInvalidUTF7 means that a transformer encountered invalid UTF-7.
var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7")
type decoder struct {
ascii bool
@@ -24,7 +20,7 @@ func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err er
ch := src[i]
if ch < min || ch > max { // Illegal code point in ASCII mode
err = ErrBadUtf7
err = ErrInvalidUTF7
return
}
@@ -47,14 +43,14 @@ func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err er
start := i + 1
for i++; i < len(src) && src[i] != '-'; i++ {
if src[i] == '\r' || src[i] == '\n' { // base64 package ignores CR and LF
err = ErrBadUtf7
err = ErrInvalidUTF7
return
}
}
if i == len(src) { // Implicit shift ("&...")
if atEOF {
err = ErrBadUtf7
err = ErrInvalidUTF7
} else {
err = transform.ErrShortSrc
}
@@ -67,7 +63,7 @@ func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err er
d.ascii = true
} else { // Control or non-ASCII code points in base64
if !d.ascii { // Null shift ("&...-&...-")
err = ErrBadUtf7
err = ErrInvalidUTF7
return
}
@@ -76,7 +72,7 @@ func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err er
}
if len(b) == 0 { // Bad encoding
err = ErrBadUtf7
err = ErrInvalidUTF7
return
}
@@ -116,16 +112,16 @@ func decode(b64 []byte) []byte {
if n := len(b64); b64[n-1] == '=' {
return nil
} else if n&3 == 0 {
b = make([]byte, enc.DecodedLen(n)*3)
b = make([]byte, b64Enc.DecodedLen(n)*3)
} else {
n += 4 - n&3
b = make([]byte, n+enc.DecodedLen(n)*3)
b = make([]byte, n+b64Enc.DecodedLen(n)*3)
copy(b[copy(b, b64):n], []byte("=="))
b64, b = b[:n], b[n:]
}
// Decode Base64 into the first 1/3rd of b
n, err := enc.Decode(b, b64)
n, err := b64Enc.Decode(b, b64)
if err != nil || n&1 == 1 {
return nil
}

View File

@@ -4,14 +4,9 @@ import (
"unicode/utf16"
"unicode/utf8"
"golang.org/x/text/encoding"
"golang.org/x/text/transform"
)
var Encoder = &encoding.Encoder{
Transformer: &encoder{},
}
type encoder struct{}
func (e *encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
@@ -81,9 +76,9 @@ func encode(s []byte) []byte {
}
// Encode as base64
n := enc.EncodedLen(len(b)) + 2
n := b64Enc.EncodedLen(len(b)) + 2
b64 := make([]byte, n)
enc.Encode(b64[1:], b)
b64Enc.Encode(b64[1:], b)
// Strip padding
n -= 2 - (len(b)+2)%3

View File

@@ -3,6 +3,8 @@ package utf7
import (
"encoding/base64"
"golang.org/x/text/encoding"
)
const (
@@ -12,4 +14,21 @@ const (
repl = '\uFFFD' // Unicode replacement code point
)
var enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,")
var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,")
type enc struct{}
func (e enc) NewDecoder() *encoding.Decoder {
return &encoding.Decoder{
Transformer: &decoder{true},
}
}
func (e enc) NewEncoder() *encoding.Encoder {
return &encoding.Encoder{
Transformer: &encoder{},
}
}
// Encoding is the modified UTF-7 encoding.
var Encoding encoding.Encoding = enc{}

View File

@@ -14,8 +14,12 @@ type flusher interface {
Flush() error
}
// A string that will be quoted.
type Quoted string
type (
// A string that will be quoted.
Quoted string
// A raw atom.
Atom string
)
type WriterTo interface {
WriteTo(w *Writer) error
@@ -83,8 +87,7 @@ func (w *Writer) writeAstring(s string) error {
return w.writeLiteral(bytes.NewBufferString(s))
}
specials := string([]rune{dquote, listStart, listEnd, literalStart, sp})
if strings.ToUpper(s) == nilAtom || s == "" || strings.ContainsAny(s, specials) {
if strings.ToUpper(s) == nilAtom || s == "" || strings.ContainsAny(s, atomSpecials) {
return w.writeQuoted(s)
}
@@ -162,6 +165,8 @@ func (w *Writer) writeField(field interface{}) error {
return w.writeAstring(field)
case Quoted:
return w.writeQuoted(string(field))
case Atom:
return w.writeAtom(string(field))
case int:
return w.writeNumber(uint32(field))
case uint32:
@@ -184,18 +189,18 @@ func (w *Writer) writeField(field interface{}) error {
return w.writeString(field.String())
case *BodySectionName:
// Can contain spaces - that's why we don't just pass it as a string
return w.writeString(field.String())
return w.writeString(string(field.FetchItem()))
}
return fmt.Errorf("imap: cannot format field: %v", field)
}
func (w *Writer) writeRespCode(code string, args []interface{}) error {
func (w *Writer) writeRespCode(code StatusRespCode, args []interface{}) error {
if err := w.writeString(string(respCodeStart)); err != nil {
return err
}
fields := []interface{}{code}
fields := []interface{}{string(code)}
fields = append(fields, args...)
if err := w.writeFields(fields); err != nil {