diff --git a/Changelog.md b/Changelog.md
index 03e699b3..f875dfd3 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -22,6 +22,40 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-700 Fix UTF-7 incompatibility.
* GODT-837 Fix flaky TestFailUnpauseAndStops.
+## [Bridge 1.4.5] Forth
+
+### Fixed
+* GODT-829 Remove `NoInferior` to display sub-folders in apple mail.
+
+## [Bridge 1.4.4] Forth
+
+### Fixed
+* GODT-798 Replace, don't add, transfer encoding when making body 7-bit clean.
+* Move/Copy duplicate for emails with References in Outlook
+* CSB-247 Cannot update from 1.4.0
+
+
+## [Bridge 1.4.3] Forth
+
+### Changed
+* Reverted sending IMAP updates to be not blocking again.
+
+### Fixed
+* GODT-783 Settings flags by FLAGS (not using +/-FLAGS) do not change spam state.
+
+
+## [Bridge 1.4.2] Forth
+
+### Changed
+* GODT-761 Use label.Path instead of Name to partially support subfolders for webapp beta release.
+* GODT-765 Improve speed of checking whether message is deleted.
+
+
+## [IE 1.1.2] Danube (beta 2020-09-xx)
+
+### Fixed
+* GODT-770 Better handling of extraneous end-of-mail indicator.
+
### Changed
* Bump crypto version to v0.0.0-20200818122824-ed5d25e28db8
* GODT-785 Clear separation of different message IDs in integration tests.
@@ -38,6 +72,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-752 Parsing non-utf8 multipart/alternative message.
* GODT-752 Parsing message with duplicate charset parameter.
+
## [IE 1.1.0] Danube
### Fixed
@@ -46,6 +81,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Fixed
* GODT-732 Do not mix font awesome icon with regular text to avoid issues on Fedora.
+
## [Bridge 1.4.0] Forth
### Added
diff --git a/internal/bridge/credits.go b/internal/bridge/credits.go
index 15d6e6a2..12f61bc5 100644
--- a/internal/bridge/credits.go
+++ b/internal/bridge/credits.go
@@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-// Code generated by ./credits.sh at Wed Sep 16 16:48:58 CEST 2020. DO NOT EDIT.
+// Code generated by ./credits.sh at Tue Sep 29 14:56:25 CEST 2020. DO NOT EDIT.
package bridge
-const Credits = "github.com/0xAX/notificator;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
+const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/mbox;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
diff --git a/internal/imap/mailbox.go b/internal/imap/mailbox.go
index 217842c5..746382dc 100644
--- a/internal/imap/mailbox.go
+++ b/internal/imap/mailbox.go
@@ -80,7 +80,10 @@ func (im *imapMailbox) Info() (*imap.MailboxInfo, error) {
}
func (im *imapMailbox) getFlags() []string {
- flags := []string{imap.NoInferiorsAttr} // Subfolders are not yet supported by API.
+ flags := []string{}
+ if !im.storeMailbox.IsFolder() || im.storeMailbox.IsSystem() {
+ flags = append(flags, imap.NoInferiorsAttr) // Subfolders are not supported for System or Label
+ }
switch im.storeMailbox.LabelID() {
case pmapi.SentLabel:
flags = append(flags, specialuse.Sent)
diff --git a/internal/imap/mailbox_message.go b/internal/imap/mailbox_message.go
index 5d25a2ae..5ae84d9c 100644
--- a/internal/imap/mailbox_message.go
+++ b/internal/imap/mailbox_message.go
@@ -24,7 +24,6 @@ import (
"mime/multipart"
"net/mail"
"net/textproto"
- "regexp"
"sort"
"strings"
"time"
@@ -141,18 +140,19 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
references := m.Header.Get("References")
referenceList := strings.Fields(references)
- if len(referenceList) > 0 {
+ // In case there is a mail client which corrupts headers, try
+ // "References" too.
+ if internalID == "" && len(referenceList) > 0 {
lastReference := referenceList[len(referenceList)-1]
- // In case we are using a mail client which corrupts headers, try "References" too.
- re := regexp.MustCompile(pmapi.InternalReferenceFormat)
- match := re.FindStringSubmatch(lastReference)
- if len(match) > 0 {
- internalID = match[0]
+ match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(lastReference)
+ if len(match) == 2 {
+ internalID = match[1]
}
}
- // Avoid appending a message which is already on the server. Apply the new
- // label instead. This sometimes happens which Outlook (it uses APPEND instead of COPY).
+ // Avoid appending a message which is already on the server. Apply the
+ // new label instead. This always happens with Outlook (it uses APPEND
+ // instead of COPY).
if internalID != "" {
// Check to see if this belongs to a different address in split mode or another ProtonMail account.
msg, err := im.storeMailbox.GetMessage(internalID)
diff --git a/internal/imap/mailbox_messages.go b/internal/imap/mailbox_messages.go
index f45a7062..95419dfd 100644
--- a/internal/imap/mailbox_messages.go
+++ b/internal/imap/mailbox_messages.go
@@ -57,6 +57,10 @@ func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operat
return im.addOrRemoveFlags(operation, messageIDs, flags)
}
+// setFlags is used for FLAGS command (not +FLAGS or -FLAGS), which means
+// to set flags passed as an argument and unset the rest. For example,
+// if message is not read, is flagged and is not deleted, call FLAGS \Seen
+// should flag message as read, unflagged and keep undeleted.
func (im *imapMailbox) setFlags(messageIDs, flags []string) error { //nolint
seen := false
flagged := false
@@ -106,16 +110,17 @@ func (im *imapMailbox) setFlags(messageIDs, flags []string) error { //nolint
}
}
- spamMailbox, err := im.storeAddress.GetMailbox("Spam")
- if err != nil {
- return err
- }
+ // Spam should not be taken into action here as Outlook is using FLAGS
+ // without preserving junk flag. Probably it's because junk is not standard
+ // in the rfc3501 and thus Outlook expects calling FLAGS \Seen will not
+ // change the state of junk or other non-standard flags.
+ // Still, its safe to label as spam once any client sends the request.
if spam {
- if err := spamMailbox.LabelMessages(messageIDs); err != nil {
+ spamMailbox, err := im.storeAddress.GetMailbox("Spam")
+ if err != nil {
return err
}
- } else {
- if err := spamMailbox.UnlabelMessages(messageIDs); err != nil {
+ if err := spamMailbox.LabelMessages(messageIDs); err != nil {
return err
}
}
diff --git a/internal/importexport/credits.go b/internal/importexport/credits.go
index a8d79c8d..1b05aead 100644
--- a/internal/importexport/credits.go
+++ b/internal/importexport/credits.go
@@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-// Code generated by ./credits.sh at Wed Sep 23 01:34:10 PM CEST 2020. DO NOT EDIT.
+// Code generated by ./credits.sh at Tue Sep 29 14:56:25 CEST 2020. DO NOT EDIT.
package importexport
-const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/mbox;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
+const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/mbox;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
diff --git a/internal/smtp/user.go b/internal/smtp/user.go
index 079c920f..37957249 100644
--- a/internal/smtp/user.go
+++ b/internal/smtp/user.go
@@ -25,7 +25,6 @@ import (
"io"
"mime"
"net/mail"
- "regexp"
"strings"
"time"
@@ -408,9 +407,9 @@ func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID
if !strings.Contains(reference, "@"+pmapi.InternalIDDomain) {
newReferences = append(newReferences, reference)
} else { // internalid is the parentID.
- idMatch := regexp.MustCompile(pmapi.InternalReferenceFormat).FindStringSubmatch(reference)
- if len(idMatch) > 0 {
- lastID := strings.TrimSuffix(strings.Trim(idMatch[0], "<>"), "@protonmail.internalid")
+ idMatch := pmapi.RxInternalReferenceFormat.FindStringSubmatch(reference)
+ if len(idMatch) == 2 {
+ lastID := idMatch[1]
filter := &pmapi.MessagesFilter{ID: []string{lastID}}
if su.addressID != "" {
filter.AddressID = su.addressID
diff --git a/internal/store/address.go b/internal/store/address.go
index d890742b..65e1fb07 100644
--- a/internal/store/address.go
+++ b/internal/store/address.go
@@ -69,7 +69,7 @@ func (storeAddress *Address) init(foldersAndLabels []*pmapi.Label) (err error) {
prefix := getLabelPrefix(label)
var mailbox *Mailbox
- if mailbox, err = txNewMailbox(tx, storeAddress, label.ID, prefix, label.Name, label.Color); err != nil {
+ if mailbox, err = txNewMailbox(tx, storeAddress, label.ID, prefix, label.Path, label.Color); err != nil {
storeAddress.log.
WithError(err).
WithField("labelID", label.ID).
diff --git a/internal/store/address_mailbox.go b/internal/store/address_mailbox.go
index dfa4a78e..33efcdf7 100644
--- a/internal/store/address_mailbox.go
+++ b/internal/store/address_mailbox.go
@@ -73,14 +73,14 @@ func (storeAddress *Address) createOrUpdateMailboxEvent(label *pmapi.Label) erro
prefix := getLabelPrefix(label)
mailbox, ok := storeAddress.mailboxes[label.ID]
if !ok {
- mailbox, err := newMailbox(storeAddress, label.ID, prefix, label.Name, label.Color)
+ mailbox, err := newMailbox(storeAddress, label.ID, prefix, label.Path, label.Color)
if err != nil {
return err
}
storeAddress.mailboxes[label.ID] = mailbox
mailbox.store.imapMailboxCreated(storeAddress.address, mailbox.labelName)
} else {
- mailbox.labelName = prefix + label.Name
+ mailbox.labelName = prefix + label.Path
mailbox.color = label.Color
}
return nil
diff --git a/internal/store/change.go b/internal/store/change.go
index 96c1c061..39946b3b 100644
--- a/internal/store/change.go
+++ b/internal/store/change.go
@@ -122,22 +122,10 @@ func (store *Store) imapSendUpdate(update imapBackend.Update) {
return
}
- done := update.Done()
- go func() {
- // This timeout is to not keep running many blocked goroutines.
- // In case nothing listens to this channel, this thread should stop.
- select {
- case store.imapUpdates <- update:
- case <-time.After(1 * time.Second):
- store.log.Warn("IMAP update could not be sent (timeout).")
- }
- }()
-
- // This timeout is to not block IMAP backend by wait for IMAP client.
select {
- case <-done:
case <-time.After(1 * time.Second):
- store.log.Warn("IMAP update could not be delivered (timeout).")
+ store.log.Warn("IMAP update could not be sent (timeout)")
return
+ case store.imapUpdates <- update:
}
}
diff --git a/internal/store/mailbox.go b/internal/store/mailbox.go
index e711093c..89857af3 100644
--- a/internal/store/mailbox.go
+++ b/internal/store/mailbox.go
@@ -142,6 +142,9 @@ func initMailboxBucket(tx *bolt.Tx, bucketName []byte) error {
if _, err := bucket.CreateBucketIfNotExists(apiIDsBucket); err != nil {
return err
}
+ if _, err := bucket.CreateBucketIfNotExists(deletedIDsBucket); err != nil {
+ return err
+ }
return nil
}
@@ -240,13 +243,7 @@ func (storeMailbox *Mailbox) txGetAPIIDsBucket(tx *bolt.Tx) *bolt.Bucket {
// txGetDeletedIDsBucket returns the bucket with messagesID marked as deleted
func (storeMailbox *Mailbox) txGetDeletedIDsBucket(tx *bolt.Tx) *bolt.Bucket {
- // There should be no error since it _...returns an error if the bucket
- // name is blank, or if the bucket name is too long._
- bucket, err := storeMailbox.txGetBucket(tx).CreateBucketIfNotExists(deletedIDsBucket)
- if err != nil || bucket == nil {
- storeMailbox.log.WithError(err).Error("Cannot create or get bucket with deleted IDs.")
- }
- return bucket
+ return storeMailbox.txGetBucket(tx).Bucket(deletedIDsBucket)
}
// txGetBucket returns the bucket of mailbox containing mapping buckets.
diff --git a/internal/store/mailbox_counts.go b/internal/store/mailbox_counts.go
index 3674a1d8..4fd3a636 100644
--- a/internal/store/mailbox_counts.go
+++ b/internal/store/mailbox_counts.go
@@ -125,6 +125,7 @@ func (mc *mailboxCounts) getPMLabel() *pmapi.Label {
return &pmapi.Label{
ID: mc.LabelID,
Name: mc.LabelName,
+ Path: mc.LabelName,
Color: mc.Color,
Order: mc.Order,
Type: pmapi.LabelTypeMailbox,
@@ -158,7 +159,7 @@ func (store *Store) createOrUpdateMailboxCountsBuckets(labels []*pmapi.Label) er
}
// Update mailbox info, but dont change on-API-counts.
- mailbox.LabelName = label.Name
+ mailbox.LabelName = label.Path
mailbox.Color = label.Color
mailbox.Order = label.Order
mailbox.IsFolder = label.Exclusive == 1
diff --git a/internal/store/message.go b/internal/store/message.go
index b88698c1..38b230a6 100644
--- a/internal/store/message.go
+++ b/internal/store/message.go
@@ -66,7 +66,7 @@ func (message *Message) Message() *pmapi.Message {
// mailbox
func (message *Message) IsMarkedDeleted() bool {
isMarkedAsDeleted := false
- err := message.storeMailbox.db().Update(func(tx *bolt.Tx) error {
+ err := message.storeMailbox.db().View(func(tx *bolt.Tx) error {
isMarkedAsDeleted = message.storeMailbox.txGetDeletedIDsBucket(tx).Get([]byte(message.msg.ID)) != nil
return nil
})
diff --git a/internal/updates/updates.go b/internal/updates/updates.go
index 538c9d84..cdd11fe6 100644
--- a/internal/updates/updates.go
+++ b/internal/updates/updates.go
@@ -310,7 +310,9 @@ func (u *Updates) StartUpgrade(currentStatus chan<- Progress) { // nolint[funlen
status.UpdateDescription(InfoUpgrading)
switch runtime.GOOS {
case "windows": //nolint[goconst]
- installerFile := strings.Split(u.winInstallerFile, "/")[1]
+ // Cannot use filepath.Base on windows it has different delimiter
+ split := strings.Split(u.winInstallerFile, "/")
+ installerFile := split[len(split)-1]
cmd := exec.Command("./" + installerFile) // nolint[gosec]
cmd.Dir = u.updateTempDir
status.Err = cmd.Start()
diff --git a/pkg/message/parser/parser.go b/pkg/message/parser/parser.go
index 7d4d4f7b..6639006e 100644
--- a/pkg/message/parser/parser.go
+++ b/pkg/message/parser/parser.go
@@ -32,7 +32,7 @@ type Parser struct {
func New(r io.Reader) (*Parser, error) {
p := new(Parser)
- entity, err := message.Read(r)
+ entity, err := message.Read(newEndOfMailTrimmer(r))
if err != nil && !message.IsUnknownCharset(err) {
return nil, err
}
diff --git a/pkg/message/parser/trimmer.go b/pkg/message/parser/trimmer.go
new file mode 100644
index 00000000..fe90a202
--- /dev/null
+++ b/pkg/message/parser/trimmer.go
@@ -0,0 +1,56 @@
+// 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 parser
+
+import (
+ "bytes"
+ "io"
+)
+
+const endOfMail = "\r\n.\r\n"
+
+// endOfMailTrimmer wraps a reader to trim the End-Of-Mail indicator at the end
+// of the input, if present.
+//
+// During SMTP sending of a message, the DATA command indicates that you are
+// about to send the text (or body) of the message. The message text must end
+// with "\r\n.\r\n." I'm 99% sure that these 5 bytes should not be considered
+// part of the message body. However, some mail servers keep them as part of
+// the message, which our parser sometimes doesn't like. Therefore, we strip
+// them if we find them.
+type endOfMailTrimmer struct {
+ r io.Reader
+ buf bytes.Buffer
+}
+
+func newEndOfMailTrimmer(r io.Reader) *endOfMailTrimmer {
+ return &endOfMailTrimmer{r: r}
+}
+
+func (r *endOfMailTrimmer) Read(p []byte) (int, error) {
+ _, err := io.CopyN(&r.buf, r.r, int64(len(p)+len(endOfMail)-r.buf.Len()))
+ if err != nil && err != io.EOF {
+ return 0, err
+ }
+
+ if err == io.EOF && bytes.HasSuffix(r.buf.Bytes(), []byte(endOfMail)) {
+ r.buf.Truncate(r.buf.Len() - len(endOfMail))
+ }
+
+ return r.buf.Read(p)
+}
diff --git a/pkg/message/parser/trimmer_test.go b/pkg/message/parser/trimmer_test.go
new file mode 100644
index 00000000..a8d7d66b
--- /dev/null
+++ b/pkg/message/parser/trimmer_test.go
@@ -0,0 +1,55 @@
+// 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 parser
+
+import (
+ "io"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEndOfMailTrimmer(t *testing.T) {
+ var tests = []struct {
+ in string
+ out string
+ }{
+ {"string without eom", "string without eom"},
+ {"string with eom\r\n.\r\n", "string with eom"},
+ {"string with eom\r\n.\r\nin the middle", "string with eom\r\n.\r\nin the middle"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.in, func(t *testing.T) {
+ res := dumbRead(newEndOfMailTrimmer(strings.NewReader(tt.in)))
+ assert.Equal(t, tt.out, string(res))
+ })
+ }
+}
+
+func dumbRead(r io.Reader) []byte {
+ out := []byte{}
+
+ b := make([]byte, 1)
+ for _, err := r.Read(b); err == nil; _, err = r.Read(b) {
+ out = append(out, b...)
+ }
+
+ return out
+}
diff --git a/pkg/message/parser/writer.go b/pkg/message/parser/writer.go
index e1e4e3d5..b4cbaefc 100644
--- a/pkg/message/parser/writer.go
+++ b/pkg/message/parser/writer.go
@@ -35,7 +35,7 @@ func newWriter(root *Part) *Writer {
func (w *Writer) Write(ww io.Writer) error {
if !w.root.is7BitClean() {
- w.root.Header.Add("Content-Transfer-Encoding", "base64")
+ w.root.Header.Set("Content-Transfer-Encoding", "base64")
}
msgWriter, err := message.CreateWriter(ww, w.root.Header)
@@ -68,7 +68,7 @@ func (w *Writer) write(writer *message.Writer, p *Part) error {
func (w *Writer) writeAsChild(writer *message.Writer, p *Part) error {
if !p.is7BitClean() {
- p.Header.Add("Content-Transfer-Encoding", "base64")
+ p.Header.Set("Content-Transfer-Encoding", "base64")
}
childWriter, err := writer.CreatePart(p.Header)
diff --git a/pkg/message/parser_test.go b/pkg/message/parser_test.go
index 5c2fabb0..9c5c7075 100644
--- a/pkg/message/parser_test.go
+++ b/pkg/message/parser_test.go
@@ -467,6 +467,19 @@ func TestParseMultipartAlternativeLatin1(t *testing.T) {
assert.Equal(t, "*aoeuaoeu*\n\n", plainBody)
}
+func TestParseWithTrailingEndOfMailIndicator(t *testing.T) {
+ f := getFileReader("text_html_trailing_end_of_mail.eml")
+
+ m, _, plainBody, _, err := Parse(f, "", "")
+ require.NoError(t, err)
+
+ assert.Equal(t, `"Sender" `, m.Sender.String())
+ assert.Equal(t, `"Receiver" `, m.ToList[0].String())
+
+ assert.Equal(t, "boo!", m.Body)
+ assert.Equal(t, "boo!", plainBody)
+}
+
func getFileReader(filename string) io.Reader {
f, err := os.Open(filepath.Join("testdata", filename))
if err != nil {
diff --git a/pkg/message/testdata/text_html_trailing_end_of_mail.eml b/pkg/message/testdata/text_html_trailing_end_of_mail.eml
new file mode 100644
index 00000000..ea8190b5
--- /dev/null
+++ b/pkg/message/testdata/text_html_trailing_end_of_mail.eml
@@ -0,0 +1,8 @@
+From: "Sender"
+To: "Receiver"
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: base64
+MIME-Version: 1.0
+
+PCFET0NUWVBFIEhUTUw+CjxodG1sPjxib2R5PmJvbyE8L2JvZHk+PC9odG1sPg==
+.
diff --git a/pkg/pmapi/labels.go b/pkg/pmapi/labels.go
index cc970740..9e0c7808 100644
--- a/pkg/pmapi/labels.go
+++ b/pkg/pmapi/labels.go
@@ -80,6 +80,7 @@ const (
type Label struct {
ID string
Name string
+ Path string
Color string
Order int `json:",omitempty"`
Display int // Not used for now, leave it empty.
diff --git a/pkg/pmapi/messages.go b/pkg/pmapi/messages.go
index ed5d38b5..623bd07b 100644
--- a/pkg/pmapi/messages.go
+++ b/pkg/pmapi/messages.go
@@ -29,6 +29,7 @@ import (
"net/http"
"net/mail"
"net/url"
+ "regexp"
"strconv"
"strings"
@@ -149,8 +150,9 @@ const ConversationIDDomain = `protonmail.conversationid`
// InternalIDDomain is used as a placeholder for reference/message ID headers to improve compatibility with various clients.
const InternalIDDomain = `protonmail.internalid`
-// InternalReferenceFormat describes format of the message ID (as regex) used for parsing reference headers.
-const InternalReferenceFormat = `(?U)<.*@` + InternalIDDomain + `>`
+// RxInternalReferenceFormat is compiled regexp which describes the match for
+// a message ID used in reference headers.
+var RxInternalReferenceFormat = regexp.MustCompile(`(?U)<(.+)@` + regexp.QuoteMeta(InternalIDDomain) + `>`) //nolint[gochecknoglobals]
// Message structure.
type Message struct {
diff --git a/pkg/pmapi/mocks/mocks.go b/pkg/pmapi/mocks/mocks.go
index 1e37cc2a..ecf7103c 100644
--- a/pkg/pmapi/mocks/mocks.go
+++ b/pkg/pmapi/mocks/mocks.go
@@ -5,11 +5,12 @@
package mocks
import (
+ io "io"
+ reflect "reflect"
+
crypto "github.com/ProtonMail/gopenpgp/v2/crypto"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
- io "io"
- reflect "reflect"
)
// MockClient is a mock of Client interface
diff --git a/test/fakeapi/controller_control.go b/test/fakeapi/controller_control.go
index 4cdb1756..21ee61f7 100644
--- a/test/fakeapi/controller_control.go
+++ b/test/fakeapi/controller_control.go
@@ -83,6 +83,9 @@ func (ctl *Controller) AddUserLabel(username string, label *pmapi.Label) error {
}
label.ID = ctl.labelIDGenerator.next(prefix)
label.Name = labelName
+ if label.Path == "" {
+ label.Path = label.Name
+ }
ctl.labelsByUsername[username] = append(ctl.labelsByUsername[username], label)
ctl.resetUsers()
return nil
diff --git a/test/fakeapi/labels.go b/test/fakeapi/labels.go
index 24be7e5a..8d88c009 100644
--- a/test/fakeapi/labels.go
+++ b/test/fakeapi/labels.go
@@ -53,6 +53,9 @@ func (api *FakePMAPI) CreateLabel(label *pmapi.Label) (*pmapi.Label, error) {
prefix = "folder"
}
label.ID = api.controller.labelIDGenerator.next(prefix)
+ if label.Path == "" {
+ label.Path = label.Name
+ }
api.labels = append(api.labels, label)
api.addEventLabel(pmapi.EventCreate, label)
return label, nil
@@ -67,6 +70,9 @@ func (api *FakePMAPI) UpdateLabel(label *pmapi.Label) (*pmapi.Label, error) {
// Request doesn't have to include all properties and these have to stay the same.
label.Type = existingLabel.Type
label.Exclusive = existingLabel.Exclusive
+ if label.Path == "" {
+ label.Path = label.Name
+ }
api.labels[idx] = label
api.addEventLabel(pmapi.EventUpdate, label)
return label, nil
diff --git a/test/features/bridge/imap/mailbox/list.feature b/test/features/bridge/imap/mailbox/list.feature
index 0fa0989e..e8848f1f 100644
--- a/test/features/bridge/imap/mailbox/list.feature
+++ b/test/features/bridge/imap/mailbox/list.feature
@@ -1,11 +1,11 @@
Feature: IMAP list mailboxes
Background:
Given there is connected user "user"
- And there is "user" with mailbox "Folders/mbox1"
- And there is "user" with mailbox "Labels/mbox2"
- And there is IMAP client logged in as "user"
Scenario: List mailboxes
+ Given there is "user" with mailbox "Folders/mbox1"
+ And there is "user" with mailbox "Labels/mbox2"
+ And there is IMAP client logged in as "user"
When IMAP client lists mailboxes
Then IMAP response contains "INBOX"
Then IMAP response contains "Sent"
@@ -14,3 +14,16 @@ Feature: IMAP list mailboxes
Then IMAP response contains "All Mail"
Then IMAP response contains "Folders/mbox1"
Then IMAP response contains "Labels/mbox2"
+
+ @ignore-live
+ Scenario: List mailboxes with subfolders
+ # Escaped slash in the name contains slash in the name.
+ # Not-escaped slash in the name means tree structure.
+ # We keep escaping in an IMAP communication so each mailbox is unique and
+ # both mailboxes are accessible. The slash is visible in the IMAP client.
+ Given there is "user" with mailbox "Folders/a\/b"
+ And there is "user" with mailbox "Folders/a/b"
+ And there is IMAP client logged in as "user"
+ When IMAP client lists mailboxes
+ Then IMAP response contains "Folders/a\/b"
+ Then IMAP response contains "Folders/a/b"
diff --git a/test/features/bridge/imap/message/update_spam.feature b/test/features/bridge/imap/message/update_spam.feature
new file mode 100644
index 00000000..f2c127da
--- /dev/null
+++ b/test/features/bridge/imap/message/update_spam.feature
@@ -0,0 +1,21 @@
+Feature: IMAP update messages in Spam folder
+ Background:
+ Given there is connected user "user"
+ # Messages are inserted in opposite way to keep increasing ID.
+ # Sequence numbers are then opposite than listed above.
+ And there are messages in mailbox "Spam" for "user"
+ | from | to | subject | body | read | starred | deleted |
+ | john.doe@mail.com | user@pm.me | foo | hello | false | false | false |
+ | jane.doe@mail.com | name@pm.me | bar | world | true | true | false |
+ And there is IMAP client logged in as "user"
+ And there is IMAP client selected in "Spam"
+
+ Scenario: Mark message as read only
+ When IMAP client marks message "2" with "\Seen"
+ Then IMAP response is "OK"
+ And message "1" in "Spam" for "user" is marked as read
+ And message "1" in "Spam" for "user" is marked as unstarred
+ And API mailbox "Spam" for "user" has messages
+ | from | to | subject |
+ | john.doe@mail.com | user@pm.me | foo |
+ | jane.doe@mail.com | name@pm.me | bar |