Compare commits

...

10 Commits

28 changed files with 221 additions and 53 deletions

View File

@ -4,13 +4,33 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Unreleased ## Unreleased
## [IE 1.1.x] Danube (v1.1.0 beta 2020-09-XX) ## [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.1] Danube (beta 2020-09-xx) [Bridge 1.4.1] Forth (beta 2020-09-xx)
### Fixed
* GODT-752 Parsing message with empty addresses.
* GODT-752 Parsing non-utf8 multipart/alternative message.
* GODT-752 Parsing message with duplicate charset parameter.
## [IE 1.1.0] Danube
### Fixed ### Fixed
* GODT-703 Import-Export showed always at least one total message. * GODT-703 Import-Export showed always at least one total message.
* GODT-738 Fix for mbox files with long lines. * GODT-738 Fix for mbox files with long lines.
## [Bridge 1.4.x] Forth (v1.4.0 beta 2020-09-XX) ## [Bridge 1.4.0] Forth
### Added ### Added
* GODT-682 Persistent anonymous API cookies for Import-Export. * GODT-682 Persistent anonymous API cookies for Import-Export.

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// 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 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;"

View File

@ -57,6 +57,10 @@ func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operat
return im.addOrRemoveFlags(operation, messageIDs, flags) 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 func (im *imapMailbox) setFlags(messageIDs, flags []string) error { //nolint
seen := false seen := false
flagged := false flagged := false
@ -106,16 +110,17 @@ func (im *imapMailbox) setFlags(messageIDs, flags []string) error { //nolint
} }
} }
spamMailbox, err := im.storeAddress.GetMailbox("Spam") // Spam should not be taken into action here as Outlook is using FLAGS
if err != nil { // without preserving junk flag. Probably it's because junk is not standard
return err // 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 spam {
if err := spamMailbox.LabelMessages(messageIDs); err != nil { spamMailbox, err := im.storeAddress.GetMailbox("Spam")
if err != nil {
return err return err
} }
} else { if err := spamMailbox.LabelMessages(messageIDs); err != nil {
if err := spamMailbox.UnlabelMessages(messageIDs); err != nil {
return err return err
} }
} }

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// 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 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;"

View File

@ -69,7 +69,7 @@ func (storeAddress *Address) init(foldersAndLabels []*pmapi.Label) (err error) {
prefix := getLabelPrefix(label) prefix := getLabelPrefix(label)
var mailbox *Mailbox 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. storeAddress.log.
WithError(err). WithError(err).
WithField("labelID", label.ID). WithField("labelID", label.ID).

View File

@ -73,14 +73,14 @@ func (storeAddress *Address) createOrUpdateMailboxEvent(label *pmapi.Label) erro
prefix := getLabelPrefix(label) prefix := getLabelPrefix(label)
mailbox, ok := storeAddress.mailboxes[label.ID] mailbox, ok := storeAddress.mailboxes[label.ID]
if !ok { 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 { if err != nil {
return err return err
} }
storeAddress.mailboxes[label.ID] = mailbox storeAddress.mailboxes[label.ID] = mailbox
mailbox.store.imapMailboxCreated(storeAddress.address, mailbox.labelName) mailbox.store.imapMailboxCreated(storeAddress.address, mailbox.labelName)
} else { } else {
mailbox.labelName = prefix + label.Name mailbox.labelName = prefix + label.Path
mailbox.color = label.Color mailbox.color = label.Color
} }
return nil return nil

View File

@ -122,22 +122,10 @@ func (store *Store) imapSendUpdate(update imapBackend.Update) {
return 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 { select {
case <-done:
case <-time.After(1 * time.Second): 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 return
case store.imapUpdates <- update:
} }
} }

View File

