// 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 . // 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 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 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 == "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.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)) }