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:
authorChristoph Wurst <christoph@winzerhof-wurst.at>2018-11-05 14:05:07 +0300
committerChristoph Wurst <christoph@winzerhof-wurst.at>2018-11-05 18:50:34 +0300
commit881880e5def73a7ac846a0d3b96b64395bf22ba8 (patch)
treec3630762dc4ed5228bf111e7d99d2db2bf58e1d1
parent20eb16ea63fff0bae159fc16bd0fc86f5299bd1b (diff)
Fill reply recipients, subject and body
* Revive reply builder * Prefill reply to and cc * Revive HtmlHelper * Revive reply bodies Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
-rw-r--r--package-lock.json160
-rw-r--r--package.json5
-rw-r--r--src/ReplyBuilder.js111
-rw-r--r--src/components/Composer.vue23
-rw-r--r--src/components/Envelope.vue2
-rw-r--r--src/components/Message.vue43
-rw-r--r--src/components/MessageHTMLBody.vue16
-rw-r--r--src/components/Moment.vue4
-rw-r--r--src/replybuilder.js96
-rw-r--r--src/tests/setup.js4
-rw-r--r--src/tests/unit/ReplyBuilder.spec.js229
-rw-r--r--src/tests/unit/util/HtmlHelper.spec.js107
-rw-r--r--src/util/HtmlHelper.js20
-rw-r--r--src/util/htmlhelper.js30
-rw-r--r--webpack.common.js3
-rw-r--r--webpack.dev.js2
16 files changed, 665 insertions, 190 deletions
diff --git a/package-lock.json b/package-lock.json
index efa3f7068..91ecf4d7f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1949,7 +1949,7 @@
},
"util": {
"version": "0.10.3",
- "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
@@ -3152,7 +3152,7 @@
},
"cacache": {
"version": "10.0.4",
- "resolved": "http://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz",
"integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==",
"dev": true,
"requires": {
@@ -4110,12 +4110,33 @@
"esutils": "^2.0.2"
}
},
+ "dom-serializer": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+ "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
+ "requires": {
+ "domelementtype": "~1.1.1",
+ "entities": "~1.1.1"
+ },
+ "dependencies": {
+ "domelementtype": {
+ "version": "1.1.3",
+ "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
+ "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs="
+ }
+ }
+ },
"domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
"integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
"dev": true
},
+ "domelementtype": {
+ "version": "1.3.0",
+ "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
+ "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI="
+ },
"domexception": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
@@ -4125,6 +4146,23 @@
"webidl-conversions": "^4.0.2"
}
},
+ "domhandler": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+ "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+ "requires": {
+ "domelementtype": "1"
+ }
+ },
+ "domutils": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+ "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+ "requires": {
+ "dom-serializer": "0",
+ "domelementtype": "1"
+ }
+ },
"dot-prop": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
@@ -4201,6 +4239,11 @@
"tapable": "^1.0.0"
}
},
+ "entities": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
+ },
"errno": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
@@ -4798,7 +4841,7 @@
},
"fecha": {
"version": "2.3.3",
- "resolved": "http://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
+ "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
"integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg=="
},
"figures": {
@@ -5009,8 +5052,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.2.0",
@@ -5031,14 +5073,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5053,20 +5093,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -5183,8 +5220,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.5",
@@ -5196,7 +5232,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -5211,7 +5246,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -5219,14 +5253,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -5245,7 +5277,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -5326,8 +5357,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -5339,7 +5369,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -5425,8 +5454,7 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -5462,7 +5490,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -5482,7 +5509,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -5526,14 +5552,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
}
}
},
@@ -5902,8 +5926,7 @@
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
- "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
- "dev": true
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"hmac-drbg": {
"version": "1.0.1",
@@ -5931,6 +5954,42 @@
"whatwg-encoding": "^1.0.1"
}
},
+ "html-to-text": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-4.0.0.tgz",
+ "integrity": "sha512-QQl5EEd97h6+3crtgBhkEAO6sQnZyDff8DAeJzoSkOc1Dqe1UvTUZER0B+KjBe6fPZqq549l2VUhtracus3ndA==",
+ "requires": {
+ "he": "^1.0.0",
+ "htmlparser2": "^3.9.2",
+ "lodash": "^4.17.4",
+ "optimist": "^0.6.1"
+ }
+ },
+ "htmlparser2": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz",
+ "integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==",
+ "requires": {
+ "domelementtype": "^1.3.0",
+ "domhandler": "^2.3.0",
+ "domutils": "^1.5.1",
+ "entities": "^1.1.1",
+ "inherits": "^2.0.1",
+ "readable-stream": "^3.0.6"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.0.6.tgz",
+ "integrity": "sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ }
+ }
+ },
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -6041,8 +6100,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
- "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
- "dev": true
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"inquirer": {
"version": "6.2.0",
@@ -6761,7 +6819,7 @@
},
"meow": {
"version": "3.7.0",
- "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
"dev": true,
"requires": {
@@ -6892,8 +6950,7 @@
"minimist": {
"version": "0.0.8",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
- "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
- "dev": true
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mississippi": {
"version": "2.0.0",
@@ -7570,6 +7627,22 @@
"mimic-fn": "^1.0.0"
}
},
+ "optimist": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+ "requires": {
+ "minimist": "~0.0.1",
+ "wordwrap": "~0.0.2"
+ },
+ "dependencies": {
+ "wordwrap": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+ "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
+ }
+ }
+ },
"optionator": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
@@ -8727,8 +8800,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-regex": {
"version": "1.1.0",
@@ -9391,7 +9463,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@@ -9946,8 +10017,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"util.promisify": {
"version": "1.0.0",
diff --git a/package.json b/package.json
index ccad96162..fcdd99b86 100644
--- a/package.json
+++ b/package.json
@@ -10,14 +10,15 @@
"dev": "webpack --progress --watch --config webpack.dev.js",
"lint": "$(npm bin)/eslint --ext .js,.vue js",
"lint:autofix": "eslint --ext .js,.vue js --fix",
- "test": "mocha-webpack --webpack-config webpack.test.js --require src/tests/setup.js src/tests/**/*.spec.js",
- "test:watch": "mocha-webpack -w --webpack-config webpack.test.js --require src/tests/setup.js src/tests/**/*.spec.js"
+ "test": "mocha-webpack --webpack-config webpack.test.js --require src/tests/setup.js \"src/tests/**/*.spec.js\"",
+ "test:watch": "mocha-webpack -w --webpack-config webpack.test.js --require src/tests/setup.js \"src/tests/**/*.spec.js\""
},
"dependencies": {
"@vue/babel-preset-app": "^3.1.1",
"color-convert": "^1.9.3",
"davclient.js": "^0.1.0",
"exports-loader": "^0.7.0",
+ "html-to-text": "^4.0.0",
"ical.js": "^1.2.2",
"lodash": "^4.17.11",
"md5": "^2.2.1",
diff --git a/src/ReplyBuilder.js b/src/ReplyBuilder.js
new file mode 100644
index 000000000..9ae5c53be
--- /dev/null
+++ b/src/ReplyBuilder.js
@@ -0,0 +1,111 @@
+/**
+ * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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/>.
+ *
+ */
+
+import _ from 'lodash'
+import moment from 'moment'
+import {getLocale} from 'nextcloud-server/dist/l10n'
+
+moment.locale(getLocale())
+
+export const buildReplyBody = (original, from, date) => {
+ const start = '\n\n'
+ const body = '\n> ' + original.replace(/\n/g, '\n> ')
+
+ if (from) {
+ const dateString = moment.unix(date).format('LLL')
+ return start + `"${from.label}" <${from.email}> – ${dateString}` + body
+ } else {
+ return start + body
+ }
+}
+
+const RecipientType = Object.seal({
+ None: 0,
+ To: 1,
+ Cc: 2
+})
+
+export const buildRecipients = (envelope, ownAddress) => {
+ let recipientType = RecipientType.None
+ const isOwnAddress = a => a.email === ownAddress.email
+ const isNotOwnAddress = _.negate(isOwnAddress)
+
+ // Locate why we received this envelope
+ // Can be in 'to', 'cc' or unknown
+ let replyingAddress = envelope.to.find(isOwnAddress)
+ if (!_.isUndefined(replyingAddress)) {
+ recipientType = RecipientType.To
+ } else {
+ replyingAddress = envelope.cc.find(isOwnAddress)
+ if (!_.isUndefined(replyingAddress)) {
+ recipientType = RecipientType.Cc
+ } else {
+ replyingAddress = ownAddress
+ }
+ }
+
+ let to = []
+ let cc = []
+ if (recipientType === RecipientType.To) {
+ // Send to everyone except yourself plus the original sender
+ to = envelope.to.filter(isNotOwnAddress)
+ to = to.concat(envelope.from)
+
+ // CC remains the same
+ cc = envelope.cc
+ } else if (recipientType === RecipientType.Cc) {
+ // Send to the same people plus the sender
+ to = envelope.to.concat(envelope.from)
+
+ // All CC values are being kept except the replying address
+ cc = envelope.cc.filter(isNotOwnAddress)
+ } else {
+ // Send to the same recipient and the sender -> answer all
+ to = envelope.to
+ to = to.concat(envelope.from)
+
+ // Keep CC values
+ cc = envelope.cc
+ }
+
+ return {
+ to,
+ from: replyingAddress ? [replyingAddress] : [],
+ cc,
+ }
+}
+
+// TODO: https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages
+const replyPrepends = [
+ 're',
+]
+
+/*
+ * Ref https://tools.ietf.org/html/rfc5322#section-3.6.5
+ */
+export const buildReplySubject = original => {
+ if (replyPrepends.some(prepend => original.toLowerCase().startsWith(`${prepend}:`))) {
+ return original
+ }
+
+ return `RE: ${original}`
+}
diff --git a/src/components/Composer.vue b/src/components/Composer.vue
index 2ecd58cd1..a2bc8a4f6 100644
--- a/src/components/Composer.vue
+++ b/src/components/Composer.vue
@@ -96,7 +96,7 @@
import Vue from 'vue'
import Loading from './Loading'
- import ComposerAttachments from "./ComposerAttachments";
+ import ComposerAttachments from './ComposerAttachments'
Vue.use(Autosize)
@@ -129,6 +129,10 @@
type: String,
default: '',
},
+ body: {
+ type: String,
+ default: ''
+ },
draft: {
type: Function,
required: true,
@@ -141,12 +145,12 @@
data () {
return {
hasCC: true,
- selectedAlias: this.$route.params.accountId,
+ selectedAlias: this.$route.params.accountId, // TODO: fix for unified inbox
toVal: this.addressListPlain(this.to),
ccVal: this.addressListPlain(this.cc),
bccVal: '',
subjectVal: this.subject,
- bodyVal: '',
+ bodyVal: this.body,
attachments: [],
noReply: false,
message: '',
@@ -167,12 +171,17 @@
return !_.isUndefined(this.replyTo)
}
},
- filters: {
-
- },
methods: {
addressListPlain (addresses) {
- return addresses.join('; ')
+ return addresses
+ .map(addr => {
+ if (addr.label && addr.label !== addr.email) {
+ return `"${addr.label}" <${addr.email}>`
+ } else {
+ return addr.email
+ }
+ })
+ .join('; ')
},
getMessageData () {
return uid => {
diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue
index 0092d175d..29b496a4e 100644
--- a/src/components/Envelope.vue
+++ b/src/components/Envelope.vue
@@ -40,7 +40,7 @@
{{ data.subject }}
</div>
<div class="app-content-list-item-details date">
- <Moment :timestamp="data.dateInt * 1000" />
+ <Moment :timestamp="data.dateInt" />
</div>
<Action class="app-content-list-item-menu"
:actions="actions" />
diff --git a/src/components/Message.vue b/src/components/Message.vue
index ee1884950..4b0ed2f6f 100644
--- a/src/components/Message.vue
+++ b/src/components/Message.vue
@@ -17,7 +17,8 @@
</div>
<div class="mail-message-body">
<MessageHTMLBody v-if="message.hasHtmlBody"
- :url="htmlUrl"/>
+ :url="htmlUrl"
+ @loaded="onHtmlBodyLoaded"/>
<MessagePlainTextBody v-else
:body="message.body"
:signature="message.signature"/>
@@ -25,7 +26,12 @@
<div id="reply-composer"></div>
<input type="button" id="forward-button" value="Forward">
</div>
- <Composer :replyTo="replyTo"
+ <Composer v-if="!message.hasHtmlBody || htmlBodyLoaded"
+ :to="replyRecipient.to"
+ :cc="replyRecipient.cc"
+ :subject="replySubject"
+ :body="replyBody"
+ :replyTo="replyTo"
:send="sendReply"
:draft="saveReplyDraft"/>
</template>
@@ -36,7 +42,13 @@
import { generateUrl } from 'nextcloud-server/dist/router'
import AddressList from './AddressList'
+ import {
+ buildReplyBody,
+ buildRecipients as buildReplyRecipients,
+ buildReplySubject,
+ } from '../ReplyBuilder'
import Composer from './Composer'
+ import {htmlToText} from '../util/HtmlHelper'
import MessageHTMLBody from './MessageHTMLBody'
import MessagePlainTextBody from './MessagePlainTextBody'
import Loading from './Loading'
@@ -57,6 +69,10 @@
return {
loading: true,
message: undefined,
+ htmlBodyLoaded: false,
+ replyRecipient: {},
+ replySubject: '',
+ replyBody: '',
};
},
computed: {
@@ -86,6 +102,10 @@
fetchMessage () {
this.loading = true
this.message = undefined
+ this.replyRecipient = {}
+ this.replySubject = ''
+ this.replyBody = ''
+ this.htmlBodyLoaded = false
const accountId = this.$route.params.accountId
const folderId = this.$route.params.folderId
@@ -98,6 +118,14 @@
id
}).then(message => {
this.message = message
+
+ this.replyRecipient = buildReplyRecipients(message, {}) // TODO: own address
+ this.replySubject = buildReplySubject(message.subject)
+
+ if (!message.hasHtmlBody) {
+ this.setReplyText(message.body)
+ }
+
this.loading = false
}).then(() => {
// TODO: add timeout so that message isn't flagged when only viewed
@@ -122,6 +150,17 @@
})
}).catch(console.error.bind(this))
},
+ setReplyText (text) {
+ this.replyBody = buildReplyBody(
+ htmlToText(text),
+ this.message.from[0],
+ this.message.dateInt,
+ )
+ },
+ onHtmlBodyLoaded (bodyString) {
+ this.setReplyText(bodyString)
+ this.htmlBodyLoaded = true
+ },
saveReplyDraft (data) {
return saveDraft(data.account, data)
.then(({uid}) => uid)
diff --git a/src/components/MessageHTMLBody.vue b/src/components/MessageHTMLBody.vue
index 5d8498805..fefc383e2 100644
--- a/src/components/MessageHTMLBody.vue
+++ b/src/components/MessageHTMLBody.vue
@@ -22,9 +22,12 @@
<script>
export default {
name: "MessageHTMLBody",
- props: [
- 'url',
- ],
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ },
data () {
return {
loading: true
@@ -32,7 +35,12 @@
},
methods: {
onMessageFrameLoad () {
- console.log('todo: resize', this.$refs.iframe)
+ const iframe = this.$refs.iframe
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow.document
+ const iframeBody = iframeDoc.querySelectorAll('body')[0]
+
+ console.log('todo: resize', iframe)
+ this.$emit('loaded', iframeBody.outerHTML)
this.loading = false
}
}
diff --git a/src/components/Moment.vue b/src/components/Moment.vue
index 1a1ffe233..3e0ee066c 100644
--- a/src/components/Moment.vue
+++ b/src/components/Moment.vue
@@ -20,10 +20,10 @@
],
computed: {
title () {
- return moment.unix(this.timestamp / 1000).format(this.format || 'LLL');
+ return moment.unix(this.timestamp).format(this.format || 'LLL');
},
formatted () {
- return moment.unix(this.timestamp / 1000).fromNow();
+ return moment.unix(this.timestamp).fromNow();
}
}
}
diff --git a/src/replybuilder.js b/src/replybuilder.js
deleted file mode 100644
index e9d67b681..000000000
--- a/src/replybuilder.js
+++ /dev/null
@@ -1,96 +0,0 @@
-/**
- * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @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/>.
- *
- */
-
-define(function(require) {
- 'use strict';
-
- var _ = require('underscore');
-
- var RecipientType = Object.seal({
- None: 0,
- To: 1,
- Cc: 2
- });
-
- var buildReply = function(message, messageBody) {
- var recipientType = RecipientType.None;
- var ownAddress = message.folder.account.get('emailAddress');
- var isOwnAddress = function(a) {
- return a.email === ownAddress;
- };
- var isNotOwnAddress = _.negate(isOwnAddress);
-
- // Locate why we received this message
- // Can be in 'to', 'cc' or unknown
- var replyingAddress = _.find(messageBody.get('to'), isOwnAddress);
- if (!_.isUndefined(replyingAddress)) {
- recipientType = RecipientType.To;
- } else {
- replyingAddress = _.find(messageBody.get('cc'), isOwnAddress);
- if (!_.isUndefined(replyingAddress)) {
- recipientType = RecipientType.Cc;
- } else {
- replyingAddress = {
- label: ownAddress,
- email: ownAddress
- };
- }
- }
-
- var to = [];
- var cc = [];
- if (recipientType === RecipientType.To) {
- // Send to everyone except yourself plus the original sender
- to = messageBody.get('to').filter(isNotOwnAddress);
- to = to.concat(messageBody.get('from'));
-
- // CC remains the same
- cc = messageBody.get('cc');
- } else if (recipientType === RecipientType.Cc) {
- // Send to the same people plus the sender
- to = messageBody.get('to').concat(messageBody.get('from'));
-
- // All CC values are being kept except the replying address
- cc = messageBody.get('cc').filter(isNotOwnAddress);
- } else {
- // Send to the same recipient and the sender -> answer all
- to = messageBody.get('to');
- to = to.concat(messageBody.get('from'));
-
- // Keep CC values
- cc = messageBody.get('cc');
- }
-
- return {
- to: to,
- from: replyingAddress ? [replyingAddress] : [],
- fromEmail: message.folder.account.get('emailAddress'), // TODO: alias?
- cc: cc,
- body: ''
- };
- };
-
- return {
- buildReply: buildReply
- };
-
-});
diff --git a/src/tests/setup.js b/src/tests/setup.js
index 18e56e1be..feb4c9bf6 100644
--- a/src/tests/setup.js
+++ b/src/tests/setup.js
@@ -21,3 +21,7 @@
require('jsdom-global')()
global.expect = require('chai').expect
+
+global.OC = {
+ getLocale: () => 'en'
+}
diff --git a/src/tests/unit/ReplyBuilder.spec.js b/src/tests/unit/ReplyBuilder.spec.js
new file mode 100644
index 000000000..445be5591
--- /dev/null
+++ b/src/tests/unit/ReplyBuilder.spec.js
@@ -0,0 +1,229 @@
+/*
+ * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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/>.
+ */
+
+/* global expect */
+
+/**
+ * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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/>.
+ *
+ */
+
+import {
+ buildReplyBody,
+ buildRecipients,
+ buildReplySubject,
+} from '../../ReplyBuilder'
+
+describe('ReplyBuilder', () => {
+
+ it('creates a reply body without any sender', () => {
+ const body = 'Newsletter\nhello\ncheers'
+ const expectedReply = '\n\n\n> Newsletter\n> hello\n> cheers'
+
+ const replyBody = buildReplyBody(body)
+
+ expect(replyBody).to.equal(expectedReply)
+ })
+
+ it('creates a reply body', () => {
+ const body = 'Newsletter\nhello'
+ const expectedReply = '\n\n"Test User" <test@user.ru> – November 5, 2018 2:57 PM\n> Newsletter\n> hello'
+
+ const replyBody = buildReplyBody(
+ body,
+ {
+ label: 'Test User',
+ email: 'test@user.ru',
+ },
+ 1541426237,
+ )
+
+ expect(replyBody).to.equal(expectedReply)
+ })
+
+ let envelope
+
+ beforeEach(function () {
+ envelope = {}
+ })
+
+ const createAddress = addr => {
+ return {
+ label: addr,
+ email: addr
+ }
+ }
+
+ const assertSameAddressList = (l1, l2) => {
+ const rawL1 = l1.map(a => a.email)
+ const rawL2 = l2.map(a => a.email)
+ rawL1.sort()
+ rawL2.sort()
+ expect(rawL1).to.deep.equal(rawL2)
+ }
+
+ // b -> a to a -as b
+ it('handles a one-on-one reply', () => {
+ const a = createAddress('a@domain.tld')
+ const b = createAddress('b@domain.tld')
+ envelope.from = [b]
+ envelope.to = [a]
+ envelope.cc = []
+
+ const reply = buildRecipients(envelope, a)
+
+ assertSameAddressList(reply.from, [a])
+ assertSameAddressList(reply.to, [b])
+ assertSameAddressList(reply.cc, [])
+ })
+
+ it('handles simple group reply', () => {
+ const a = createAddress('a@domain.tld')
+ const b = createAddress('b@domain.tld')
+ const c = createAddress('c@domain.tld')
+ envelope.from = [a]
+ envelope.to = [b, c]
+ envelope.cc = []
+
+ var reply = buildRecipients(envelope, b)
+
+ assertSameAddressList(reply.from, [b])
+ assertSameAddressList(reply.to, [a, c])
+ assertSameAddressList(reply.cc, [])
+ })
+
+ it('handles group reply with CC', () => {
+ const a = createAddress('a@domain.tld')
+ const b = createAddress('b@domain.tld')
+ const c = createAddress('c@domain.tld')
+ const d = createAddress('d@domain.tld')
+ envelope.from = [a]
+ envelope.to = [b, c]
+ envelope.cc = [d]
+
+ const reply = buildRecipients(envelope, b)
+
+ assertSameAddressList(reply.from, [b])
+ assertSameAddressList(reply.to, [a, c])
+ assertSameAddressList(reply.cc, [d])
+ })
+
+ it('handles group reply of CC address', () => {
+ const a = createAddress('a@domain.tld')
+ const b = createAddress('b@domain.tld')
+ const c = createAddress('c@domain.tld')
+ const d = createAddress('d@domain.tld')
+ envelope.from = [a]
+ envelope.to = [b, c]
+ envelope.cc = [d]
+
+ const reply = buildRecipients(envelope, d)
+
+ assertSameAddressList(reply.from, [d])
+ assertSameAddressList(reply.to, [a, b, c])
+ assertSameAddressList(reply.cc, [])
+ })
+
+ it('handles group reply of CC address with many CCs', () => {
+ const a = createAddress('a@domain.tld')
+ const b = createAddress('b@domain.tld')
+ const c = createAddress('c@domain.tld')
+ const d = createAddress('d@domain.tld')
+ const e = createAddress('e@domain.tld')
+ envelope.from = [a]
+ envelope.to = [b, c]
+ envelope.cc = [d, e]
+
+ const reply = buildRecipients(envelope, e)
+
+ assertSameAddressList(reply.from, [e])
+ assertSameAddressList(reply.to, [a, b, c])
+ assertSameAddressList(reply.cc, [d])
+ })
+
+ it('handles reply of message where the recipient is in the CC', () => {
+ const ali = createAddress('ali@domain.tld')
+ const bob = createAddress('bob@domain.tld')
+ const me = createAddress('c@domain.tld')
+ const dani = createAddress('d@domain.tld')
+
+ envelope.from = [ali]
+ envelope.to = [bob]
+ envelope.cc = [me, dani]
+
+ const reply = buildRecipients(envelope, me)
+
+ assertSameAddressList(reply.from, [me])
+ assertSameAddressList(reply.to, [ali, bob])
+ assertSameAddressList(reply.cc, [dani])
+ })
+
+ it('handles jan\'s reply to nina\'s mesage to a mailing list', () => {
+ const nina = createAddress('nina@nc.com')
+ const list = createAddress('list@nc.com')
+ const jan = createAddress('jan@nc.com')
+
+ envelope.from = [nina]
+ envelope.to = [list]
+ envelope.cc = []
+
+ const reply = buildRecipients(envelope, jan)
+
+ assertSameAddressList(reply.from, [jan])
+ assertSameAddressList(reply.to, [nina, list])
+ assertSameAddressList(reply.cc, [])
+ })
+
+ it('adds re: to a reply subject', () => {
+ const orig = 'Hello'
+
+ const replySubject = buildReplySubject(orig)
+
+ expect(replySubject).to.equal('RE: Hello')
+ })
+
+ it('does not stack subject re:\'s', () => {
+ const orig = 'Re: Hello'
+
+ const replySubject = buildReplySubject(orig)
+
+ expect(replySubject).to.equal('Re: Hello')
+ })
+
+})
+
diff --git a/src/tests/unit/util/HtmlHelper.spec.js b/src/tests/unit/util/HtmlHelper.spec.js
new file mode 100644
index 000000000..b2498e09c
--- /dev/null
+++ b/src/tests/unit/util/HtmlHelper.spec.js
@@ -0,0 +1,107 @@
+/* global expect */
+
+/**
+ * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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/>.
+ *
+ */
+
+import {htmlToText} from '../../../util/HtmlHelper'
+
+describe('HtmlHelper', () => {
+ it('preserves breaks', () => {
+ const html = 'line1<br>line2'
+ const expected = 'line1\nline2'
+
+ const actual = htmlToText(html)
+
+ expect(actual).to.equal(expected)
+ })
+
+ it('concats divs', () => {
+ const html = '<div>one</div><div>two</div>'
+ const expected = 'onetwo'
+
+ const actual = htmlToText(html)
+
+ expect(actual).to.equal(expected)
+ })
+
+ it('does not produce large number of line breaks for nested elements', () => {
+ const html =
+ '<div>' +
+ ' <div>' +
+ ' line1' +
+ ' </div>' +
+ '</div>' +
+ '<div>line2</div>'
+ const expected = ' line1 line2'
+
+ const actual = htmlToText(html)
+
+ expect(actual).to.equal(expected)
+ })
+
+ it('converts blocks to text', () => {
+ const html = '<div>hello</div>'
+ const expected = 'hello'
+
+ const actual = htmlToText(html)
+
+ expect(actual).to.equal(expected)
+ })
+
+ it('converts paragraphs to text', () => {
+ const html = '<p>hello</p>'
+ const expected = 'hello'
+
+ const actual = htmlToText(html)
+
+ expect(actual).to.equal(expected)
+ })
+
+ it('converts lists to text', () => {
+ const html = '<ul><li>one</li><li>two</li><li>three</li></ul>'
+ const expected = ' * one\n * two\n * three'
+
+ const actual = htmlToText(html)
+
+ expect(actual).to.equal(expected)
+ })
+
+ it('converts deeply nested elements to text', () => {
+ const html = '<html>'
+ + '<body><p>Hello!</p><p>this <i>is</i> <b>some</b> random <strong>text</strong></p></body>'
+ + '</html>'
+ const expected = 'Hello!\n\nthis is some random text'
+
+ const actual = htmlToText(html)
+
+ expect(actual).to.equal(expected)
+ })
+
+ it('does not leak internal redirection URLs', () => {
+ const html = '<a href="https://localhost/apps/mail/redirect?src=domain.tld">domain.tld</a>'
+ const expected = 'domain.tld'
+
+ const actual = htmlToText(html)
+
+ expect(actual).to.equal(expected)
+ })
+})
diff --git a/src/util/HtmlHelper.js b/src/util/HtmlHelper.js
new file mode 100644
index 000000000..1ad9856f8
--- /dev/null
+++ b/src/util/HtmlHelper.js
@@ -0,0 +1,20 @@
+/**
+ * Mail
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @copyright Christoph Wurst 2016
+ */
+
+import {fromString} from 'html-to-text'
+
+export const htmlToText = html => {
+ return fromString(html, {
+ noLinkBrackets: true,
+ ignoreHref: true,
+ ignoreImage: true,
+ wordwrap: 78 // 80 minus '> ' prefix for replies
+ });
+}
diff --git a/src/util/htmlhelper.js b/src/util/htmlhelper.js
deleted file mode 100644
index 28bc07a10..000000000
--- a/src/util/htmlhelper.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Mail
- *
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @copyright Christoph Wurst 2016
- */
-
-define(function(require) {
- 'use strict';
-
- var $ = require('jquery');
-
- var htmlToTextLib = require('html-to-text');
-
- function htmlToText(html) {
- return htmlToTextLib.fromString(html, {
- noLinkBrackets: true,
- ignoreHref: true,
- ignoreImage: true,
- wordwrap: 78 // 80 minus '> ' prefix for replies
- });
- }
-
- return {
- htmlToText: htmlToText
- };
-});
diff --git a/webpack.common.js b/webpack.common.js
index 73c84b30b..7a4ef5bca 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -8,6 +8,9 @@ module.exports = {
publicPath: '/js/',
filename: 'mail.js'
},
+ node: {
+ fs: 'empty'
+ },
module: {
rules: [
{
diff --git a/webpack.dev.js b/webpack.dev.js
index 9f66562f9..db837f09b 100644
--- a/webpack.dev.js
+++ b/webpack.dev.js
@@ -8,5 +8,5 @@ module.exports = merge(common, {
noInfo: true,
overlay: true
},
- devtool: '#cheap-source-map',
+ devtool: 'cheap-source-map',
})