@ -142,6 +142,9 @@ func initMailboxBucket(tx *bolt.Tx, bucketName []byte) error {
if _, err := bucket.CreateBucketIfNotExists(apiIDsBucket); err != nil { if _, err := bucket.CreateBucketIfNotExists(apiIDsBucket); err != nil {
return err return err
} }
if _, err := bucket.CreateBucketIfNotExists(deletedIDsBucket); err != nil {
return err
}
return nil return nil
} }
@ -240,13 +243,7 @@ func (storeMailbox *Mailbox) txGetAPIIDsBucket(tx *bolt.Tx) *bolt.Bucket {
// txGetDeletedIDsBucket returns the bucket with messagesID marked as deleted // txGetDeletedIDsBucket returns the bucket with messagesID marked as deleted
func (storeMailbox *Mailbox) txGetDeletedIDsBucket(tx *bolt.Tx) *bolt.Bucket { func (storeMailbox *Mailbox) txGetDeletedIDsBucket(tx *bolt.Tx) *bolt.Bucket {
// There should be no error since it _...returns an error if the bucket return storeMailbox.txGetBucket(tx).Bucket(deletedIDsBucket)
// 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
} }
// txGetBucket returns the bucket of mailbox containing mapping buckets. // txGetBucket returns the bucket of mailbox containing mapping buckets.

View File

@ -125,6 +125,7 @@ func (mc *mailboxCounts) getPMLabel() *pmapi.Label {
return &pmapi.Label{ return &pmapi.Label{
ID: mc.LabelID, ID: mc.LabelID,
Name: mc.LabelName, Name: mc.LabelName,
Path: mc.LabelName,
Color: mc.Color, Color: mc.Color,
Order: mc.Order, Order: mc.Order,
Type: pmapi.LabelTypeMailbox, Type: pmapi.LabelTypeMailbox,
@ -158,7 +159,7 @@ func (store *Store) createOrUpdateMailboxCountsBuckets(labels []*pmapi.Label) er
} }
// Update mailbox info, but dont change on-API-counts. // Update mailbox info, but dont change on-API-counts.
mailbox.LabelName = label.Name mailbox.LabelName = label.Path
mailbox.Color = label.Color mailbox.Color = label.Color
mailbox.Order = label.Order mailbox.Order = label.Order
mailbox.IsFolder = label.Exclusive == 1 mailbox.IsFolder = label.Exclusive == 1

View File

@ -66,7 +66,7 @@ func (message *Message) Message() *pmapi.Message {
// mailbox // mailbox
func (message *Message) IsMarkedDeleted() bool { func (message *Message) IsMarkedDeleted() bool {
isMarkedAsDeleted := false 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 isMarkedAsDeleted = message.storeMailbox.txGetDeletedIDsBucket(tx).Get([]byte(message.msg.ID)) != nil
return nil return nil
}) })

View File

@ -152,6 +152,10 @@ func parseAddressComment(raw string) string {
} }
func parseAddressList(val string) (addrs []*mail.Address, err error) { func parseAddressList(val string) (addrs []*mail.Address, err error) {
if val == "" || val == "<>" {
return
}
addrs, err = mail.ParseAddressList(parseAddressComment(val)) addrs, err = mail.ParseAddressList(parseAddressComment(val))
if err == nil { if err == nil {
if addrs == nil { if addrs == nil {

View File

@ -103,7 +103,11 @@ func convertForeignEncodings(p *parser.Parser) error {
return p.ConvertToUTF8() return p.ConvertToUTF8()
}). }).
RegisterDefaultHandler(func(p *parser.Part) error { RegisterDefaultHandler(func(p *parser.Part) error {
t, _, _ := p.Header.ContentType() t, params, _ := p.ContentType()
// multipart/alternative, for example, can contain extra charset.
if params != nil && params["charset"] != "" {
return p.ConvertToUTF8()
}
logrus.WithField("type", t).Trace("Not converting part to utf-8") logrus.WithField("type", t).Trace("Not converting part to utf-8")
return nil return nil
}). }).
@ -293,7 +297,7 @@ func allPartsHaveContentType(parts parser.Parts, contentType string) bool {
} }
for _, part := range parts { for _, part := range parts {
t, _, err := part.Header.ContentType() t, _, err := part.ContentType()
if err != nil { if err != nil {
return false return false
} }
@ -329,7 +333,7 @@ func determineMIMEType(p *parser.Parser) (string, error) {
// getPlainBody returns the body of the given part, converting html to // getPlainBody returns the body of the given part, converting html to
// plaintext where possible. // plaintext where possible.
func getPlainBody(part *parser.Part) []byte { func getPlainBody(part *parser.Part) []byte {
contentType, _, err := part.Header.ContentType() contentType, _, err := part.ContentType()
if err != nil { if err != nil {
return part.Body return part.Body
} }

View File

@ -17,7 +17,9 @@
package parser package parser
import "regexp" import (
"regexp"
)
type HandlerFunc func(*Part) error type HandlerFunc func(*Part) error
@ -35,7 +37,7 @@ func (h *handler) matchType(p *Part) bool {
return false return false
} }
t, _, err := p.Header.ContentType() t, _, err := p.ContentType()
if err != nil { if err != nil {
t = "" t = ""
} }

View File

@ -40,6 +40,18 @@ type Part struct {
children Parts children Parts
} }
func (p *Part) ContentType() (string, map[string]string, error) {
t, params, err := p.Header.ContentType()
if err != nil {
// go-message's implementation of ContentType() doesn't handle duplicate parameters
// e.g. Content-Type: text/plain; charset=utf-8; charset=UTF-8
// so if it fails, we try again with pmmime's implementation, which does.
t, params, err = pmmime.ParseMediaType(p.Header.Get("Content-Type"))
}
return t, params, err
}
func (p *Part) Child(n int) (part *Part, err error) { func (p *Part) Child(n int) (part *Part, err error) {
if len(p.children) < n { if len(p.children) < n {
return nil, errors.New("no such part") return nil, errors.New("no such part")
@ -72,7 +84,7 @@ func (p *Part) AddChild(child *Part) {
func (p *Part) ConvertToUTF8() error { func (p *Part) ConvertToUTF8() error {
logrus.Trace("Converting part to utf-8") logrus.Trace("Converting part to utf-8")
t, params, err := p.Header.ContentType() t, params, err := p.ContentType()
if err != nil { if err != nil {
return err return err
} }
@ -102,7 +114,7 @@ func (p *Part) ConvertMetaCharset() error {
goquery.NewDocumentFromNode(doc).Find("meta").Each(func(n int, sel *goquery.Selection) { goquery.NewDocumentFromNode(doc).Find("meta").Each(func(n int, sel *goquery.Selection) {
if val, ok := sel.Attr("content"); ok { if val, ok := sel.Attr("content"); ok {
t, params, err := mime.ParseMediaType(val) t, params, err := pmmime.ParseMediaType(val)
if err != nil { if err != nil {
return return
} }
@ -163,7 +175,7 @@ func (p *Part) is7BitClean() bool {
} }
func (p *Part) isMultipartMixed() bool { func (p *Part) isMultipartMixed() bool {
t, _, err := p.Header.ContentType() t, _, err := p.ContentType()
if err != nil { if err != nil {
return false return false
} }

View File

@ -49,7 +49,7 @@ func TestPart(t *testing.T) {
part, err := p.Section(getSectionNumber(partNumber)) part, err := p.Section(getSectionNumber(partNumber))
require.NoError(t, err) require.NoError(t, err)
contType, _, err := part.Header.ContentType() contType, _, err := part.ContentType()
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, wantContType, contType) assert.Equal(t, wantContType, contType)
} }

View File

@ -58,7 +58,7 @@ func (v *Visitor) Visit() (interface{}, error) {
} }
func (v *Visitor) visit(p *Part) (interface{}, error) { func (v *Visitor) visit(p *Part) (interface{}, error) {
t, _, err := p.Header.ContentType() t, _, err := p.ContentType()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -224,6 +224,21 @@ func TestParseTextPlainWithPlainAttachment(t *testing.T) {
assert.Equal(t, readerToString(attReaders[0]), "attachment") assert.Equal(t, readerToString(attReaders[0]), "attachment")
} }
func TestParseTextPlainEmptyAddresses(t *testing.T) {
f := getFileReader("text_plain_empty_addresses.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextPlainWithImageInline(t *testing.T) { func TestParseTextPlainWithImageInline(t *testing.T) {
f := getFileReader("text_plain_image_inline.eml") f := getFileReader("text_plain_image_inline.eml")
@ -244,6 +259,21 @@ func TestParseTextPlainWithImageInline(t *testing.T) {
assert.Equal(t, 8, img.Height) assert.Equal(t, 8, img.Height)
} }
func TestParseTextPlainWithDuplicateCharset(t *testing.T) {
f := getFileReader("text_plain_duplicate_charset.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseWithMultipleTextParts(t *testing.T) { func TestParseWithMultipleTextParts(t *testing.T) {
f := getFileReader("multiple_text_parts.eml") f := getFileReader("multiple_text_parts.eml")
@ -416,6 +446,27 @@ func TestParseMultipartAlternativeNested(t *testing.T) {
assert.Equal(t, "*multipart 2.1*\n\n", plainBody) assert.Equal(t, "*multipart 2.1*\n\n", plainBody)
} }
func TestParseMultipartAlternativeLatin1(t *testing.T) {
f := getFileReader("multipart_alternative_latin1.eml")
m, _, plainBody, _, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"schizofrenic" <schizofrenic@pm.me>`, m.Sender.String())
assert.Equal(t, `<pmbridgeietest@outlook.com>`, m.ToList[0].String())
assert.Equal(t, `<html><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
</head>
<body>
<b>aoeuaoeu</b>
</body></html>`, m.Body)
assert.Equal(t, "*aoeuaoeu*\n\n", plainBody)
}
func getFileReader(filename string) io.Reader { func getFileReader(filename string) io.Reader {
f, err := os.Open(filepath.Join("testdata", filename)) f, err := os.Open(filepath.Join("testdata", filename))
if err != nil { if err != nil {

View File

@ -0,0 +1,30 @@
To: pmbridgeietest@outlook.com
From: schizofrenic <schizofrenic@pm.me>
Subject: aoeuaoeu
Date: Thu, 30 Jul 2020 13:35:24 +0200
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="------------22BC647264E52252E386881A"; charset="iso-8859-1"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------22BC647264E52252E386881A
Content-Type: text/plain
Content-Transfer-Encoding: 7bit
*aoeuaoeu*
--------------22BC647264E52252E386881A
Content-Type: text/html
Content-Transfer-Encoding: 7bit
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1">
</head>
<body>
<b>aoeuaoeu</b>
</body>
</html>
--------------22BC647264E52252E386881A--

View File

@ -0,0 +1,5 @@
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Content-Type: text/plain; charset=utf-8; charset=UTF-8
body

View File

@ -0,0 +1,6 @@
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
CC:
Reply-To: <>
body

View File

@ -80,6 +80,7 @@ const (
type Label struct { type Label struct {
ID string ID string
Name string Name string
Path string
Color string Color string
Order int `json:",omitempty"` Order int `json:",omitempty"`
Display int // Not used for now, leave it empty. Display int // Not used for now, leave it empty.

View File

@ -83,6 +83,9 @@ func (ctl *Controller) AddUserLabel(username string, label *pmapi.Label) error {
} }
label.ID = ctl.labelIDGenerator.next(prefix) label.ID = ctl.labelIDGenerator.next(prefix)
label.Name = labelName label.Name = labelName
if label.Path == "" {
label.Path = label.Name
}
ctl.labelsByUsername[username] = append(ctl.labelsByUsername[username], label) ctl.labelsByUsername[username] = append(ctl.labelsByUsername[username], label)
ctl.resetUsers() ctl.resetUsers()
return nil return nil

View File

@ -53,6 +53,9 @@ func (api *FakePMAPI) CreateLabel(label *pmapi.Label) (*pmapi.Label, error) {
prefix = "folder" prefix = "folder"
} }
label.ID = api.controller.labelIDGenerator.next(prefix) label.ID = api.controller.labelIDGenerator.next(prefix)
if label.Path == "" {
label.Path = label.Name
}
api.labels = append(api.labels, label) api.labels = append(api.labels, label)
api.addEventLabel(pmapi.EventCreate, label) api.addEventLabel(pmapi.EventCreate, label)
return label, nil 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. // Request doesn't have to include all properties and these have to stay the same.
label.Type = existingLabel.Type label.Type = existingLabel.Type
label.Exclusive = existingLabel.Exclusive label.Exclusive = existingLabel.Exclusive
if label.Path == "" {
label.Path = label.Name
}
api.labels[idx] = label api.labels[idx] = label
api.addEventLabel(pmapi.EventUpdate, label) api.addEventLabel(pmapi.EventUpdate, label)
return label, nil return label, nil

View File

@ -1,11 +1,11 @@
Feature: IMAP list mailboxes Feature: IMAP list mailboxes
Background: Background:
Given there is connected user "user" 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 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 When IMAP client lists mailboxes
Then IMAP response contains "INBOX" Then IMAP response contains "INBOX"
Then IMAP response contains "Sent" Then IMAP response contains "Sent"
@ -14,3 +14,16 @@ Feature: IMAP list mailboxes
Then IMAP response contains "All Mail" Then IMAP response contains "All Mail"
Then IMAP response contains "Folders/mbox1" Then IMAP response contains "Folders/mbox1"
Then IMAP response contains "Labels/mbox2" 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"

View File

@ -4,7 +4,6 @@ Feature: IMAP import messages
And there is IMAP client logged in as "user" And there is IMAP client logged in as "user"
And there is IMAP client selected in "INBOX" And there is IMAP client selected in "INBOX"
@ignore
Scenario: Import message with double charset in content type Scenario: Import message with double charset in content type
When IMAP client imports message to "INBOX" When IMAP client imports message to "INBOX"
""" """

View File

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