Files
proton-bridge/internal/imap/uidplus/extension.go
2020-09-16 09:51:57 +00:00

225 lines
5.9 KiB
Go

// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package uidplus DOES NOT implement full RFC4315!
//
// Excluded parts are:
// * Response `UIDNOTSTICKY`: All mailboxes of Bridge support stable
// UIDVALIDITY so it would never return this response
//
// Otherwise the standard RFC4315 is followed.
package uidplus
import (
"errors"
"fmt"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/server"
"github.com/sirupsen/logrus"
)
// Capability extension identifier
const Capability = "UIDPLUS"
const (
copyuid = "COPYUID"
appenduid = "APPENDUID"
copySuccess = "COPY completed"
appendSucess = "APPEND completed"
)
var log = logrus.WithField("pkg", "imap/uidplus") //nolint[gochecknoglobals]
// OrderedSeq to remember Seq in order they are added.
// We didn't find any restriction in RFC that server must respond with ranges
// so we decided to always do explicit list. This makes sure that no dynamic
// ranges or out of the bound ranges are possible.
//
// NOTE: potential issue with response length
// * the user selects large number of messages to be copied and the
// response line will be long,
// * list of UIDs which high values
// which can create long response line. We didn't find a maximum length of one
// IMAP response line or maximum length of IMAP "response code" with parameters.
type OrderedSeq []uint32
// Len return number of added seq numbers.
func (os OrderedSeq) Len() int { return len(os) }
// Add number to sequence. Zero is not acceptable UID and it won't be added to list.
func (os *OrderedSeq) Add(num uint32) {
if num == 0 {
return
}
*os = append(*os, num)
}
func (os *OrderedSeq) String() string {
out := ""
if len(*os) == 0 {
return out
}
lastS := uint32(0)
isRangeOpened := false
for i, s := range *os {
// write first
if i == 0 {
out += fmt.Sprintf("%d", s)
isRangeOpened = false
lastS = s
continue
}
isLast := (i == len(*os)-1)
isContinuous := (lastS+1 == s)
if isContinuous {
isRangeOpened = true
lastS = s
if isLast {
out += fmt.Sprintf(":%d", s)
}
continue
}
if isRangeOpened && !isContinuous { // close range
out += fmt.Sprintf(":%d,%d", lastS, s)
isRangeOpened = false
lastS = s
continue
}
// Range is not opened and it is not continuous.
out += fmt.Sprintf(",%d", s)
isRangeOpened = false
lastS = s
}
return out
}
// UIDExpunge implements server.Handler but Bridge is not supporting
// UID EXPUNGE with specific UIDs.
type UIDExpunge struct {
expunge *server.Expunge
}
func newUIDExpunge() *UIDExpunge {
return &UIDExpunge{expunge: &server.Expunge{}}
}
func (e *UIDExpunge) Parse(fields []interface{}) error {
if len(fields) < 1 {
return e.expunge.Parse(fields)
}
// RFC4315#section-2.1
// The UID EXPUNGE command permanently removes all messages that both
// have the \Deleted flag set and have a UID that is included in the
// specified sequence set from the currently selected mailbox. If a
// message either does not have the \Deleted flag set or has a UID
// that is not included in the specified sequence set, it is not
// affected.
//
// Current implementation supports only deletion of all messages
// marked as deleted. It will probably need mailbox interface change:
// ExpungeUIDs(seqSet). Not sure how to combine with original
// e.expunge.Handle().
return errors.New("UID EXPUNGE with UIDs is not supported")
}
func (e *UIDExpunge) Handle(conn server.Conn) error {
return e.expunge.Handle(conn)
}
func (e *UIDExpunge) UidHandle(conn server.Conn) error { //nolint[golint]
return e.expunge.Handle(conn)
}
type extension struct{}
// NewExtension of UIDPLUS.
func NewExtension() server.Extension {
return &extension{}
}
func (ext *extension) Capabilities(c server.Conn) []string {
if c.Context().State&imap.AuthenticatedState != 0 {
return []string{Capability}
}
return nil
}
func (ext *extension) Command(name string) server.HandlerFactory {
if name == "EXPUNGE" {
return func() server.Handler {
return newUIDExpunge()
}
}
return nil
}
func getStatusResponseCopy(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq) *imap.StatusResp {
info := copySuccess
if sourceSeq.Len() != 0 && targetSeq.Len() != 0 &&
sourceSeq.Len() == targetSeq.Len() {
info = fmt.Sprintf("[%s %d %s %s] %s",
copyuid,
uidValidity,
sourceSeq.String(),
targetSeq.String(),
copySuccess,
)
}
return &imap.StatusResp{
Type: imap.StatusRespOk,
Info: info,
}
}
// CopyResponse prepares OK response with extended UID information about copied message.
func CopyResponse(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq) error {
return server.ErrStatusResp(getStatusResponseCopy(uidValidity, sourceSeq, targetSeq))
}
func getStatusResponseAppend(uidValidity uint32, targetSeq *OrderedSeq) *imap.StatusResp {
info := appendSucess
if targetSeq.Len() > 0 {
info = fmt.Sprintf("[%s %d %s] %s",
appenduid,
uidValidity,
targetSeq.String(),
appendSucess,
)
}
return &imap.StatusResp{
Type: imap.StatusRespOk,
Info: info,
}
}
// AppendResponse prepares OK response with extended UID information about appended message.
func AppendResponse(uidValidity uint32, targetSeq *OrderedSeq) error {
return server.ErrStatusResp(getStatusResponseAppend(uidValidity, targetSeq))
}