Compare commits

..

29 Commits

Author SHA1 Message Date
b5321f8993 Other: Bridge Nihonbashi 2.3.0 (fix 1840) 2022-08-31 17:03:07 +02:00
bcf799732f GODT-1840: Safe map for mailboxID cache 2022-08-31 15:48:40 +02:00
13ba2182c2 Other: Bridge Nihonbashi 2.3.0 2022-08-30 15:34:31 +02:00
0d25c607e7 GODT-1795: fix automatic installation of profile for AppleMail on macOS Ventura beta (qt 5). 2022-08-30 08:37:50 +00:00
b3f8866ef7 GODT-1737: Improve logging during import 2022-08-29 16:10:28 +02:00
9bb16dec48 GODT-1754: Add logs for unilateral updates and SEARCH. 2022-08-29 16:09:54 +02:00
bdb35f1c1d GODT-1799: fix dependency link [skip-ci] 2022-08-29 16:09:17 +02:00
d421b5aa5a GODT-1833: Fix gobinsec cache. 2022-08-29 15:51:20 +02:00
1ec05e8a6c GODT-1794: CLI wording 2022-08-26 16:49:58 +02:00
5b941013de Other: Update SSL certificate fingerprint for test 2022-08-26 16:43:24 +02:00
a93ed35eee GODT-1794: Add confirmation dialog and change wording 2022-08-26 15:01:18 +02:00
76469969f3 GODT-1741: GUI and CLI settings to change visibility of All Mail folder. 2022-08-25 13:43:33 +02:00
8b39ea4acb GODT-1740: Opt-out All Mail visibility in settings file. 2022-08-16 23:43:19 +02:00
252ca9a5f9 Other: Bridge Millau 2.2.2 2022-08-16 16:05:42 +02:00
c4eb1a0f5b GODT-1743: Terminate running bridge if has old version. 2022-08-16 16:05:42 +02:00
1e2f4e9ebb GODT-1743: Quit bridge when opening manual install 2022-08-16 16:05:42 +02:00
2a7aefac45 Other: Introduce gobinsec cache. 2022-08-16 16:05:42 +02:00
ea39e2d842 Other: Bridge Millau 2.2.1 2022-08-16 16:05:42 +02:00
fc5879a204 GODT-1565: Improve mac icon. 2022-08-16 16:05:42 +02:00
5ae2229e37 GODT-1475: Improve systray icon size. 2022-08-16 16:05:42 +02:00
12e5ce0ff0 GODT-1565: Update Bridge application icons. 2022-08-16 16:05:42 +02:00
5ef3774d11 GODT-1564: Update welcome illustration 2022-08-16 16:05:42 +02:00
654e816e6b GODT-1686: Add Label/Folder filtering to pmapi 2022-08-16 16:05:42 +02:00
7cad7bcddb GODT-1659: Convert charset only for text/* MIME types. 2022-08-16 16:05:42 +02:00
136d514cf7 GODT-1626: Update gopenpgp v2.4.7
This patch also replaces the deprecated calls to `SeparateKeyAndData`
with `SplitMessage`.
2022-08-16 16:05:42 +02:00
6e48345d54 GODT-1627: Update go-srvp to v0.0.5 2022-08-16 16:05:42 +02:00
8ebdb466f7 GODT-1523: Reduce unnecessary shell executions. Inspired by @kortschak. 2022-08-16 15:56:10 +02:00
1ed7b690a5 mitigate shelling out behaviour risks 2022-08-16 15:46:44 +02:00
5c28a3eda7 Don't shell out to obtain process and system stats 2022-08-16 15:46:34 +02:00
39 changed files with 552 additions and 105 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"
@ -59,6 +60,7 @@ func (api *apiServer) ListenAndServe() {
server := &http.Server{
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)

View File

@ -57,6 +57,7 @@ type Bridge struct {
// Bridge's global errors list.
errors []error
isAllMailVisible bool
isFirstStart bool
lastVersion string
}
@ -101,6 +102,7 @@ func New(
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
}

View File

@ -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")
}

View File

@ -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" {

View File

@ -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",

View File

@ -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.")

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
})
}

View File

@ -92,6 +92,8 @@ type Bridger interface {
DisableAutostart() error
GetLastVersion() string
IsFirstStart() bool
IsAllMailVisible() bool
SetIsAllMailVisible(bool)
}
type bridgeWrap struct {

View File

@ -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) {

View File

@ -31,6 +31,7 @@ type cacheProvider interface {
type bridger interface {
GetUser(query string) (bridgeUser, error)
HasError(err error) bool
IsAllMailVisible() bool
}
type bridgeUser interface {

View File

@ -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
}

View File

@ -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
View 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
}

View File

@ -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,
l := iu.updateLog(address, mailboxName).
WithFields(logrus.Fields{
"seqNum": sequenceNumber,
"uid": uid,
"flags": message.GetFlags(msg),
"deleted": hasDeletedFlag,
}).Trace("IDLE update")
})
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,
l := iu.updateLog(address, mailboxName).
WithFields(logrus.Fields{
"total": total,
"unread": unread,
"unreadSeqNum": unreadSeqNum,
}).Trace("IDLE status")
})
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)
}

View File

@ -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")

View File

@ -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
}

View File

@ -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>).

View File

@ -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))
}

View File

@ -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)),
)
}

View File

@ -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
}

View File

@ -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)))
}

View File

@ -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")
}

View File

@ -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")
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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"

View File

@ -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"

View File

@ -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.

View File

@ -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
}

View File

@ -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)