diff --git a/go.mod b/go.mod index 607832f0..bfb16c19 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.0 github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7 github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a - github.com/ProtonMail/go-proton-api v0.4.1-0.20231129105641-0ee691e470f2 + github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366 github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton github.com/PuerkitoBio/goquery v1.8.1 github.com/abiosoft/ishell v2.0.0+incompatible diff --git a/go.sum b/go.sum index f544e00d..348176e4 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,7 @@ github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a h1:eQO/G github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= @@ -38,8 +39,8 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= -github.com/ProtonMail/go-proton-api v0.4.1-0.20231129105641-0ee691e470f2 h1:LxSqvOInBW9QBJqx2p4tzVHiOw/i3gFvjUzMTE9h1Uc= -github.com/ProtonMail/go-proton-api v0.4.1-0.20231129105641-0ee691e470f2/go.mod h1:t+hb0BfkmZ9fpvzVRpHC7limoowym6ln/j0XL9a8DDw= +github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366 h1:W9P5GdDnuGkB3tbzKnXmUrTjIs6zk/K+4lpPTWzsoRE= +github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366/go.mod h1:t+hb0BfkmZ9fpvzVRpHC7limoowym6ln/j0XL9a8DDw= github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8= github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI= @@ -116,8 +117,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VRvi4= -github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542 h1:IFTm6NBbfSgZCaeEzorQhH4T7ZERl4j+1u7oXWzmJcM= github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ= github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= @@ -178,6 +181,7 @@ github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -282,7 +286,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng= @@ -327,6 +330,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= diff --git a/internal/services/imapservice/connector.go b/internal/services/imapservice/connector.go index a09869fa..fb3b9625 100644 --- a/internal/services/imapservice/connector.go +++ b/internal/services/imapservice/connector.go @@ -678,17 +678,27 @@ func (s *Connector) importMessage( if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error { var messageID string - + p, err2 := parser.New(bytes.NewReader(literal)) + if err2 != nil { + return fmt.Errorf("failed to parse literal: %w", err2) + } if slices.Contains(labelIDs, proton.DraftsLabel) { - msg, err := s.createDraft(ctx, literal, addrKR, addr) + msg, err := s.createDraftWithParser(ctx, p, addrKR, addr) if err != nil { return fmt.Errorf("failed to create draft: %w", err) } // apply labels - messageID = msg.ID } else { + // multipart body requires at least one text part to be properly encrypted. + if p.AttachEmptyTextPartIfNoneExists() { + buf := new(bytes.Buffer) + if err := p.NewWriter().Write(buf); err != nil { + return fmt.Errorf("failed build new MIMEBody: %w", err) + } + literal = buf.Bytes() + } str, err := s.client.ImportMessages(ctx, addrKR, 1, 1, []proton.ImportReq{{ Metadata: proton.ImportMetadata{ AddressID: s.addrID, @@ -728,13 +738,7 @@ func (s *Connector) importMessage( return toIMAPMessage(full.MessageMetadata), literal, nil } -func (s *Connector) createDraft(ctx context.Context, literal []byte, addrKR *crypto.KeyRing, sender proton.Address) (proton.Message, error) { - // Create a new message parser from the reader. - parser, err := parser.New(bytes.NewReader(literal)) - if err != nil { - return proton.Message{}, fmt.Errorf("failed to create parser: %w", err) - } - +func (s *Connector) createDraftWithParser(ctx context.Context, parser *parser.Parser, addrKR *crypto.KeyRing, sender proton.Address) (proton.Message, error) { message, err := message.ParseWithParser(parser, true) if err != nil { return proton.Message{}, fmt.Errorf("failed to parse message: %w", err) diff --git a/pkg/message/parser/parser.go b/pkg/message/parser/parser.go index a06e8655..7923c6fa 100644 --- a/pkg/message/parser/parser.go +++ b/pkg/message/parser/parser.go @@ -84,7 +84,7 @@ func (p *Parser) AttachPublicKey(key, keyName string) { }) } -func (p *Parser) AttachEmptyTextPartIfNoneExists() { +func (p *Parser) AttachEmptyTextPartIfNoneExists() bool { root := p.Root() if root.isMultipartMixed() { for _, v := range root.children { @@ -95,14 +95,14 @@ func (p *Parser) AttachEmptyTextPartIfNoneExists() { contentType, _, err := v.Header.ContentType() if err == nil && strings.HasPrefix(contentType, "text/") { // Message already has text part - return + return false } } } else { contentType, _, err := root.Header.ContentType() if err == nil && strings.HasPrefix(contentType, "text/") { // Message already has text part - return + return false } } @@ -115,6 +115,7 @@ func (p *Parser) AttachEmptyTextPartIfNoneExists() { Header: h, Body: nil, }) + return true } // Section returns the message part referred to by the given section. A section diff --git a/tests/features/imap/message/import.feature b/tests/features/imap/message/import.feature index c1f359ba..ad0cf1bb 100644 --- a/tests/features/imap/message/import.feature +++ b/tests/features/imap/message/import.feature @@ -440,19 +440,24 @@ Feature: IMAP import messages "from": "Bridge Second Test ", "subject": "MESSAGE WITH REMOTE CONTENT", "content": { - "content-type": "multipart/alternative", + "content-type": "multipart/mixed", "sections":[ { - "content-type": "text/plain", - "content-type-charset": "utf-8", - "transfer-encoding": "7bit", - "body-is": "Remote content\n\n\nBridge\n\n\nRemote content" - }, - { - "content-type": "text/html", - "content-type-charset": "utf-8", - "transfer-encoding": "7bit", - "body-is": "\n\n \n\n \n \n \n

Remote content

\n


\n

\n

\n


\n

\n

Remote content
\n

\n
\n \n" + "content-type": "multipart/alternative", + "sections":[ + { + "content-type": "text/plain", + "content-type-charset": "utf-8", + "transfer-encoding": "7bit", + "body-is": "Remote content\n\n\nBridge\n\n\nRemote content" + }, + { + "content-type": "text/html", + "content-type-charset": "utf-8", + "transfer-encoding": "7bit", + "body-is": "\n\n \n\n \n \n \n

Remote content

\n


\n

\n

\n


\n

\n

Remote content
\n

\n
\n \n" + } + ] } ] } @@ -529,8 +534,8 @@ Feature: IMAP import messages { "content-type": "text/html", "content-type-charset": "utf-8", - "transfer-encoding": "quoted-printable", - "body-is": "\r\n\r\n\r\n\r\n\r\n


\r\n

\r\n

Behold! An inline 3D\"\"\r\nwidth=3D\"24\"
\r\n

\r\n\r\n" + "transfer-encoding": "7bit", + "body-is": "\n\n\n\n\n


\n

\n

Behold! An inline \"\"\nwidth=\"24\"
\n

\n\n" }, { "content-type": "image/gif", @@ -546,3 +551,116 @@ Feature: IMAP import messages } } """ + + Scenario: Message import with text part and attachment + When IMAP client "1" appends the following message to "INBOX": + """ + From: Bridge Test + Date: 01 Jan 1980 00:00:00 +0000 + To: Internal Bridge + Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000 + Subject: Message import with text part + Content-Type: multipart/mixed; boundary="BOUNDARY" + + This is a multi-part message in MIME format. + + --BOUNDARY + Content-Type: text/plain; charset=utf-8; format=flowed + Content-Transfer-Encoding: 7bit + + Hello World + + --BOUNDARY + Content-Disposition: attachment; filename=image.png + Content-Transfer-Encoding: base64 + Content-Type: image/png + + iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD///////////////// + //////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd + OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg + WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv + 1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I + BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6 + 9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0 + YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP + HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy + KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg== + + --BOUNDARY-- + """ + Then it succeeds + And IMAP client "1" eventually sees the following message in "INBOX" with this structure: + """ + { + "from": "Bridge Test ", + "date": "01 Jan 80 00:00 +0000", + "to": "Internal Bridge ", + "subject": "Message import with text part", + "content": { + "content-type": "multipart/mixed", + "sections":[ + { + "content-type": "text/plain", + "body-is": "Hello World" + }, + { + "content-type": "image/png" + } + ] + } + } + """ + + + Scenario: Message import without text part + When IMAP client "1" appends the following message to "INBOX": + """ + From: Bridge Test + Date: 01 Jan 1980 00:00:00 +0000 + To: Internal Bridge + Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000 + Subject: Message import without text part + Content-Type: multipart/mixed; boundary="BOUNDARY" + + This is a multi-part message in MIME format. + + --BOUNDARY + Content-Disposition: attachment; filename=image.png + Content-Transfer-Encoding: base64 + Content-Type: image/png + + iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD///////////////// + //////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd + OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg + WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv + 1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I + BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6 + 9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0 + YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP + HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy + KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg== + + --BOUNDARY-- + """ + Then it succeeds + And IMAP client "1" eventually sees the following message in "INBOX" with this structure: + """ + { + "from": "Bridge Test ", + "date": "01 Jan 80 00:00 +0000", + "to": "Internal Bridge ", + "subject": "Message import without text part", + "content": { + "content-type": "multipart/mixed", + "sections":[ + { + "content-type": "text/plain", + "body-is": "" + }, + { + "content-type": "image/png" + } + ] + } + } + """ \ No newline at end of file