mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5321f8993 | |||
| bcf799732f | |||
| 13ba2182c2 | |||
| 0d25c607e7 | |||
| b3f8866ef7 | |||
| 9bb16dec48 | |||
| bdb35f1c1d | |||
| d421b5aa5a | |||
| 1ec05e8a6c | |||
| 5b941013de | |||
| a93ed35eee | |||
| 76469969f3 | |||
| 8b39ea4acb | |||
| 252ca9a5f9 | |||
| c4eb1a0f5b | |||
| 1e2f4e9ebb | |||
| 2a7aefac45 | |||
| ea39e2d842 | |||
| fc5879a204 | |||
| 5ae2229e37 | |||
| 12e5ce0ff0 | |||
| 5ef3774d11 | |||
| 654e816e6b | |||
| 7cad7bcddb | |||
| 136d514cf7 | |||
| 6e48345d54 | |||
| 8ebdb466f7 | |||
| 1ed7b690a5 | |||
| 5c28a3eda7 |
@ -108,7 +108,8 @@ test-integration:
|
||||
dependency-updates:
|
||||
stage: test
|
||||
script:
|
||||
- make updates
|
||||
- "echo 'NOTE: Do not run on go1.15 ( 'if...' can be removed once fully updated to go1.18)'"
|
||||
- if [ 18 -le $(go version | cut -d. -f2 | cut -d " " -f1) ]; then make updates; fi
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
@ -246,10 +247,11 @@ check-gobinsec:
|
||||
before_script:
|
||||
- mkdir build
|
||||
- tar -xzf bridge_linux_*.tgz -C build
|
||||
- "echo api-key: \"${GOBINSEC_NVD_API_KEY}\" >> utils/gobinsec_conf.yml"
|
||||
script:
|
||||
- "[ ! -f ./gobinsec-cache.yml ] && wget bridgeteam.protontech.ch/bridgeteam/gobinsec-cache.yml"
|
||||
- cat ./gobinsec-cache.yml
|
||||
- gobinsec -cache -config utils/gobinsec_conf.yml build/proton-bridge
|
||||
- gobinsec -wait -cache -config utils/gobinsec_conf.yml build/proton-bridge
|
||||
|
||||
|
||||
|
||||
|
||||
21
Changelog.md
21
Changelog.md
@ -2,6 +2,27 @@
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## [Bridge 2.3.0] Nihonbashi
|
||||
|
||||
### Added
|
||||
* GODT-1739: Opt-out All Mail visibility in settings file.
|
||||
* GODT-1794: CLI wording.
|
||||
* GODT-1794: Add confirmation dialog and change wording.
|
||||
* GODT-1741: GUI and CLI settings to change visibility of All Mail folder.
|
||||
* GODT-1740: Opt-out All Mail visibility in settings file.
|
||||
|
||||
### Changed
|
||||
* GODT-1737: Improve logging during import.
|
||||
* GODT-1754: Add logs for unilateral updates and SEARCH.
|
||||
|
||||
### Fixed
|
||||
* GODT-1840: Use Safe map for mailboxID cache.
|
||||
* GODT-1795: Fix automatic installation of profile for AppleMail on macOS Ventura beta (qt 5).
|
||||
* GODT-1833: Fix gobinsec cache.
|
||||
* GODT-1799: Fix dependency link.
|
||||
* Other: Update SSL certificate fingerprint for test.
|
||||
|
||||
|
||||
## [Bridge 2.2.2] Millau
|
||||
|
||||
### Added
|
||||
|
||||
4
Makefile
4
Makefile
@ -10,7 +10,7 @@ TARGET_OS?=${GOOS}
|
||||
.PHONY: build build-nogui build-launcher versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=2.2.2+git
|
||||
BRIDGE_APP_VERSION?=2.3.0+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
SRC_ICO:=bridge.ico
|
||||
SRC_ICNS:=Bridge.icns
|
||||
@ -166,7 +166,7 @@ update-qt-docs:
|
||||
LINTVER:="v1.39.0"
|
||||
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
||||
|
||||
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
|
||||
install-dev-dependencies: install-devel-tools install-linter
|
||||
|
||||
install-devel-tools: check-has-go
|
||||
go get -v github.com/golang/mock/gomock
|
||||
|
||||
@ -24,6 +24,7 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
|
||||
@ -57,8 +58,9 @@ func (api *apiServer) ListenAndServe() {
|
||||
|
||||
addr := api.getAddress()
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second, // fix gosec G112 (vulnerability to [Slowloris](https://www.cloudflare.com/en-gb/learning/ddos/ddos-attack-tools/slowloris/) attack).
|
||||
}
|
||||
|
||||
log.Info("API listening at ", addr)
|
||||
|
||||
@ -57,8 +57,9 @@ type Bridge struct {
|
||||
// Bridge's global errors list.
|
||||
errors []error
|
||||
|
||||
isFirstStart bool
|
||||
lastVersion string
|
||||
isAllMailVisible bool
|
||||
isFirstStart bool
|
||||
lastVersion string
|
||||
}
|
||||
|
||||
func New(
|
||||
@ -92,15 +93,16 @@ func New(
|
||||
)
|
||||
|
||||
b := &Bridge{
|
||||
Users: u,
|
||||
locations: locations,
|
||||
settings: setting,
|
||||
clientManager: clientManager,
|
||||
updater: updater,
|
||||
versioner: versioner,
|
||||
cacheProvider: cacheProvider,
|
||||
autostart: autostart,
|
||||
isFirstStart: false,
|
||||
Users: u,
|
||||
locations: locations,
|
||||
settings: setting,
|
||||
clientManager: clientManager,
|
||||
updater: updater,
|
||||
versioner: versioner,
|
||||
cacheProvider: cacheProvider,
|
||||
autostart: autostart,
|
||||
isFirstStart: false,
|
||||
isAllMailVisible: setting.GetBool(settings.IsAllMailVisible),
|
||||
}
|
||||
|
||||
if setting.GetBool(settings.FirstStartKey) {
|
||||
@ -302,3 +304,14 @@ func (b *Bridge) GetLastVersion() string {
|
||||
func (b *Bridge) IsFirstStart() bool {
|
||||
return b.isFirstStart
|
||||
}
|
||||
|
||||
// IsAllMailVisible can be called extensively by IMAP. Therefore, it is better
|
||||
// to cache the value instead of reading from settings file.
|
||||
func (b *Bridge) IsAllMailVisible() bool {
|
||||
return b.isAllMailVisible
|
||||
}
|
||||
|
||||
func (b *Bridge) SetIsAllMailVisible(isVisible bool) {
|
||||
b.settings.SetBool(settings.IsAllMailVisible, isVisible)
|
||||
b.isAllMailVisible = isVisible
|
||||
}
|
||||
|
||||
@ -55,6 +55,7 @@ const (
|
||||
AttachmentWorkers = "attachment_workers"
|
||||
ColorScheme = "color_scheme"
|
||||
RebrandingMigrationKey = "rebranding_migrated"
|
||||
IsAllMailVisible = "is_all_mail_visible"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
@ -110,4 +111,6 @@ func (s *Settings) setDefaultValues() {
|
||||
|
||||
// By default, stick to STARTTLS. If the user uses catalina+applemail they'll have to change to SSL.
|
||||
s.setDefault(SMTPSSLKey, "false")
|
||||
|
||||
s.setDefault(IsAllMailVisible, "true")
|
||||
}
|
||||
|
||||
@ -24,18 +24,24 @@ import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
// IsCatalinaOrNewer checks whether the host is MacOS Catalina 10.15.x or higher.
|
||||
// IsCatalinaOrNewer checks whether the host is macOS Catalina 10.15.x or higher.
|
||||
func IsCatalinaOrNewer() bool {
|
||||
return isThisDarwinNewerOrEqual(getMinCatalina())
|
||||
}
|
||||
|
||||
// IsBigSurOrNewer checks whether the host is MacOS BigSur 10.16.x or higher.
|
||||
// IsBigSurOrNewer checks whether the host is macOS BigSur 10.16.x or higher.
|
||||
func IsBigSurOrNewer() bool {
|
||||
return isThisDarwinNewerOrEqual(getMinBigSur())
|
||||
}
|
||||
|
||||
// IsVenturaOrNewer checks whether the host is macOS BigSur 13.x or higher.
|
||||
func IsVenturaOrNewer() bool {
|
||||
return isThisDarwinNewerOrEqual(getMinVentura())
|
||||
}
|
||||
|
||||
func getMinCatalina() *semver.Version { return semver.MustParse("19.0.0") }
|
||||
func getMinBigSur() *semver.Version { return semver.MustParse("20.0.0") }
|
||||
func getMinVentura() *semver.Version { return semver.MustParse("22.0.0") }
|
||||
|
||||
func isThisDarwinNewerOrEqual(minVersion *semver.Version) bool {
|
||||
if runtime.GOOS != "darwin" {
|
||||
|
||||
@ -137,6 +137,23 @@ func New( //nolint:funlen
|
||||
})
|
||||
fe.AddCmd(dohCmd)
|
||||
|
||||
// All mail visibility commands.
|
||||
allMailCmd := &ishell.Cmd{
|
||||
Name: "all-mail-visibility",
|
||||
Help: "choose not to list the All Mail folder in your local client",
|
||||
}
|
||||
allMailCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "hide",
|
||||
Help: "All Mail folder will not be listed in your local client",
|
||||
Func: fe.hideAllMail,
|
||||
})
|
||||
allMailCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "show",
|
||||
Help: "All Mail folder will be listed in your local client",
|
||||
Func: fe.showAllMail,
|
||||
})
|
||||
fe.AddCmd(allMailCmd)
|
||||
|
||||
// Cache-On-Disk commands.
|
||||
codCmd := &ishell.Cmd{
|
||||
Name: "local-cache",
|
||||
|
||||
@ -152,6 +152,32 @@ func (f *frontendCLI) disallowProxy(c *ishell.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) hideAllMail(c *ishell.Context) {
|
||||
if !f.bridge.IsAllMailVisible() {
|
||||
f.Println("All Mail folder is not listed in your local client.")
|
||||
return
|
||||
}
|
||||
|
||||
f.Println("All Mail folder is listed in your client right now.")
|
||||
|
||||
if f.yesNoQuestion("Do you want to hide All Mail folder") {
|
||||
f.bridge.SetIsAllMailVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) showAllMail(c *ishell.Context) {
|
||||
if f.bridge.IsAllMailVisible() {
|
||||
f.Println("All Mail folder is listed in your local client.")
|
||||
return
|
||||
}
|
||||
|
||||
f.Println("All Mail folder is not listed in your client right now.")
|
||||
|
||||
if f.yesNoQuestion("Do you want to show All Mail folder") {
|
||||
f.bridge.SetIsAllMailVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) enableCacheOnDisk(c *ishell.Context) {
|
||||
if f.settings.GetBool(settings.CacheEnabledKey) {
|
||||
f.Println("The local cache is already enabled.")
|
||||
|
||||
@ -36,7 +36,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
bigSurPreferncesPane = "/System/Library/PreferencePanes/Profiles.prefPane"
|
||||
bigSurPreferencesPane = "/System/Library/PreferencePanes/Profiles.prefPane"
|
||||
venturaPreferencesPane = "x-apple.systempreferences:com.apple.preferences.configurationprofiles"
|
||||
)
|
||||
|
||||
func init() { //nolint:gochecknoinit
|
||||
@ -56,7 +57,13 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
|
||||
}
|
||||
|
||||
if useragent.IsBigSurOrNewer() {
|
||||
return execabs.Command("open", bigSurPreferncesPane, confPath).Run() //nolint:gosec G204: open command is safe, mobileconfig is generated by us
|
||||
prefPane := bigSurPreferencesPane
|
||||
|
||||
if useragent.IsVenturaOrNewer() {
|
||||
prefPane = venturaPreferencesPane
|
||||
}
|
||||
|
||||
return execabs.Command("open", prefPane, confPath).Run() //nolint:gosec // G204 open command is safe, mobileconfig is generated by us
|
||||
}
|
||||
|
||||
return execabs.Command("open", confPath).Run() //nolint:gosec G204: open command is safe, mobileconfig is generated by us
|
||||
|
||||
@ -672,6 +672,10 @@ Window {
|
||||
Label {colorScheme: root.colorScheme; text: "DoH:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isDoHEnabled; onClicked: root.isDoHEnabled = !root.isDoHEnabled}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "All Mail disabled:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isAllMailVisible; onClicked: root.isAllMailVisible = !root.isAllMailVisible}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Ports:"}
|
||||
TextField {
|
||||
@ -811,6 +815,13 @@ Window {
|
||||
root.isDoHEnabled = makeItActive
|
||||
}
|
||||
|
||||
property bool isAllMailVisible : true
|
||||
function changeIsAllMailVisible(isVisible){
|
||||
console.debug("-> All Mail Visible", isVisible, root.isAllMailVisible)
|
||||
root.isAllMailVisible = isVisible
|
||||
}
|
||||
|
||||
|
||||
property bool useSSLforSMTP: false
|
||||
function toggleUseSSLforSMTP(makeItActive){
|
||||
console.debug("-> SMTP SSL", makeItActive, root.useSSLforSMTP)
|
||||
|
||||
@ -156,6 +156,19 @@ SettingsView {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
id: allMail
|
||||
visible: root._isAdvancedShown
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Show All Mail")
|
||||
description: qsTr("Choose to list the All Mail folder in your local client.")
|
||||
type: SettingsItem.Toggle
|
||||
checked: root.backend.isAllMailVisible
|
||||
onClicked: root.notifications.askChangeAllMailVisibility(root.backend.isAllMailVisible)
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
id: ports
|
||||
visible: root._isAdvancedShown
|
||||
|
||||
@ -110,6 +110,11 @@ Item {
|
||||
notification: root.notifications.resetBridge
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.changeAllMailVisibility
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.deleteAccount
|
||||
|
||||
@ -34,6 +34,7 @@ QtObject {
|
||||
signal askDisableLocalCache()
|
||||
signal askEnableLocalCache(var path)
|
||||
signal askResetBridge()
|
||||
signal askChangeAllMailVisibility(var isVisibleNow)
|
||||
signal askDeleteAccount(var user)
|
||||
|
||||
enum Group {
|
||||
@ -72,6 +73,7 @@ QtObject {
|
||||
root.disableLocalCache,
|
||||
root.enableLocalCache,
|
||||
root.resetBridge,
|
||||
root.changeAllMailVisibility,
|
||||
root.deleteAccount,
|
||||
root.noKeychain,
|
||||
root.rebuildKeychain,
|
||||
@ -840,6 +842,47 @@ QtObject {
|
||||
]
|
||||
}
|
||||
|
||||
property Notification changeAllMailVisibility: Notification {
|
||||
title: root.changeAllMailVisibility.isVisibleNow ?
|
||||
qsTr("Hide All Mail folder?") :
|
||||
qsTr("Show All Mail folder?")
|
||||
brief: title
|
||||
icon: "./icons/ic-info-circle-filled.svg"
|
||||
description: qsTr("Switching between showing and hiding the All Mail folder will require you to restart your client.")
|
||||
type: Notification.NotificationType.Info
|
||||
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
||||
|
||||
property var isVisibleNow
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
onAskChangeAllMailVisibility: {
|
||||
root.changeAllMailVisibility.isVisibleNow = isVisibleNow
|
||||
root.changeAllMailVisibility.active = true
|
||||
}
|
||||
}
|
||||
|
||||
action: [
|
||||
Action {
|
||||
id: allMail_change
|
||||
text: root.changeAllMailVisibility.isVisibleNow ?
|
||||
qsTr("Hide All Mail folder") :
|
||||
qsTr("Show All Mail folder")
|
||||
onTriggered: {
|
||||
root.backend.changeIsAllMailVisible(!root.changeAllMailVisibility.isVisibleNow)
|
||||
root.changeAllMailVisibility.active = false
|
||||
}
|
||||
},
|
||||
Action {
|
||||
id: allMail_cancel
|
||||
text: qsTr("Cancel")
|
||||
onTriggered: {
|
||||
root.changeAllMailVisibility.active = false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
property Notification deleteAccount: Notification {
|
||||
title: qsTr("Remove this account?")
|
||||
brief: title
|
||||
|
||||
@ -155,6 +155,9 @@ type QMLBackend struct {
|
||||
_ func() `signal:apiCertIssue`
|
||||
|
||||
_ func(userID string) `signal:userChanged`
|
||||
|
||||
_ bool `property:"isAllMailVisible"`
|
||||
_ func(isDisabled bool) `slot:"changeIsAllMailVisible"`
|
||||
}
|
||||
|
||||
func (q *QMLBackend) setup(f *FrontendQt) {
|
||||
@ -304,4 +307,11 @@ func (q *QMLBackend) setup(f *FrontendQt) {
|
||||
f.changeKeychain(k)
|
||||
}()
|
||||
})
|
||||
|
||||
q.SetIsAllMailVisible(f.bridge.IsAllMailVisible())
|
||||
q.ConnectChangeIsAllMailVisible(func(isVisible bool) {
|
||||
f.bridge.SetIsAllMailVisible(isVisible)
|
||||
f.qml.SetIsAllMailVisible(isVisible)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@ -92,6 +92,8 @@ type Bridger interface {
|
||||
DisableAutostart() error
|
||||
GetLastVersion() string
|
||||
IsFirstStart() bool
|
||||
IsAllMailVisible() bool
|
||||
SetIsAllMailVisible(bool)
|
||||
}
|
||||
|
||||
type bridgeWrap struct {
|
||||
|
||||
@ -93,10 +93,9 @@ func newIMAPBackend(
|
||||
eventListener listener.Listener,
|
||||
listWorkers int,
|
||||
) *imapBackend {
|
||||
return &imapBackend{
|
||||
ib := &imapBackend{
|
||||
panicHandler: panicHandler,
|
||||
bridge: bridge,
|
||||
updates: newIMAPUpdates(),
|
||||
eventListener: eventListener,
|
||||
|
||||
users: map[string]*imapUser{},
|
||||
@ -106,6 +105,8 @@ func newIMAPBackend(
|
||||
imapCacheLock: &sync.RWMutex{},
|
||||
listWorkers: listWorkers,
|
||||
}
|
||||
ib.updates = newIMAPUpdates(ib)
|
||||
return ib
|
||||
}
|
||||
|
||||
func (ib *imapBackend) getUser(address string) (*imapUser, error) {
|
||||
|
||||
@ -31,6 +31,7 @@ type cacheProvider interface {
|
||||
type bridger interface {
|
||||
GetUser(query string) (bridgeUser, error)
|
||||
HasError(err error) bool
|
||||
IsAllMailVisible() bool
|
||||
}
|
||||
|
||||
type bridgeUser interface {
|
||||
|
||||
@ -197,7 +197,7 @@ func (im *imapMailbox) labelExistingMessage(msg storeMessageProvider) error { //
|
||||
}
|
||||
|
||||
func (im *imapMailbox) importMessage(kr *crypto.KeyRing, hdr textproto.Header, body []byte, imapFlags []string, date time.Time) error { //nolint:funlen
|
||||
im.log.Info("Importing external message")
|
||||
im.log.WithField("size", len(body)).Info("Importing external message")
|
||||
|
||||
var (
|
||||
seen bool
|
||||
@ -251,6 +251,7 @@ func (im *imapMailbox) importMessage(kr *crypto.KeyRing, hdr textproto.Header, b
|
||||
|
||||
messageID, err := targetMailbox.ImportMessage(enc, seen, labelIDs, flags, time)
|
||||
if err != nil {
|
||||
log.WithField("enc.size", len(enc)).Error("Import failed")
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -332,7 +332,16 @@ func (im *imapMailbox) labelMessages(uid bool, seqSet *imap.SeqSet, targetLabel
|
||||
|
||||
// SearchMessages searches messages. The returned list must contain UIDs if
|
||||
// uid is set to true, or sequence numbers otherwise.
|
||||
func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { //nolint:gocyclo,funlen
|
||||
func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria) (ids []uint32, err error) {
|
||||
err = im.logCommand(func() error {
|
||||
var searchError error
|
||||
ids, searchError = im.searchMessages(isUID, criteria)
|
||||
return searchError
|
||||
}, "SEARCH", isUID, criteria.Format())
|
||||
return ids, err
|
||||
}
|
||||
|
||||
func (im *imapMailbox) searchMessages(isUID bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { //nolint:gocyclo,funlen
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
|
||||
44
internal/imap/map.go
Normal file
44
internal/imap/map.go
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail 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.
|
||||
//
|
||||
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package imap
|
||||
|
||||
import "sync"
|
||||
|
||||
type safeMapOfStrings struct {
|
||||
data map[string]string
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func newSafeMapOfString() safeMapOfStrings {
|
||||
return safeMapOfStrings{
|
||||
data: map[string]string{},
|
||||
mutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *safeMapOfStrings) get(key string) string {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.data[key]
|
||||
}
|
||||
|
||||
func (m *safeMapOfStrings) set(key, value string) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.data[key] = value
|
||||
}
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/algo"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
||||
imap "github.com/emersion/go-imap"
|
||||
@ -42,14 +43,16 @@ type imapUpdates struct {
|
||||
blocking map[string]bool
|
||||
delayedExpunges map[string][]chan struct{}
|
||||
ch chan goIMAPBackend.Update
|
||||
ib *imapBackend
|
||||
}
|
||||
|
||||
func newIMAPUpdates() *imapUpdates {
|
||||
func newIMAPUpdates(ib *imapBackend) *imapUpdates {
|
||||
return &imapUpdates{
|
||||
lock: &sync.Mutex{},
|
||||
blocking: map[string]bool{},
|
||||
delayedExpunges: map[string][]chan struct{}{},
|
||||
ch: make(chan goIMAPBackend.Update),
|
||||
ib: ib,
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +116,8 @@ func (iu *imapUpdates) CanDelete(mailboxID string) (bool, func()) {
|
||||
}
|
||||
|
||||
func (iu *imapUpdates) Notice(address, notice string) {
|
||||
l := iu.updateLog(address, "")
|
||||
l.Info("Notice")
|
||||
update := new(goIMAPBackend.StatusUpdate)
|
||||
update.Update = goIMAPBackend.NewUpdate(address, "")
|
||||
update.StatusResp = &imap.StatusResp{
|
||||
@ -120,7 +125,7 @@ func (iu *imapUpdates) Notice(address, notice string) {
|
||||
Code: imap.CodeAlert,
|
||||
Info: notice,
|
||||
}
|
||||
iu.sendIMAPUpdate(update, false)
|
||||
iu.sendIMAPUpdate(l, update, false)
|
||||
}
|
||||
|
||||
func (iu *imapUpdates) UpdateMessage(
|
||||
@ -128,14 +133,14 @@ func (iu *imapUpdates) UpdateMessage(
|
||||
uid, sequenceNumber uint32,
|
||||
msg *pmapi.Message, hasDeletedFlag bool,
|
||||
) {
|
||||
log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
"seqNum": sequenceNumber,
|
||||
"uid": uid,
|
||||
"flags": message.GetFlags(msg),
|
||||
"deleted": hasDeletedFlag,
|
||||
}).Trace("IDLE update")
|
||||
l := iu.updateLog(address, mailboxName).
|
||||
WithFields(logrus.Fields{
|
||||
"seqNum": sequenceNumber,
|
||||
"uid": uid,
|
||||
"flags": message.GetFlags(msg),
|
||||
"deleted": hasDeletedFlag,
|
||||
})
|
||||
l.Info("IDLE update")
|
||||
update := new(goIMAPBackend.MessageUpdate)
|
||||
update.Update = goIMAPBackend.NewUpdate(address, mailboxName)
|
||||
update.Message = imap.NewMessage(sequenceNumber, []imap.FetchItem{imap.FetchFlags, imap.FetchUid})
|
||||
@ -144,26 +149,22 @@ func (iu *imapUpdates) UpdateMessage(
|
||||
update.Message.Flags = append(update.Message.Flags, imap.DeletedFlag)
|
||||
}
|
||||
update.Message.Uid = uid
|
||||
iu.sendIMAPUpdate(update, iu.isBlocking(address, mailboxName, operationUpdateMessage))
|
||||
iu.sendIMAPUpdate(l, update, iu.isBlocking(address, mailboxName, operationUpdateMessage))
|
||||
}
|
||||
|
||||
func (iu *imapUpdates) DeleteMessage(address, mailboxName string, sequenceNumber uint32) {
|
||||
log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
"seqNum": sequenceNumber,
|
||||
}).Trace("IDLE delete")
|
||||
l := iu.updateLog(address, mailboxName).
|
||||
WithField("seqNum", sequenceNumber)
|
||||
l.Info("IDLE delete")
|
||||
update := new(goIMAPBackend.ExpungeUpdate)
|
||||
update.Update = goIMAPBackend.NewUpdate(address, mailboxName)
|
||||
update.SeqNum = sequenceNumber
|
||||
iu.sendIMAPUpdate(update, iu.isBlocking(address, mailboxName, operationDeleteMessage))
|
||||
iu.sendIMAPUpdate(l, update, iu.isBlocking(address, mailboxName, operationDeleteMessage))
|
||||
}
|
||||
|
||||
func (iu *imapUpdates) MailboxCreated(address, mailboxName string) {
|
||||
log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
}).Trace("IDLE mailbox info")
|
||||
l := iu.updateLog(address, mailboxName)
|
||||
l.Info("IDLE mailbox info")
|
||||
update := new(goIMAPBackend.MailboxInfoUpdate)
|
||||
update.Update = goIMAPBackend.NewUpdate(address, "")
|
||||
update.MailboxInfo = &imap.MailboxInfo{
|
||||
@ -171,29 +172,30 @@ func (iu *imapUpdates) MailboxCreated(address, mailboxName string) {
|
||||
Delimiter: store.PathDelimiter,
|
||||
Name: mailboxName,
|
||||
}
|
||||
iu.sendIMAPUpdate(update, false)
|
||||
iu.sendIMAPUpdate(l, update, false)
|
||||
}
|
||||
|
||||
func (iu *imapUpdates) MailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint32) {
|
||||
log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
"total": total,
|
||||
"unread": unread,
|
||||
"unreadSeqNum": unreadSeqNum,
|
||||
}).Trace("IDLE status")
|
||||
l := iu.updateLog(address, mailboxName).
|
||||
WithFields(logrus.Fields{
|
||||
"total": total,
|
||||
"unread": unread,
|
||||
"unreadSeqNum": unreadSeqNum,
|
||||
})
|
||||
l.Info("IDLE status")
|
||||
update := new(goIMAPBackend.MailboxUpdate)
|
||||
update.Update = goIMAPBackend.NewUpdate(address, mailboxName)
|
||||
update.MailboxStatus = imap.NewMailboxStatus(mailboxName, []imap.StatusItem{imap.StatusMessages, imap.StatusUnseen})
|
||||
update.MailboxStatus.Messages = total
|
||||
update.MailboxStatus.Unseen = unread
|
||||
update.MailboxStatus.UnseenSeqNum = unreadSeqNum
|
||||
iu.sendIMAPUpdate(update, true)
|
||||
iu.sendIMAPUpdate(l, update, true)
|
||||
}
|
||||
|
||||
func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, isBlocking bool) {
|
||||
func (iu *imapUpdates) sendIMAPUpdate(updateLog *logrus.Entry, update goIMAPBackend.Update, isBlocking bool) {
|
||||
l := updateLog.WithField("blocking", isBlocking)
|
||||
if iu.ch == nil {
|
||||
log.Trace("IMAP IDLE unavailable")
|
||||
l.Info("IMAP IDLE unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
@ -201,7 +203,7 @@ func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, isBlocking bo
|
||||
go func() {
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
log.Warn("IMAP update could not be sent (timeout)")
|
||||
l.Warn("IMAP update could not be sent (timeout)")
|
||||
return
|
||||
case iu.ch <- update:
|
||||
}
|
||||
@ -214,7 +216,35 @@ func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, isBlocking bo
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(1 * time.Second):
|
||||
log.Warn("IMAP update could not be delivered (timeout)")
|
||||
l.Warn("IMAP update could not be delivered (timeout)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (iu *imapUpdates) getIDs(address, mailboxName string) (addressID, mailboxID string) {
|
||||
addressID = "unknown-" + algo.HashBase64SHA256(address)
|
||||
mailboxID = "unknown-" + algo.HashBase64SHA256(mailboxName)
|
||||
|
||||
if iu == nil || iu.ib == nil {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := iu.ib.getUser(address)
|
||||
if err != nil || user == nil || user.storeAddress == nil {
|
||||
return
|
||||
}
|
||||
addressID = user.addressID
|
||||
|
||||
if v := user.mailboxIDs.get(mailboxName); v != "" {
|
||||
mailboxID = v
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (iu *imapUpdates) updateLog(address, mailboxName string) *logrus.Entry {
|
||||
addressID, mailboxID := iu.getIDs(address, mailboxName)
|
||||
return log.
|
||||
WithField("address", addressID).
|
||||
WithField("mailbox", mailboxID)
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
func TestUpdatesCanDelete(t *testing.T) {
|
||||
u := newIMAPUpdates()
|
||||
u := newIMAPUpdates(nil)
|
||||
|
||||
can, _ := u.CanDelete("mbox")
|
||||
require.True(t, can)
|
||||
@ -38,7 +38,7 @@ func TestUpdatesCanDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdatesCannotDelete(t *testing.T) {
|
||||
u := newIMAPUpdates()
|
||||
u := newIMAPUpdates(nil)
|
||||
|
||||
u.forbidExpunge("mbox")
|
||||
can, wait := u.CanDelete("mbox")
|
||||
|
||||
@ -53,6 +53,9 @@ type imapUser struct {
|
||||
// not cause huge slow down as EXPUNGE is implicitly called also after
|
||||
// UNSELECT, CLOSE, or LOGOUT.
|
||||
appendExpungeLock sync.Mutex
|
||||
|
||||
addressID string // cached value for logs to avoid lock
|
||||
mailboxIDs safeMapOfStrings // cached values for logs to avoid lock
|
||||
}
|
||||
|
||||
// newIMAPUser returns struct implementing go-imap/user interface.
|
||||
@ -84,6 +87,8 @@ func newIMAPUser(
|
||||
storeAddress: storeAddress,
|
||||
|
||||
currentAddressLowercase: strings.ToLower(address),
|
||||
addressID: addressID,
|
||||
mailboxIDs: newSafeMapOfString(),
|
||||
}, err
|
||||
}
|
||||
|
||||
@ -128,6 +133,12 @@ func (iu *imapUser) ListMailboxes(showOnlySubcribed bool) ([]goIMAPBackend.Mailb
|
||||
|
||||
mailboxes := []goIMAPBackend.Mailbox{}
|
||||
for _, storeMailbox := range iu.storeAddress.ListMailboxes() {
|
||||
iu.mailboxIDs.set(storeMailbox.Name(), storeMailbox.LabelID())
|
||||
|
||||
if storeMailbox.LabelID() == pmapi.AllMailLabel && !iu.backend.bridge.IsAllMailVisible() {
|
||||
continue
|
||||
}
|
||||
|
||||
if showOnlySubcribed && !iu.isSubscribed(storeMailbox.LabelID()) {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -107,7 +107,7 @@ func (l *Locations) getLicenseFilePath() string {
|
||||
|
||||
// GetDependencyLicensesLink returns link to page listing dependencies.
|
||||
func (l *Locations) GetDependencyLicensesLink() string {
|
||||
return "https://github.com/ProtonMail/proton-bridge/v2/blob/master/COPYING_NOTES.md#dependencies"
|
||||
return "https://github.com/ProtonMail/proton-bridge/blob/master/COPYING_NOTES.md#dependencies"
|
||||
}
|
||||
|
||||
// ProvideSettingsPath returns a location for user settings (e.g. ~/.config/<company>/<app>).
|
||||
|
||||
14
internal/store/cache/disk.go
vendored
14
internal/store/cache/disk.go
vendored
@ -21,7 +21,6 @@ import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@ -29,6 +28,7 @@ import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/algo"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/semaphore"
|
||||
"github.com/ricochet2200/go-disk-usage/du"
|
||||
)
|
||||
@ -100,13 +100,7 @@ func (c *onDiskCache) Lock(userID string) {
|
||||
}
|
||||
|
||||
func (c *onDiskCache) Unlock(userID string, passphrase []byte) error {
|
||||
hash := sha256.New()
|
||||
|
||||
if _, err := hash.Write(passphrase); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aes, err := aes.NewCipher(hash.Sum(nil))
|
||||
aes, err := aes.NewCipher(algo.Hash256(passphrase))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -279,9 +273,9 @@ func (c *onDiskCache) update() {
|
||||
}
|
||||
|
||||
func (c *onDiskCache) getUserPath(userID string) string {
|
||||
return filepath.Join(c.path, getHash(userID))
|
||||
return filepath.Join(c.path, algo.HashHexSHA256(userID))
|
||||
}
|
||||
|
||||
func (c *onDiskCache) getMessagePath(userID, messageID string) string {
|
||||
return filepath.Join(c.getUserPath(userID), getHash(messageID))
|
||||
return filepath.Join(c.getUserPath(userID), algo.HashHexSHA256(messageID))
|
||||
}
|
||||
|
||||
@ -15,20 +15,27 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package cache
|
||||
package algo
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func getHash(name string) string {
|
||||
hash := sha256.New()
|
||||
|
||||
if _, err := hash.Write([]byte(name)); err != nil {
|
||||
// sha256.Write always returns nill err so this should never happen
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
func Hash256(b []byte) []byte {
|
||||
h := sha256.Sum256(b)
|
||||
return h[:]
|
||||
}
|
||||
|
||||
func HashBase64SHA256(s string) string {
|
||||
return base64.StdEncoding.EncodeToString(
|
||||
Hash256([]byte(s)),
|
||||
)
|
||||
}
|
||||
|
||||
func HashHexSHA256(s string) string {
|
||||
return hex.EncodeToString(
|
||||
Hash256([]byte(s)),
|
||||
)
|
||||
}
|
||||
@ -18,8 +18,7 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/algo"
|
||||
)
|
||||
|
||||
type boundary struct {
|
||||
@ -31,13 +30,6 @@ func newBoundary(seed string) *boundary {
|
||||
}
|
||||
|
||||
func (bw *boundary) gen() string {
|
||||
hash := sha256.New()
|
||||
|
||||
if _, err := hash.Write([]byte(bw.val)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bw.val = hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
bw.val = algo.HashHexSHA256(bw.val)
|
||||
return bw.val
|
||||
}
|
||||
|
||||
@ -18,13 +18,13 @@
|
||||
package pmapi
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/algo"
|
||||
)
|
||||
|
||||
// ErrTLSMismatch indicates that no TLS fingerprint match could be found.
|
||||
@ -63,6 +63,5 @@ func (p *pinChecker) checkCertificate(conn net.Conn) error {
|
||||
}
|
||||
|
||||
func certFingerprint(cert *x509.Certificate) string {
|
||||
hash := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
|
||||
return fmt.Sprintf(`pin-sha256=%q`, base64.StdEncoding.EncodeToString(hash[:]))
|
||||
return fmt.Sprintf(`pin-sha256=%q`, algo.HashBase64SHA256(string(cert.RawSubjectPublicKeyInfo)))
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
|
||||
|
||||
_, dialer, _ := createClientWithPinningDialer("")
|
||||
copyTrustedPins(dialer.pinChecker)
|
||||
dialer.pinChecker.trustedPins = append(dialer.pinChecker.trustedPins, `pin-sha256="2opdB7b5INED5jS7duIDR7dM8Er99i7trnwKuW3GMCY="`)
|
||||
dialer.pinChecker.trustedPins = append(dialer.pinChecker.trustedPins, `pin-sha256="SA4v9d2YY4vX5YQOQ1qZHYTBMCTSD/sxPvyj+JL6+vI="`)
|
||||
_, err := dialer.DialTLS("tcp", "rsa4096.badssl.com:443")
|
||||
r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA")
|
||||
}
|
||||
|
||||
@ -90,7 +90,14 @@ type ImportMsgRes struct {
|
||||
|
||||
// Import imports messages to the user's account.
|
||||
func (c *client) Import(ctx context.Context, reqs ImportMsgReqs) ([]*ImportMsgRes, error) {
|
||||
if len(reqs) == 0 {
|
||||
return nil, errors.New("missing import requests")
|
||||
}
|
||||
|
||||
if len(reqs) > MaxImportMessageRequestLength {
|
||||
log.
|
||||
WithField("count", len(reqs)).
|
||||
Warn("Importing too many messages at once.")
|
||||
return nil, errors.New("request is too long")
|
||||
}
|
||||
|
||||
@ -98,6 +105,10 @@ func (c *client) Import(ctx context.Context, reqs ImportMsgReqs) ([]*ImportMsgRe
|
||||
for _, req := range reqs {
|
||||
remainingSize -= len(req.Message)
|
||||
if remainingSize < 0 {
|
||||
log.
|
||||
WithField("count", len(reqs)).
|
||||
WithField("size", MaxImportMessageRequestLength-remainingSize).
|
||||
Warn("Importing too big message(s)")
|
||||
return nil, errors.New("request size is too big")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.PHONY: check-go check-godog install-godog test test-bridge test-live test-live-bridge test-stage test-debug test-live-debug bench
|
||||
|
||||
export GO111MODULE=on
|
||||
export BRIDGE_VERSION:=2.2.2+integrationtests
|
||||
export BRIDGE_VERSION:=2.3.0+integrationtests
|
||||
export VERBOSITY?=fatal
|
||||
export TEST_DATA=testdata
|
||||
|
||||
|
||||
@ -24,6 +24,8 @@ import (
|
||||
func BridgeActionsFeatureContext(s *godog.ScenarioContext) {
|
||||
s.Step(`^bridge starts$`, bridgeStarts)
|
||||
s.Step(`^bridge syncs "([^"]*)"$`, bridgeSyncsUser)
|
||||
s.Step(`^All mail mailbox is hidden$`, allMailMailboxIsHidden)
|
||||
s.Step(`^All mail mailbox is visible$`, allMailMailboxIsVisible)
|
||||
}
|
||||
|
||||
func bridgeStarts() error {
|
||||
@ -42,3 +44,13 @@ func bridgeSyncsUser(bddUserID string) error {
|
||||
ctx.SetLastError(ctx.GetTestingError())
|
||||
return nil
|
||||
}
|
||||
|
||||
func allMailMailboxIsHidden() error {
|
||||
ctx.GetBridge().SetIsAllMailVisible(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func allMailMailboxIsVisible() error {
|
||||
ctx.GetBridge().SetIsAllMailVisible(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -80,8 +80,10 @@ func (ctl *Controller) AddUserLabel(username string, label *pmapi.Label) error {
|
||||
ctl.labelsByUsername[username] = []*pmapi.Label{}
|
||||
}
|
||||
|
||||
userLabels := ctl.labelsByUsername[username]
|
||||
|
||||
labelName := getLabelNameWithoutPrefix(label.Name)
|
||||
for _, existingLabel := range ctl.labelsByUsername[username] {
|
||||
for _, existingLabel := range userLabels {
|
||||
if existingLabel.Name == labelName {
|
||||
return fmt.Errorf("folder or label %s already exists", label.Name)
|
||||
}
|
||||
@ -97,7 +99,9 @@ func (ctl *Controller) AddUserLabel(username string, label *pmapi.Label) error {
|
||||
if label.Path == "" {
|
||||
label.Path = label.Name
|
||||
}
|
||||
ctl.labelsByUsername[username] = append(ctl.labelsByUsername[username], label)
|
||||
userLabels = append(userLabels, label)
|
||||
|
||||
ctl.labelsByUsername[username] = userLabels
|
||||
ctl.resetUsers()
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ package fakeapi
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
||||
)
|
||||
@ -87,16 +88,23 @@ func (api *FakePMAPI) listLabels(_ context.Context, labeType string, route strin
|
||||
if err := api.checkAndRecordCall(GET, route+"/"+labeType, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return api.labels, nil
|
||||
return append([]*pmapi.Label{}, api.labels...), nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) createLabel(_ context.Context, label *pmapi.Label, route string) (*pmapi.Label, error) {
|
||||
if err := api.checkAndRecordCall(POST, route, &pmapi.LabelReq{Label: label}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// API blocks certain names
|
||||
switch strings.ToLower(label.Name) {
|
||||
case "inbox", "drafts", "trash", "spam", "starred":
|
||||
return nil, fmt.Errorf("Invalid name") //nolint:stylecheck
|
||||
}
|
||||
|
||||
for _, existingLabel := range api.labels {
|
||||
if existingLabel.Name == label.Name {
|
||||
return nil, fmt.Errorf("folder or label %s already exists", label.Name)
|
||||
return nil, fmt.Errorf("A label or folder with this name already exists") //nolint:stylecheck
|
||||
}
|
||||
}
|
||||
prefix := "label"
|
||||
|
||||
@ -17,11 +17,34 @@ Feature: IMAP create mailbox
|
||||
And "user" does not have mailbox "Folders/mbox"
|
||||
And "user" has mailbox "Labels/mbox"
|
||||
|
||||
Scenario: Creating label with existing name is not possible
|
||||
Given there is "user" with mailbox "Folders/mbox"
|
||||
When IMAP client creates mailbox "Labels/mbox"
|
||||
Then IMAP response is "IMAP error: NO A label or folder with this name already exists"
|
||||
And "user" has mailbox "Folders/mbox"
|
||||
And "user" does not have mailbox "Labels/mbox"
|
||||
|
||||
Scenario: Creating folder with existing name is not possible
|
||||
Given there is "user" with mailbox "Labels/mbox"
|
||||
When IMAP client creates mailbox "Folders/mbox"
|
||||
Then IMAP response is "IMAP error: NO A label or folder with this name already exists"
|
||||
And "user" has mailbox "Labels/mbox"
|
||||
And "user" does not have mailbox "Folders/mbox"
|
||||
|
||||
Scenario: Creating system mailbox is not possible
|
||||
When IMAP client creates mailbox "INBOX"
|
||||
Then IMAP response is "IMAP error: NO mailbox INBOX already exists"
|
||||
When IMAP client creates mailbox "Folders/INBOX"
|
||||
Then IMAP response is "IMAP error: NO Invalid name"
|
||||
# API allows you to create custom folder with naem `All Mail`
|
||||
#When IMAP client creates mailbox "Folders/All mail"
|
||||
#Then IMAP response is "IMAP error: NO mailbox All Mail already exists"
|
||||
|
||||
Scenario: Creating mailbox without prefix is not possible
|
||||
When IMAP client creates mailbox "mbox"
|
||||
Then IMAP response is "OK"
|
||||
And "user" does not have mailbox "mbox"
|
||||
When All mail mailbox is hidden
|
||||
And IMAP client creates mailbox "All mail"
|
||||
Then IMAP response is "OK"
|
||||
And "user" does not have mailbox "All mail"
|
||||
|
||||
@ -15,6 +15,120 @@ Feature: IMAP list mailboxes
|
||||
Then IMAP response contains "Folders/mbox1"
|
||||
Then IMAP response contains "Labels/mbox2"
|
||||
|
||||
Scenario: List mailboxes without All Mail
|
||||
Given there is IMAP client logged in as "user"
|
||||
When IMAP client lists mailboxes
|
||||
Then IMAP response contains "INBOX"
|
||||
Then IMAP response contains "Sent"
|
||||
Then IMAP response contains "Archive"
|
||||
Then IMAP response contains "Trash"
|
||||
Then IMAP response contains "All Mail"
|
||||
When All mail mailbox is hidden
|
||||
And IMAP client lists mailboxes
|
||||
Then IMAP response contains "INBOX"
|
||||
Then IMAP response contains "Sent"
|
||||
Then IMAP response contains "Archive"
|
||||
Then IMAP response contains "Trash"
|
||||
Then IMAP response doesn't contain "All Mail"
|
||||
When All mail mailbox is visible
|
||||
And IMAP client lists mailboxes
|
||||
Then IMAP response contains "INBOX"
|
||||
Then IMAP response contains "Sent"
|
||||
Then IMAP response contains "Archive"
|
||||
Then IMAP response contains "Trash"
|
||||
Then IMAP response contains "All Mail"
|
||||
|
||||
Scenario: List multiple times in parallel without crash
|
||||
Given there is "user" with mailboxes
|
||||
| Folders/mbox1 |
|
||||
| Folders/mbox2 |
|
||||
| Folders/mbox3 |
|
||||
| Folders/mbox4 |
|
||||
| Folders/mbox5 |
|
||||
| Folders/mbox6 |
|
||||
| Folders/mbox7 |
|
||||
| Folders/mbox8 |
|
||||
| Folders/mbox9 |
|
||||
| Folders/mbox10 |
|
||||
| Folders/mbox11 |
|
||||
| Folders/mbox12 |
|
||||
| Folders/mbox13 |
|
||||
| Folders/mbox14 |
|
||||
| Folders/mbox15 |
|
||||
| Folders/mbox16 |
|
||||
| Folders/mbox17 |
|
||||
| Folders/mbox18 |
|
||||
| Folders/mbox19 |
|
||||
| Folders/mbox20 |
|
||||
| Labels/lab1 |
|
||||
| Labels/lab2 |
|
||||
| Labels/lab3 |
|
||||
| Labels/lab4 |
|
||||
| Labels/lab5 |
|
||||
| Labels/lab6 |
|
||||
| Labels/lab7 |
|
||||
| Labels/lab8 |
|
||||
| Labels/lab9 |
|
||||
| Labels/lab10 |
|
||||
| Labels/lab11 |
|
||||
| Labels/lab12 |
|
||||
| Labels/lab13 |
|
||||
| Labels/lab14 |
|
||||
| Labels/lab15 |
|
||||
| Labels/lab16 |
|
||||
| Labels/lab17 |
|
||||
| Labels/lab18 |
|
||||
| Labels/lab19 |
|
||||
| Labels/lab20 |
|
||||
| Labels/lab1.1 |
|
||||
| Labels/lab1.2 |
|
||||
| Labels/lab1.3 |
|
||||
| Labels/lab1.4 |
|
||||
| Labels/lab1.5 |
|
||||
| Labels/lab1.6 |
|
||||
| Labels/lab1.7 |
|
||||
| Labels/lab1.8 |
|
||||
| Labels/lab1.9 |
|
||||
| Labels/lab1.10 |
|
||||
| Labels/lab1.11 |
|
||||
| Labels/lab1.12 |
|
||||
| Labels/lab1.13 |
|
||||
| Labels/lab1.14 |
|
||||
| Labels/lab1.15 |
|
||||
| Labels/lab1.16 |
|
||||
| Labels/lab1.17 |
|
||||
| Labels/lab1.18 |
|
||||
| Labels/lab1.19 |
|
||||
| Labels/lab1.20 |
|
||||
| Labels/lab2.1 |
|
||||
| Labels/lab2.2 |
|
||||
| Labels/lab2.3 |
|
||||
| Labels/lab2.4 |
|
||||
| Labels/lab2.5 |
|
||||
| Labels/lab2.6 |
|
||||
| Labels/lab2.7 |
|
||||
| Labels/lab2.8 |
|
||||
| Labels/lab2.9 |
|
||||
| Labels/lab2.10 |
|
||||
| Labels/lab2.11 |
|
||||
| Labels/lab2.12 |
|
||||
| Labels/lab2.13 |
|
||||
| Labels/lab2.14 |
|
||||
| Labels/lab2.15 |
|
||||
| Labels/lab2.16 |
|
||||
| Labels/lab2.17 |
|
||||
| Labels/lab2.18 |
|
||||
| Labels/lab2.19 |
|
||||
| Labels/lab2.20 |
|
||||
And there is IMAP client "A" logged in as "user"
|
||||
And there is IMAP client "B" logged in as "user"
|
||||
When IMAP client "A" lists mailboxes
|
||||
And IMAP client "B" lists mailboxes
|
||||
Then IMAP response to "A" is "OK"
|
||||
And IMAP response to "A" contains "mbox1"
|
||||
And IMAP response to "A" contains "mbox10"
|
||||
And IMAP response to "A" contains "mbox20"
|
||||
|
||||
@ignore-live
|
||||
Scenario: List mailboxes with subfolders
|
||||
# Escaped slash in the name contains slash in the name.
|
||||
|
||||
@ -28,6 +28,7 @@ func IMAPActionsMailboxFeatureContext(s *godog.ScenarioContext) {
|
||||
s.Step(`^IMAP client renames mailbox "([^"]*)" to "([^"]*)"$`, imapClientRenamesMailboxTo)
|
||||
s.Step(`^IMAP client deletes mailbox "([^"]*)"$`, imapClientDeletesMailbox)
|
||||
s.Step(`^IMAP client lists mailboxes$`, imapClientListsMailboxes)
|
||||
s.Step(`^IMAP client "([^"]*)" lists mailboxes$`, imapClientNamedListsMailboxes)
|
||||
s.Step(`^IMAP client selects "([^"]*)"$`, imapClientSelects)
|
||||
s.Step(`^IMAP client gets info of "([^"]*)"$`, imapClientGetsInfoOf)
|
||||
s.Step(`^IMAP client "([^"]*)" gets info of "([^"]*)"$`, imapClientNamedGetsInfoOf)
|
||||
@ -63,8 +64,12 @@ func imapClientDeletesMailbox(mailboxName string) error {
|
||||
}
|
||||
|
||||
func imapClientListsMailboxes() error {
|
||||
res := ctx.GetIMAPClient("imap").ListMailboxes()
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return imapClientNamedListsMailboxes("imap")
|
||||
}
|
||||
|
||||
func imapClientNamedListsMailboxes(clientName string) error {
|
||||
res := ctx.GetIMAPClient(clientName).ListMailboxes()
|
||||
ctx.SetIMAPLastResponse(clientName, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ func IMAPChecksFeatureContext(s *godog.ScenarioContext) {
|
||||
s.Step(`^IMAP response to "([^"]*)" contains "([^"]*)"$`, imapResponseNamedContains)
|
||||
s.Step(`^IMAP response has (\d+) message(?:s)?$`, imapResponseHasNumberOfMessages)
|
||||
s.Step(`^IMAP response to "([^"]*)" has (\d+) message(?:s)?$`, imapResponseNamedHasNumberOfMessages)
|
||||
s.Step(`^IMAP response does not contain "([^"]*)"$`, imapResponseDoesNotContain)
|
||||
s.Step(`^IMAP response does(?: not|n't) contain "([^"]*)"$`, imapResponseDoesNotContain)
|
||||
s.Step(`^IMAP response to "([^"]*)" does not contain "([^"]*)"$`, imapResponseNamedDoesNotContain)
|
||||
s.Step(`^IMAP client receives update marking message seq "([^"]*)" as read within (\d+) seconds$`, imapClientReceivesUpdateMarkingMessageSeqAsReadWithin)
|
||||
s.Step(`^IMAP client "([^"]*)" receives update marking message seq "([^"]*)" as read within (\d+) seconds$`, imapClientNamedReceivesUpdateMarkingMessageSeqAsReadWithin)
|
||||
|
||||
Reference in New Issue
Block a user