Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/mail.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--css/html-response.css7
-rwxr-xr-xlib/Controller/MessagesController.php6
-rw-r--r--lib/Http/HtmlResponse.php27
-rw-r--r--package-lock.json5
-rw-r--r--package.json1
-rw-r--r--src/components/Message.vue7
-rw-r--r--src/components/MessageHTMLBody.vue38
-rw-r--r--src/components/NewMessageDetail.vue2
-rw-r--r--src/components/Thread.vue1
-rw-r--r--src/components/ThreadEnvelope.vue10
-rw-r--r--src/html-response.js38
-rw-r--r--tests/Unit/Controller/MessagesControllerTest.php31
-rw-r--r--tests/Unit/Http/HtmlResponseTest.php16
-rw-r--r--webpack.common.js3
14 files changed, 161 insertions, 31 deletions
diff --git a/css/html-response.css b/css/html-response.css
new file mode 100644
index 000000000..2b5c86d04
--- /dev/null
+++ b/css/html-response.css
@@ -0,0 +1,7 @@
+* {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', Arial, 'Noto Color Emoji', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+}
+
+body {
+ color: var(--color-main-text);
+}
diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php
index b9882970b..9c4dbb62c 100755
--- a/lib/Controller/MessagesController.php
+++ b/lib/Controller/MessagesController.php
@@ -336,12 +336,13 @@ class MessagesController extends Controller {
* @TrapError
*
* @param int $id
+ * @param bool $plain do not inject scripts if true (default=false)
*
* @return HtmlResponse|TemplateResponse
*
* @throws ClientException
*/
- public function getHtmlBody(int $id): Response {
+ public function getHtmlBody(int $id, bool $plain=false): Response {
try {
try {
$message = $this->mailManager->getMessage($this->currentUserId, $id);
@@ -364,7 +365,8 @@ class MessagesController extends Controller {
true
)->getHtmlBody(
$id
- )
+ ),
+ $plain
);
// Harden the default security policy
diff --git a/lib/Http/HtmlResponse.php b/lib/Http/HtmlResponse.php
index 9fbc6cdc3..5918dc16a 100644
--- a/lib/Http/HtmlResponse.php
+++ b/lib/Http/HtmlResponse.php
@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace OCA\Mail\Http;
+use OCP\Util;
use OCP\AppFramework\Http\Response;
class HtmlResponse extends Response {
@@ -32,22 +33,32 @@ class HtmlResponse extends Response {
/** @var string */
private $content;
- private $injectedStyles = <<<EOF
-* { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', Arial, 'Noto Color Emoji', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; }
-EOF;
+ /** @var bool */
+ private $plain;
-
- public function __construct(string $content) {
+ /**
+ * @param string $content message html content
+ * @param bool $plain do not inject scripts if true (default=false)
+ */
+ public function __construct(string $content, bool $plain=false) {
parent::__construct();
$this->content = $content;
+ $this->plain = $plain;
}
/**
- * Simply sets the headers and returns the file contents
+ * Inject scripts if not plain and return message html content.
*
- * @return string the file contents
+ * @return string message html content
*/
public function render(): string {
- return '<style>' . $this->injectedStyles . '</style>' . $this->content;
+ if ($this->plain) {
+ return $this->content;
+ }
+
+ $nonce = \OC::$server->getContentSecurityPolicyNonceManager()->getNonce();
+ $scriptSrc = Util::linkToAbsolute('mail', 'js/htmlresponse.js');
+ return '<script nonce="' . $nonce. '" src="' . $scriptSrc . '"></script>'
+ . $this->content;
}
}
diff --git a/package-lock.json b/package-lock.json
index eef5b54a7..0dd110791 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8024,6 +8024,11 @@
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true
},
+ "iframe-resizer": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.2.11.tgz",
+ "integrity": "sha512-fj5vX5kkpRbMb5Qje6veIDzqoJpnCEqUDdSOwASOeQHYmb8hLYX6Ev2yXf3jjMs2MclwcYY3chyZ3diGKcr8DA=="
+ },
"ignore": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
diff --git a/package.json b/package.json
index 381780ca5..9b70f5565 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"dompurify": "^2.2.0",
"html-to-text": "^5.1.1",
"ical.js": "^1.4.0",
+ "iframe-resizer": "^4.2.11",
"js-base64": "^3.5.2",
"lodash": "^4.17.20",
"md5": "^2.3.0",
diff --git a/src/components/Message.vue b/src/components/Message.vue
index 2d5c49dbc..c3fc1fa00 100644
--- a/src/components/Message.vue
+++ b/src/components/Message.vue
@@ -24,7 +24,7 @@
<div v-if="message.itineraries.length > 0" class="message-itinerary">
<Itinerary :entries="message.itineraries" :message-id="message.messageId" />
</div>
- <MessageHTMLBody v-if="message.hasHtmlBody" :url="htmlUrl" />
+ <MessageHTMLBody v-if="message.hasHtmlBody" :url="htmlUrl" :full-height="fullHeight" />
<MessageEncryptedBody v-else-if="isEncrypted" :body="message.body" :from="from" />
<MessagePlainTextBody v-else :body="message.body" :signature="message.signature" />
<Popover v-if="message.attachments[0]" class="attachment-popover">
@@ -74,6 +74,11 @@ export default {
required: true,
type: Object,
},
+ fullHeight: {
+ required: false,
+ type: Boolean,
+ default: false,
+ },
},
computed: {
from() {
diff --git a/src/components/MessageHTMLBody.vue b/src/components/MessageHTMLBody.vue
index 180027cb8..b0af31512 100644
--- a/src/components/MessageHTMLBody.vue
+++ b/src/components/MessageHTMLBody.vue
@@ -7,9 +7,9 @@
</button>
</div>
<div v-if="loading" class="icon-loading" />
- <div id="message-container" :class="{hidden: loading}">
- <iframe id="message-frame"
- ref="iframe"
+ <div id="message-container" :class="{hidden: loading, scroll: !fullHeight}">
+ <iframe ref="iframe"
+ class="message-frame"
:title="t('mail', 'Message frame')"
:src="url"
seamless
@@ -19,6 +19,7 @@
</template>
<script>
+import { iframeResizer } from 'iframe-resizer'
import PrintScout from 'printscout'
import logger from '../logger'
@@ -31,6 +32,11 @@ export default {
type: String,
required: true,
},
+ fullHeight: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -42,9 +48,26 @@ export default {
scout.on('beforeprint', this.onBeforePrint)
scout.on('afterprint', this.onAfterPrint)
},
+ mounted() {
+ iframeResizer({
+ onInit: () => {
+ const getCssVar = (key) => ({
+ [key]: getComputedStyle(document.documentElement).getPropertyValue(key),
+ })
+
+ // send css vars to client page
+ this.$refs.iframe.iFrameResizer.sendMessage({
+ cssVars: {
+ ...getCssVar('--color-main-text'),
+ },
+ })
+ },
+ }, this.$refs.iframe)
+ },
beforeDestroy() {
scout.off('beforeprint', this.onBeforePrint)
scout.off('afterprint', this.onAfterPrint)
+ this.$refs.iframe.iFrameResizer.close()
},
methods: {
getIframeDoc() {
@@ -97,11 +120,16 @@ export default {
#message-container {
flex: 1;
- min-height: 50vh;
display: flex;
+
+ // TODO: collapse quoted text and remove inner scrollbar
+ &.scroll {
+ max-height: 50vh;
+ overflow-y: auto;
+ }
}
-#message-frame {
+.message-frame {
width: 100%;
}
</style>
diff --git a/src/components/NewMessageDetail.vue b/src/components/NewMessageDetail.vue
index 71885c4cc..66e88a392 100644
--- a/src/components/NewMessageDetail.vue
+++ b/src/components/NewMessageDetail.vue
@@ -253,7 +253,7 @@ export default {
if (message.hasHtmlBody) {
logger.debug('original message has HTML body')
const resp = await Axios.get(
- generateUrl('/apps/mail/api/messages/{id}/html', {
+ generateUrl('/apps/mail/api/messages/{id}/html?plain=true', {
id,
})
)
diff --git a/src/components/Thread.vue b/src/components/Thread.vue
index 0b6833153..553737d02 100644
--- a/src/components/Thread.vue
+++ b/src/components/Thread.vue
@@ -20,6 +20,7 @@
:envelope="env"
:mailbox-id="$route.params.mailboxId"
:expanded="expandedThreads.includes(env.databaseId)"
+ :full-height="thread.length === 1"
@move="onMove(env.databaseId)"
@toggleExpand="toggleExpand(env.databaseId)" />
</template>
diff --git a/src/components/ThreadEnvelope.vue b/src/components/ThreadEnvelope.vue
index 088eea73d..d6de1f9e4 100644
--- a/src/components/ThreadEnvelope.vue
+++ b/src/components/ThreadEnvelope.vue
@@ -155,7 +155,10 @@
</div>
</div>
<Loading v-if="loading" />
- <Message v-else-if="message" :envelope="envelope" :message="message" />
+ <Message v-else-if="message"
+ :envelope="envelope"
+ :message="message"
+ :full-height="fullHeight" />
<Error v-else-if="error"
:error="error && error.message ? error.message : t('mail', 'Not found')"
:message="errorMessage"
@@ -218,6 +221,11 @@ export default {
type: Boolean,
default: false,
},
+ fullHeight: {
+ required: false,
+ type: Boolean,
+ default: false,
+ },
},
data() {
return {
diff --git a/src/html-response.js b/src/html-response.js
new file mode 100644
index 000000000..ccdff4476
--- /dev/null
+++ b/src/html-response.js
@@ -0,0 +1,38 @@
+/**
+ * @copyright 2020 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author 2020 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// injected styles
+import '../css/html-response.css'
+
+// iframe-resizer client script
+import 'iframe-resizer/js/iframeResizer.contentWindow.js'
+window.iFrameResizer = {
+ onMessage: (message) => {
+ if (!message.cssVars) {
+ return
+ }
+
+ // inject received css vars
+ Object.entries(message.cssVars).forEach(([key, val]) => {
+ document.documentElement.style.setProperty(key, val)
+ })
+ },
+}
diff --git a/tests/Unit/Controller/MessagesControllerTest.php b/tests/Unit/Controller/MessagesControllerTest.php
index 409497b0c..65fdd89e3 100644
--- a/tests/Unit/Controller/MessagesControllerTest.php
+++ b/tests/Unit/Controller/MessagesControllerTest.php
@@ -177,26 +177,33 @@ class MessagesControllerTest extends TestCase {
$message->setUid(123);
$mailbox->setAccountId($accountId);
$mailbox->setName($folderId);
- $this->mailManager->expects($this->once())
+ $this->mailManager->expects($this->exactly(3))
->method('getMessage')
->with($this->userId, $messageId)
->willReturn($message);
- $this->mailManager->expects($this->once())
+ $this->mailManager->expects($this->exactly(3))
->method('getMailbox')
->with($this->userId, $mailboxId)
->willReturn($mailbox);
- $this->accountService->expects($this->once())
+ $this->accountService->expects($this->exactly(3))
->method('find')
->with($this->equalTo($this->userId), $this->equalTo($accountId))
->will($this->returnValue($this->account));
$imapMessage = $this->createMock(IMAPMessage::class);
- $this->mailManager->expects($this->once())
+ $this->mailManager->expects($this->exactly(3))
->method('getImapMessage')
->with($this->account, $mailbox, 123, true)
->willReturn($imapMessage);
- $expectedResponse = new HtmlResponse('');
- $expectedResponse->cacheFor(3600);
+ $expectedDefaultResponse = new HtmlResponse('');
+ $expectedDefaultResponse->cacheFor(3600);
+
+ $expectedPlainResponse = new HtmlResponse('', true);
+ $expectedPlainResponse->cacheFor(3600);
+
+ $expectedRichResponse = new HtmlResponse('', false);
+ $expectedRichResponse->cacheFor(3600);
+
if (class_exists('\OCP\AppFramework\Http\ContentSecurityPolicy')) {
$policy = new ContentSecurityPolicy();
$policy->allowEvalScript(false);
@@ -204,12 +211,18 @@ class MessagesControllerTest extends TestCase {
$policy->disallowConnectDomain('\'self\'');
$policy->disallowFontDomain('\'self\'');
$policy->disallowMediaDomain('\'self\'');
- $expectedResponse->setContentSecurityPolicy($policy);
+ $expectedDefaultResponse->setContentSecurityPolicy($policy);
+ $expectedPlainResponse->setContentSecurityPolicy($policy);
+ $expectedRichResponse->setContentSecurityPolicy($policy);
}
- $actualResponse = $this->controller->getHtmlBody($messageId);
+ $actualDefaultResponse = $this->controller->getHtmlBody($messageId);
+ $actualPlainResponse = $this->controller->getHtmlBody($messageId, true);
+ $actualRichResponse = $this->controller->getHtmlBody($messageId, false);
- $this->assertEquals($expectedResponse, $actualResponse);
+ $this->assertEquals($expectedDefaultResponse, $actualDefaultResponse);
+ $this->assertEquals($expectedPlainResponse, $actualPlainResponse);
+ $this->assertEquals($expectedRichResponse, $actualRichResponse);
}
public function testDownloadAttachment() {
diff --git a/tests/Unit/Http/HtmlResponseTest.php b/tests/Unit/Http/HtmlResponseTest.php
index 67c07374e..be36f8088 100644
--- a/tests/Unit/Http/HtmlResponseTest.php
+++ b/tests/Unit/Http/HtmlResponseTest.php
@@ -24,6 +24,7 @@ namespace OCA\Mail\Tests\Unit\Http;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Http\HtmlResponse;
+use OCP\Util;
class HtmlResponseTest extends TestCase {
@@ -34,9 +35,18 @@ class HtmlResponseTest extends TestCase {
* @param $contentType
*/
public function testIt($content) {
- $resp = new HtmlResponse($content);
- $injectedStyles = "<style>* { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', Arial, 'Noto Color Emoji', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; }</style>";
- $this->assertEquals($injectedStyles . $content, $resp->render());
+ $defaultResp = new HtmlResponse($content);
+ $plainResp = new HtmlResponse($content, true);
+ $richResp = new HtmlResponse($content, false);
+
+ $scriptSrcRegex = preg_quote(Util::linkToAbsolute('mail', 'js/htmlresponse.js'), '/');
+ $contentRegex = preg_quote($content, '/');
+ $responseRegex = '/<script nonce=".+" src="' . $scriptSrcRegex . '"><\/script>'
+ . $contentRegex . '/';
+
+ $this->assertMatchesRegularExpression($responseRegex, $defaultResp->render());
+ $this->assertEquals($content, $plainResp->render());
+ $this->assertMatchesRegularExpression($responseRegex, $richResp->render());
}
public function providesResponseData() {
diff --git a/webpack.common.js b/webpack.common.js
index e86ce0e3d..0814297d2 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -22,7 +22,8 @@ module.exports = {
autoredirect: path.join(__dirname, 'src/autoredirect.js'),
dashboard: path.join(__dirname, 'src/main-dashboard.js'),
mail: path.join(__dirname, 'src/main.js'),
- settings: path.join(__dirname, 'src/main-settings')
+ settings: path.join(__dirname, 'src/main-settings'),
+ htmlresponse: path.join(__dirname, 'src/html-response.js'),
},
output: {
path: path.resolve(__dirname, 'js'),