mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-17 23:56:56 +00:00
We build too many walls and not enough bridges
This commit is contained in:
198
internal/imap/uidplus/extension.go
Normal file
198
internal/imap/uidplus/extension.go
Normal file
@ -0,0 +1,198 @@
|
||||
// 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 (
|
||||
"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 successful"
|
||||
appendSucess = "APPEND successful"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("pkg", "impa/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 has no effect because Bridge is not
|
||||
// using EXPUNGE at all. The message is deleted right after it was flagged as
|
||||
// \Deleted Bridge should simply ignore this command with empty `OK` response.
|
||||
//
|
||||
// If not implemented it would cause harmless IMAP error.
|
||||
//
|
||||
// This overrides the standard EXPUNGE functionality.
|
||||
type UIDExpunge struct{}
|
||||
|
||||
func (e *UIDExpunge) Parse(fields []interface{}) error { log.Traceln("parse", fields); return nil }
|
||||
func (e *UIDExpunge) Handle(conn server.Conn) error { log.Traceln("handle"); return nil }
|
||||
func (e *UIDExpunge) UidHandle(conn server.Conn) error { log.Traceln("uid handle"); return nil } //nolint[golint]
|
||||
|
||||
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 == imap.Expunge {
|
||||
return func() server.Handler {
|
||||
return &UIDExpunge{}
|
||||
}
|
||||
}
|
||||
|
||||
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.StatusOk,
|
||||
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.StatusOk,
|
||||
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))
|
||||
}
|
||||
108
internal/imap/uidplus/extension_test.go
Normal file
108
internal/imap/uidplus/extension_test.go
Normal file
@ -0,0 +1,108 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// uidValidity is constant and global for bridge IMAP.
|
||||
const uidValidity = 66
|
||||
|
||||
type testResponseData struct {
|
||||
sourceList, targetList []int
|
||||
expCopyInfo, expAppendInfo string
|
||||
}
|
||||
|
||||
func (td *testResponseData) getOrdSeqFromList(seqList []int) *OrderedSeq {
|
||||
set := &OrderedSeq{}
|
||||
for _, seq := range seqList {
|
||||
set.Add(uint32(seq))
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func (td *testResponseData) testCopyAndAppendResponses(tb testing.TB) {
|
||||
sourceSeq := td.getOrdSeqFromList(td.sourceList)
|
||||
targetSeq := td.getOrdSeqFromList(td.targetList)
|
||||
|
||||
gotCopyResp := getStatusResponseCopy(uidValidity, sourceSeq, targetSeq)
|
||||
assert.Equal(tb, td.expCopyInfo, gotCopyResp.Info, "source: %v\ntarget: %v", td.sourceList, td.targetList)
|
||||
|
||||
gotAppendResp := getStatusResponseAppend(uidValidity, targetSeq)
|
||||
assert.Equal(tb, td.expAppendInfo, gotAppendResp.Info, "source: %v\ntarget: %v", td.sourceList, td.targetList)
|
||||
}
|
||||
|
||||
func TestStatusResponseInfo(t *testing.T) {
|
||||
testData := []*testResponseData{
|
||||
{ // Dynamic range must never be returned e.g 4:* (explicitly true if you OrderedSeq used instead of imap.SeqSet).
|
||||
sourceList: []int{4, 5, 6},
|
||||
targetList: []int{1, 2, 3},
|
||||
expCopyInfo: "[" + copyuid + " 66 4:6 1:3] " + copySuccess,
|
||||
expAppendInfo: "[" + appenduid + " 66 1:3] " + appendSucess,
|
||||
},
|
||||
{ // Ranges can be used only for consecutive strictly rising sequence.
|
||||
sourceList: []int{6, 7, 8, 9, 10, 1, 3, 5, 10, 11, 20, 21, 30, 31},
|
||||
targetList: []int{1, 2, 3, 4, 50, 8, 7, 6, 12, 13, 22, 23, 32, 33},
|
||||
expCopyInfo: "[" + copyuid + " 66 6:10,1,3,5,10:11,20:21,30:31 1:4,50,8,7,6,12:13,22:23,32:33] " + copySuccess,
|
||||
expAppendInfo: "[" + appenduid + " 66 1:4,50,8,7,6,12:13,22:23,32:33] " + appendSucess,
|
||||
},
|
||||
{ // Keep order (cannot use sequence set because 3,2,1 equals 1,2,3 equals 1:3 equals 3:1).
|
||||
sourceList: []int{4, 5, 8},
|
||||
targetList: []int{3, 2, 1},
|
||||
expCopyInfo: "[" + copyuid + " 66 4:5,8 3,2,1] " + copySuccess,
|
||||
expAppendInfo: "[" + appenduid + " 66 3,2,1] " + appendSucess,
|
||||
},
|
||||
{ // Incorrect count of source and target uids is wrong and we should not report it.
|
||||
sourceList: []int{1},
|
||||
targetList: []int{1, 2, 3},
|
||||
expCopyInfo: copySuccess,
|
||||
expAppendInfo: "[" + appenduid + " 66 1:3] " + appendSucess,
|
||||
},
|
||||
{
|
||||
sourceList: []int{1, 2, 3},
|
||||
targetList: []int{1},
|
||||
expCopyInfo: copySuccess,
|
||||
expAppendInfo: "[" + appenduid + " 66 1] " + appendSucess,
|
||||
},
|
||||
{ // One item should be always interpreted as one number (don't use imap.SeqSet because 1:1 means 1).
|
||||
sourceList: []int{1},
|
||||
targetList: []int{1},
|
||||
expCopyInfo: "[" + copyuid + " 66 1 1] " + copySuccess,
|
||||
expAppendInfo: "[" + appenduid + " 66 1] " + appendSucess,
|
||||
},
|
||||
{ // No UID is wrong we should not report it.
|
||||
sourceList: []int{1},
|
||||
targetList: []int{},
|
||||
expCopyInfo: copySuccess,
|
||||
expAppendInfo: appendSucess,
|
||||
},
|
||||
{ // Duplicates should be reported as list.
|
||||
sourceList: []int{1, 1, 1},
|
||||
targetList: []int{6, 6, 6},
|
||||
expCopyInfo: "[" + copyuid + " 66 1,1,1 6,6,6] " + copySuccess,
|
||||
expAppendInfo: "[" + appenduid + " 66 6,6,6] " + appendSucess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, td := range testData {
|
||||
td.testCopyAndAppendResponses(t)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user