diff --git a/internal/imap/backend.go b/internal/imap/backend.go
index 697f4fa7..aff3778b 100644
--- a/internal/imap/backend.go
+++ b/internal/imap/backend.go
@@ -16,6 +16,19 @@
// along with ProtonMail Bridge. If not, see .
// Package imap provides IMAP server of the Bridge.
+//
+// Methods are called by the go-imap library in parallel.
+// Additional parallelism is achieved while handling each IMAP request.
+//
+// For example, ListMessages internally uses `fetchWorkers` workers to resolve each requested item.
+// When IMAP clients request message literals (or parts thereof), we sometimes need to build RFC822 message literals.
+// To do this, we pass build jobs to the message builder, which internally manages its own parallelism.
+// Summary:
+// - each IMAP fetch request is handled in parallel,
+// - within each IMAP fetch request, individual items are handled by a pool of `fetchWorkers` workers,
+// - within each worker, build jobs are posted to the message builder,
+// - the message builder handles build jobs using its own, independent worker pool,
+// The builder will handle jobs in parallel up to its own internal limit. This prevents it from overwhelming API.
package imap
import (
@@ -32,7 +45,8 @@ import (
)
const (
- // NOTE: Each fetch worker has its own set of attach workers so there can up to 20*5=100 API requests at once.
+ // NOTE: Each fetch worker has its own set of attach workers so there can be up to 20*5=100 API requests at once.
+ // This is a reasonable limit to not overwhelm API while still maintaining as much parallelism as possible.
fetchWorkers = 20 // In how many workers to fetch message (group list on IMAP).
attachWorkers = 5 // In how many workers to fetch attachments (for one message).
buildWorkers = 20 // In how many workers to build messages.
diff --git a/pkg/message/build_rfc822_custom.go b/pkg/message/build_rfc822_custom.go
index 477f0e27..07d56da4 100644
--- a/pkg/message/build_rfc822_custom.go
+++ b/pkg/message/build_rfc822_custom.go
@@ -28,6 +28,39 @@ import (
)
// writeCustomTextPart writes an armored-PGP text part for a message body that couldn't be decrypted.
+// The following is an example of such a message.
+/*
+Mime-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary=d8a04ef2e12150946d27f84fc82b9e70a5e314f91a0f2190e38c7aad623466da
+Message-Id:
+Date: Fri, 26 Mar 2021 14:10:00 +0000
+
+--d8a04ef2e12150946d27f84fc82b9e70a5e314f91a0f2190e38c7aad623466da
+Content-Type: text/plain
+
+-----BEGIN PGP MESSAGE-----
+Version: GopenPGP 2.1.3
+Comment: This message could not be decrypted: gopenpgp: error in reading message: openpgp: incorrect key
+
+wcBMA1iOyuyBImpHAQf/boKq75jyV5kGzl8ObcjLuaVrVEw5YbrI1M9gAb/5eq+t
+Mzd2qK0owYwBmuvYj3pk2b15miOIPfAYlbQVyIaseq/O4XchPvtJZKxh42eDcYhD
+zwqHrYxtCLOOBY6b15Aq8tVLORi4+/VCWJrTHtCdKq71UX7bvBdQRaZqOIJ+bUiP
+72IkaWjdVmCgLCmzPN3R3VKsmjTNBt1GRMc6vosQyoAeClyf94wgE1sXGsumXBQs
+MX69zK9a36Ya67PpQ9AGVR2peEF07s5O+t/PxkStWfFvMwnnOOSgdHfN0PSwkhoL
+EIwDA6lh7+2UlbX9pFi908PDl0OuoLgrL+NyleUPtdLAugGUa0U1fn6K1gwFGcsj
+DKflgAMXixhf83xpmz4fM3zzM988ecQsXozQcUDrxSTxzdfWXSaWQDWHguCiL0xM
+aPkf4kUn4RX3AoZVTC4GXvupzAQUqjJhyd0p7lJGOwIw0Ik77PxeCFNvWiZHJ3W+
+7PYuehSUgyDJJe7q1GFFcMUspnPmuvhbenKIHlKwGfptnXLRIW15sdcca2Etz5i4
+H71rHKwzAPdNHBQVvN0Nmt77RGAIy4Az8fRwz7hD7E92r4BkaaYntrQe9DUXYc50
+HVXyeFcvpfbEaK+fObxk1ljig+Io2liPx7pn/YBFytMy1mlwhjroMPjMUXuTXqje
+NHnOJVxnnXMwGh34wUIEwOmTskJoGkQjQbjvixkwNRGnLzIHuD6614pXtgTDtBzc
+AKuQssqOky1Q7KjdusMWnoVL7gTbSrIUxXoKChear5XgPzFevQhQNEfTI8DDMbYR
+La1Cc6aFtMPUShzNc3Wp3dVLM09WygEUsYZFmA==
+=00ma
+-----END PGP MESSAGE-----
+--d8a04ef2e12150946d27f84fc82b9e70a5e314f91a0f2190e38c7aad623466da--
+*/
func writeCustomTextPart(
w *message.Writer,
msg *pmapi.Message,
diff --git a/pkg/message/build_test.go b/pkg/message/build_test.go
index 82e909b3..b5f92d93 100644
--- a/pkg/message/build_test.go
+++ b/pkg/message/build_test.go
@@ -326,6 +326,132 @@ func TestBuildSignedHTMLEncryptedMessageWithPubKey(t *testing.T) {
expectContentDispositionParam(`filename`, is(`OpenPGP_signature`))
}
+func TestBuildSignedMultipartAlternativeEncryptedMessageWithPubKey(t *testing.T) {
+ m := gomock.NewController(t)
+ defer m.Finish()
+
+ b := NewBuilder(1, 1, 1)
+ defer b.Done()
+
+ body := readerToString(getFileReader("pgp-mime-body-signed-multipart-alternative-with-pubkey.eml"))
+
+ kr := tests.MakeKeyRing(t)
+ msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
+
+ res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
+ require.NoError(t, err)
+
+ section(t, res).
+ expectContentType(is(`multipart/mixed`)).
+ expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
+
+ section(t, res, 1).
+ expectContentType(is(`multipart/signed`)).
+ expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
+ expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
+
+ section(t, res, 1, 1).
+ expectContentType(is(`multipart/mixed`)).
+ expectContentTypeParam(`protected-headers`, is(`v1`)).
+ expectHeader(`Subject`, is(`Alternative`)).
+ expectHeader(`From`, is(`"pm.bridge.qa" `)).
+ expectHeader(`To`, is(`schizofrenic@pm.me`)).
+ expectSection(verifiesAgainst(section(t, res, 1, 1, 1, 3).pubKey(), section(t, res, 1, 2).signature()))
+
+ section(t, res, 1, 1, 1).
+ expectContentType(is(`multipart/mixed`))
+
+ section(t, res, 1, 1, 1, 1).
+ expectContentType(is(`multipart/alternative`))
+
+ section(t, res, 1, 1, 1, 1, 1).
+ expectContentType(is(`text/plain`)).
+ expectBody(contains(`This Rich formated text`)).
+ expectBody(contains(`What kind of shoes do ninjas wear`)).
+ expectBody(contains(`How does a penguin build its house`))
+
+ section(t, res, 1, 1, 1, 1, 2).
+ expectContentType(is(`text/html`)).
+ expectBody(contains(`This Rich formated text`)).
+ expectBody(contains(`What kind of shoes do ninjas wear`)).
+ expectBody(contains(`How does a penguin build its house`))
+
+ section(t, res, 1, 1, 1, 2).
+ expectContentType(is(`application/pdf`)).
+ expectTransferEncoding(is(`base64`)).
+ expectContentTypeParam(`name`, is(`minimal.pdf`)).
+ expectContentDispositionParam(`filename`, is(`minimal.pdf`))
+
+ section(t, res, 1, 1, 1, 3).
+ expectContentType(is(`application/pgp-keys`)).
+ expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)).
+ expectContentDisposition(is(`attachment`)).
+ expectContentDispositionParam(`filename`, is(`OpenPGP_0x161C0875822359F7.asc`))
+
+ section(t, res, 1, 2).
+ expectContentType(is(`application/pgp-signature`)).
+ expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
+ expectContentDisposition(is(`attachment`)).
+ expectContentDispositionParam(`filename`, is(`OpenPGP_signature`))
+}
+
+func TestBuildSignedEmbeddedMessageRFC822EncryptedMessageWithPubKey(t *testing.T) {
+ m := gomock.NewController(t)
+ defer m.Finish()
+
+ b := NewBuilder(1, 1, 1)
+ defer b.Done()
+
+ body := readerToString(getFileReader("pgp-mime-body-signed-embedded-message-rfc822-with-pubkey.eml"))
+
+ kr := tests.MakeKeyRing(t)
+ msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
+
+ res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
+ require.NoError(t, err)
+
+ section(t, res).
+ expectContentType(is(`multipart/mixed`)).
+ expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
+
+ section(t, res, 1).
+ expectContentType(is(`multipart/signed`)).
+ expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
+ expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
+
+ section(t, res, 1, 1).
+ expectContentType(is(`multipart/mixed`)).
+ expectContentTypeParam(`protected-headers`, is(`v1`)).
+ expectHeader(`Subject`, is(`Fwd: HTML with attachment external PGP`)).
+ expectHeader(`From`, is(`"pm.bridge.qa" `)).
+ expectHeader(`To`, is(`schizofrenic@pm.me`)).
+ expectSection(verifiesAgainst(section(t, res, 1, 1, 1, 2).pubKey(), section(t, res, 1, 2).signature()))
+
+ section(t, res, 1, 1, 1).
+ expectContentType(is(`multipart/mixed`))
+
+ section(t, res, 1, 1, 1, 1).
+ expectContentType(is(`text/plain`))
+
+ section(t, res, 1, 1, 1, 2).
+ expectContentType(is(`application/pgp-keys`)).
+ expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)).
+ expectContentDisposition(is(`attachment`)).
+ expectContentDispositionParam(`filename`, is(`OpenPGP_0x161C0875822359F7.asc`))
+
+ section(t, res, 1, 1, 1, 3).
+ expectContentType(is(`message/rfc822`)).
+ expectContentTypeParam(`name`, is(`HTML with attachment external PGP.eml`)).
+ expectContentDisposition(is(`attachment`)).
+ expectContentDispositionParam(`filename`, is(`HTML with attachment external PGP.eml`))
+
+ section(t, res, 1, 2).
+ expectContentType(is(`application/pgp-signature`)).
+ expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
+ expectContentDisposition(is(`attachment`)).
+ expectContentDispositionParam(`filename`, is(`OpenPGP_signature`))
+}
+
func TestBuildHTMLMessageWithAttachment(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
diff --git a/pkg/message/testdata/pgp-mime-body-signed-embedded-message-rfc822-with-pubkey.eml b/pkg/message/testdata/pgp-mime-body-signed-embedded-message-rfc822-with-pubkey.eml
new file mode 100644
index 00000000..69a0c258
--- /dev/null
+++ b/pkg/message/testdata/pgp-mime-body-signed-embedded-message-rfc822-with-pubkey.eml
@@ -0,0 +1,212 @@
+Content-Type: multipart/signed; micalg=pgp-sha256;
+ protocol="application/pgp-signature";
+ boundary="Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS"
+
+This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
+--Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS
+Content-Type: multipart/mixed; boundary="avFkF0LAPYPXcFHcnsgGmACbGIPeVDdYc";
+ protected-headers="v1"
+Subject: Fwd: HTML with attachment external PGP
+From: "pm.bridge.qa"
+To: schizofrenic@pm.me
+Message-ID: <7c04869b-c470-116f-b8e5-8b4fd5e1195d@gmail.com>
+References:
+In-Reply-To:
+
+--avFkF0LAPYPXcFHcnsgGmACbGIPeVDdYc
+Content-Type: multipart/mixed;
+ boundary="------------2F19EE9A8A1A6F779F5D14AF"
+Content-Language: en-US
+
+This is a multi-part message in MIME format.
+--------------2F19EE9A8A1A6F779F5D14AF
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+
+
+--------------2F19EE9A8A1A6F779F5D14AF
+Content-Type: application/pgp-keys;
+ name="OpenPGP_0x161C0875822359F7.asc"
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: attachment;
+ filename="OpenPGP_0x161C0875822359F7.asc"
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ=
+pDh
+I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT=
+f4S
+PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x=
+Snd
+NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB=
+OfN
+H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe=
+XUt
+RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB=
+BYC
+AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9=
+/K8
+B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7=
+Vcz
+1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO=
+V0U
+u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7=
+6Pa
+4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj=
+TVQ
+IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3=
+D07
+kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF=
+88F
+yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa=
+knm
+3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk=
+utT
+ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr=
+8RB
+owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY=
+C32
+lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm=
+L6H
+jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ=
+xI5
+RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S=
+osO
+HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx=
+Etv
+Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u
+=3Dv/1p
+-----END PGP PUBLIC KEY BLOCK-----
+
+--------------2F19EE9A8A1A6F779F5D14AF
+Content-Type: message/rfc822;
+ name="HTML with attachment external PGP.eml"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment;
+ filename="HTML with attachment external PGP.eml"
+
+Delivered-To: pm.bridge.qa@gmail.com
+Received: by 2002:a17:906:a051:0:0:0:0 with SMTP id bg17csp66709ejb;
+ Wed, 24 Mar 2021 22:03:32 -0700 (PDT)
+X-Google-Smtp-Source: ABdhPJxllBuHnnJzKWy77R291tZbVFVk0iahkLm1TQsluEYTvyAXdOWB/zp1y10e60UlGGZYH3YF
+X-Received: by 2002:a05:6000:118c:: with SMTP id g12mr6758087wrx.353.1616648612550;
+ Wed, 24 Mar 2021 22:03:32 -0700 (PDT)
+ARC-Seal: i=1; a=rsa-sha256; t=1616648612; cv=none;
+ d=google.com; s=arc-20160816;
+ b=Jf4vmKEoeJQ3rIDMbI2twiDkfn50ejNnqIbs2nkaFruITcw6XhvhbcfV9HLC80Yt8E
+ tfN7TV9qoBneSWzfSJ+Sqw31hBKKtKpMhuqZT9GPzBN5gdMJKj5ISAQ8Lgm9zvR3Zbjn
+ N0nOzCu/oT1amMMm+48hpKj8VL2tydjvNG+g/a5lk1Aw7JdqIKV6t1XhsyyYaa1O+NFC
+ rQThdalcQj2NjoZWba1mjZSzI7B7hJdZg5d+jado2TPMQXe2kz2wGmr3+/JcKvPJjrSA
+ S+jzhpjcd7ZnctkzTfpsdlBJAGKoDBnSvQc3eMJ/AgRHFc+5ks5nRDt/1DowSjQ7i7rp
+ 4a+g==
+ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
+ h=mime-version:message-id:subject:reply-to:from:to:dkim-signature
+ :date;
+ bh=vmJ0JT+IfeO4idMYP7zPvldBkdONjKTXWTp7ly/B9qk=;
+ b=f8VY+ajsE/XNYrqD666FM0WCtNEQtUyU/Zh3pFCI9sFrMnAui4Qp9Gs1fe/8HLxt2v
+ /C4l4eHELvPBv4vX0KtUvOlRZYPZbLZCNdtTcFtiuZEKUHWx370p7yyMWcmSMdlUbq4J
+ NrKMPGfaYiZe5Rt3MyD5RKm4RJpqvep34VCHMYtoFQP/0Po4/1JMDw0Fy6SXUJ54rBRw
+ bmzqNNBkonda3YghhK3WNrxTxzZ8I7KW9YdpENNS9ewJLeVtFQKdiLZwz5EpMZxOxG0I
+ LW0jRtDlmZnqRe7bvTAo51IuLf9okHRI8PRiK0UHl+4Vr5Igq4mub7Ee8pC/Nz3Yj29G
+ KODw==
+ARC-Authentication-Results: i=1; mx.google.com;
+ dkim=pass header.i=@protonmail.com header.s=protonmail header.b=EX07e46H;
+ spf=pass (google.com: domain of bridge-test-user@protonmail.com designates 185.70.40.22 as permitted sender) smtp.mailfrom=bridge-test-user@protonmail.com;
+ dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=protonmail.com
+Return-Path:
+Received: from mail2.protonmail.ch (mail2.protonmail.ch. [185.70.40.22])
+ by mx.google.com with ESMTPS id g6si2999785wrr.110.2021.03.24.22.03.32
+ for
+ (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
+ Wed, 24 Mar 2021 22:03:32 -0700 (PDT)
+Received-SPF: pass (google.com: domain of bridge-test-user@protonmail.com designates 185.70.40.22 as permitted sender) client-ip=185.70.40.22;
+Authentication-Results: mx.google.com;
+ dkim=pass header.i=@protonmail.com header.s=protonmail header.b=EX07e46H;
+ spf=pass (google.com: domain of bridge-test-user@protonmail.com designates 185.70.40.22 as permitted sender) smtp.mailfrom=bridge-test-user@protonmail.com;
+ dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=protonmail.com
+Date: Thu, 25 Mar 2021 05:03:27 +0000
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonmail.com;
+ s=protonmail; t=1616648611;
+ bh=vmJ0JT+IfeO4idMYP7zPvldBkdONjKTXWTp7ly/B9qk=;
+ h=Date:To:From:Reply-To:Subject:From;
+ b=EX07e46H5/HmotAWZ69I4qa5jCVRao/p3KEM3eQn/AQ8s+cLMaR5b2ozdHrPCsTw5
+ i5b1DLUHZHBf+6Ven47WJfKNwLUfkAGD2P0aI/dAk/h/h0Bg4Ni85pv+uPpRHLNQKv
+ T3VnDP9MSwl6IUJu5zoM2EC70MLoiHS07lxhM2pw=
+To: External Bridge
+From: Bridge Test
+Reply-To: Bridge Test
+Subject: HTML with attachment external PGP
+Message-ID:
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA"
+X-Spam-Status: No, score=-1.2 required=10.0 tests=ALL_TRUSTED,DKIM_SIGNED,
+ DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,FREEMAIL_FROM,HTML_MESSAGE
+ shortcircuit=no autolearn=disabled version=3.4.4
+X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on
+ mailout.protonmail.ch
+
+This is a multi-part message in MIME format.
+
+--b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA
+Content-Type: multipart/alternative;
+ boundary="b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA"
+
+--b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+VGhpcyBpcyBib2R5IG9mIEhUTUwgbWFpbCB3aXRoIGF0dGFjaG1lbnQ=
+
+--b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: base64
+
+PGh0bWw+PGhlYWQ+PC9oZWFkPjxib2R5PlRoaXMgaXMgYm9keSBvZiA8Yj5IVE1MIG1haWw8L2I+
+IHdpdGggYXR0YWNobWVudA0KPC9ib2R5PjwvaHRtbD4=
+
+
+--b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA--
+
+--b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA
+Content-Type: image/png; name=outline-light-instagram-48.png
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=outline-light-instagram-48.png
+
+iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD/////////////////
+//////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd
+OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg
+WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv
+1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I
+BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6
+9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0
+YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP
+HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy
+KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg==
+
+--b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA--
+
+
+--------------2F19EE9A8A1A6F779F5D14AF--
+
+--avFkF0LAPYPXcFHcnsgGmACbGIPeVDdYc--
+
+--Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS
+Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
+Content-Description: OpenPGP digital signature
+Content-Disposition: attachment; filename="OpenPGP_signature"
+
+-----BEGIN PGP SIGNATURE-----
+
+wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBciIAFAwAAAAAACgkQFhwIdYIjWfcN
+ZQf+NzAoEJRTSW5JFNgSGkwLsH89wAbw3wEt4PYuZaa+35xBuU8Sojm1oLOyuPkIasQf98Iu5P1o
+8cokViEa6wm+ZZpcFMi6T2/3+UNlSm81Epm7GrFyjAFTWrdTPLb4k4x47sz77RoTp/UEwm/7fVI5
+gMYhQyIYaocXHmDk61UshWE9q/Po6qjHBnnWS8YBnhUS9lK8uimpfRO9UQ9bIUjIYDGDPAtBoYnb
+X9V4SjBvbbdNrgoVaDxPw6HYCb3RhzRXunr5Icdnjfbc2H40/FayVi/p7GzFh+8zv/TzRxMkHo72
+DBsONaC7r8bxQ9BwJvpmWufqL7ZXHfVXQ6z+M43e1Q==
+=Stx+
+-----END PGP SIGNATURE-----
+
+--Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS--
diff --git a/pkg/message/testdata/pgp-mime-body-signed-multipart-alternative-with-pubkey.eml b/pkg/message/testdata/pgp-mime-body-signed-multipart-alternative-with-pubkey.eml
new file mode 100644
index 00000000..cfbf3434
--- /dev/null
+++ b/pkg/message/testdata/pgp-mime-body-signed-multipart-alternative-with-pubkey.eml
@@ -0,0 +1,161 @@
+Content-Type: multipart/signed; micalg=pgp-sha256;
+ protocol="application/pgp-signature";
+ boundary="MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM"
+
+This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
+--MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM
+Content-Type: multipart/mixed; boundary="FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378";
+ protected-headers="v1"
+Subject: Alternative
+From: "pm.bridge.qa"
+To: schizofrenic@pm.me
+Message-ID: <753d0314-0286-2c88-2abb-f8080ac7a4cb@gmail.com>
+
+--FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378
+Content-Type: multipart/mixed;
+ boundary="------------F97C8ED4878E94675762AE43"
+Content-Language: en-US
+
+This is a multi-part message in MIME format.
+--------------F97C8ED4878E94675762AE43
+Content-Type: multipart/alternative;
+ boundary="------------041318B15DD3FA540FED32C6"
+
+
+--------------041318B15DD3FA540FED32C6
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+This Rich formated text
+
+ * /What kind of shoes do ninjas wear? /*Sneakers!*
+ * /How does a penguin build its house?/**_/*Igloos it together.*/_
+
+
+
+
+--------------041318B15DD3FA540FED32C6
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: quoted-printable
+
+
+
+
+
+
+
+ This Rich formated text
+
+ - What kind of shoes do ninjas wear? Sneakers!
=
+
+ - How does a penguin build its house? Iglo=
+os
+ it together.
+
+
+
+
+
+
+
+
+--------------041318B15DD3FA540FED32C6--
+
+--------------F97C8ED4878E94675762AE43
+Content-Type: application/pdf;
+ name="minimal.pdf"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="minimal.pdf"
+
+JVBERi0xLjEKJcKlwrHDqwoKMSAwIG9iagogIDw8IC9UeXBlIC9DYXRhbG9nCiAgICAgL1Bh
+Z2VzIDIgMCBSCiAgPj4KZW5kb2JqCgoyIDAgb2JqCiAgPDwgL1R5cGUgL1BhZ2VzCiAgICAg
+L0tpZHMgWzMgMCBSXQogICAgIC9Db3VudCAxCiAgICAgL01lZGlhQm94IFswIDAgMzAwIDE0
+NF0KICA+PgplbmRvYmoKCjMgMCBvYmoKICA8PCAgL1R5cGUgL1BhZ2UKICAgICAgL1BhcmVu
+dCAyIDAgUgogICAgICAvUmVzb3VyY2VzCiAgICAgICA8PCAvRm9udAogICAgICAgICAgIDw8
+IC9GMQogICAgICAgICAgICAgICA8PCAvVHlwZSAvRm9udAogICAgICAgICAgICAgICAgICAv
+U3VidHlwZSAvVHlwZTEKICAgICAgICAgICAgICAgICAgL0Jhc2VGb250IC9UaW1lcy1Sb21h
+bgogICAgICAgICAgICAgICA+PgogICAgICAgICAgID4+CiAgICAgICA+PgogICAgICAvQ29u
+dGVudHMgNCAwIFIKICA+PgplbmRvYmoKCjQgMCBvYmoKICA8PCAvTGVuZ3RoIDU1ID4+CnN0
+cmVhbQogIEJUCiAgICAvRjEgMTggVGYKICAgIDAgMCBUZAogICAgKEhlbGxvIFdvcmxkKSBU
+agogIEVUCmVuZHN0cmVhbQplbmRvYmoKCnhyZWYKMCA1CjAwMDAwMDAwMDAgNjU1MzUgZiAK
+MDAwMDAwMDAxOCAwMDAwMCBuIAowMDAwMDAwMDc3IDAwMDAwIG4gCjAwMDAwMDAxNzggMDAw
+MDAgbiAKMDAwMDAwMDQ1NyAwMDAwMCBuIAp0cmFpbGVyCiAgPDwgIC9Sb290IDEgMCBSCiAg
+ICAgIC9TaXplIDUKICA+PgpzdGFydHhyZWYKNTY1CiUlRU9GCg==
+--------------F97C8ED4878E94675762AE43
+Content-Type: application/pgp-keys;
+ name="OpenPGP_0x161C0875822359F7.asc"
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: attachment;
+ filename="OpenPGP_0x161C0875822359F7.asc"
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ=
+pDh
+I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT=
+f4S
+PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x=
+Snd
+NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB=
+OfN
+H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe=
+XUt
+RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB=
+BYC
+AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9=
+/K8
+B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7=
+Vcz
+1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO=
+V0U
+u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7=
+6Pa
+4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj=
+TVQ
+IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3=
+D07
+kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF=
+88F
+yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa=
+knm
+3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk=
+utT
+ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr=
+8RB
+owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY=
+C32
+lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm=
+L6H
+jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ=
+xI5
+RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S=
+osO
+HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx=
+Etv
+Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u
+=3Dv/1p
+-----END PGP PUBLIC KEY BLOCK-----
+
+--------------F97C8ED4878E94675762AE43--
+
+--FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378--
+
+--MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM
+Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
+Content-Description: OpenPGP digital signature
+Content-Disposition: attachment; filename="OpenPGP_signature"
+
+-----BEGIN PGP SIGNATURE-----
+
+wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBciUoFAwAAAAAACgkQFhwIdYIjWfez
+rgf+NZCibnCUTovpWRVRiiPQtBPGeHUPEwz2xq2zz4AaqrHC2v4mYUIPe6am7INk8fkBLsa8Dj/A
+UN/28Qh7tNb7JsXtHDT4PIoXszukQ8VIRbe09mSkkP6jR4WzNR166d6n3rSxzHpviOyQldjjpOMr
+Zl7LxmgGr4ojsgCf6pvurWwCCOGJqbSusrD6JVv6DsmPmmQeBmnlTK/0oG9pnlNkugpNB1WS2K5d
+RY6+kWkSrxbq95HrgILpHip8Y/+ITWvQocm14PBIAAdW8Hr7iFQLETFJ/KDA+VP19Bt8n4Kitdi8
+DPqMsV0oOhATqBjnD63AePJ0VWg8R1z6GEK5A+WOpg==
+=Bc6p
+-----END PGP SIGNATURE-----
+
+--MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM--