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:
authorGreta <gretadoci@gmail.com>2020-04-02 17:48:51 +0300
committerGitHub <noreply@github.com>2020-04-02 17:48:51 +0300
commit659914c7217d9c82e67d849759e831b53878013b (patch)
treebf0bfbaa4ca63106ee39ea49cd443cd15dd887f3
parent89b6ac576b7edef0e4af98cc90f095f00fc00cd2 (diff)
parent69c9a4da0059bdcba0b12737ee67e688d457fda5 (diff)
Merge pull request #2838 from nextcloud/refactor/text-objectv1.3.0-beta3
Abstract text into an object with known format
-rw-r--r--src/ReplyBuilder.js45
-rw-r--r--src/components/Composer.vue29
-rw-r--r--src/components/NewMessageDetail.vue90
-rw-r--r--src/components/SignatureSettings.vue12
-rw-r--r--src/components/TextEditor.vue1
-rw-r--r--src/service/MessageService.js4
-rw-r--r--src/tests/unit/ReplyBuilder.spec.js13
-rw-r--r--src/tests/unit/util/HtmlHelper.spec.js133
-rw-r--r--src/tests/unit/util/text.spec.js156
-rw-r--r--src/util/HtmlHelper.js41
-rw-r--r--src/util/text.js149
11 files changed, 401 insertions, 272 deletions
diff --git a/src/ReplyBuilder.js b/src/ReplyBuilder.js
index de477e9d7..4422f709e 100644
--- a/src/ReplyBuilder.js
+++ b/src/ReplyBuilder.js
@@ -23,28 +23,39 @@
import moment from '@nextcloud/moment'
import negate from 'lodash/fp/negate'
+import {html} from './util/text'
+
+/**
+ * @param {Text} original
+ * @param {object} from
+ * @param {Number} date
+ * @return {Text}
+ */
export const buildReplyBody = (original, from, date) => {
const start = '<p></p><p></p>'
- const body = '<br>&gt; ' + original.replace(/\n/g, '<br>&gt; ')
- if (from) {
- const dateString = moment.unix(date).format('LLL')
- return `${start}"${from.label}" ${from.email} – ${dateString}` + body
- } else {
- return `${start}${body}`
+ switch (original.format) {
+ case 'plain':
+ const plainBody = '<br>&gt; ' + original.value.replace(/\n/g, '<br>&gt; ')
+
+ if (from) {
+ const dateString = moment.unix(date).format('LLL')
+ return html(`${start}"${from.label}" ${from.email} – ${dateString}` + plainBody)
+ } else {
+ return html(`${start}${plainBody}`)
+ }
+ case 'html':
+ const htmlBody = `<blockquote>${original.value}</blockquote>`
+
+ if (from) {
+ const dateString = moment.unix(date).format('LLL')
+ return html(`${start}"${from.label}" ${from.email} – ${dateString}<br>${htmlBody}`)
+ } else {
+ return html(`${start}${htmlBody}`)
+ }
}
-}
-
-export const buildHtmlReplyBody = (original, from, date) => {
- const start = `<p></p><p></p>`
- const body = `<blockquote>${original}</blockquote>`
- if (from) {
- const dateString = moment.unix(date).format('LLL')
- return `${start}"${from.label}" ${from.email} – ${dateString}<br>${body}`
- } else {
- return `${start}${body}`
- }
+ throw new Error(`can't build a reply for the format ${original.format}`)
}
const RecipientType = Object.seal({
diff --git a/src/components/Composer.vue b/src/components/Composer.vue
index 00ee4b2b1..f8a4b8687 100644
--- a/src/components/Composer.vue
+++ b/src/components/Composer.vue
@@ -199,7 +199,7 @@ import Vue from 'vue'
import ComposerAttachments from './ComposerAttachments'
import {findRecipient} from '../service/AutocompleteService'
-import {htmlToText, textToSimpleHtml} from '../util/HtmlHelper'
+import {detect, html, toHtml} from '../util/text'
import Loading from './Loading'
import logger from '../logger'
import TextEditor from './TextEditor'
@@ -249,8 +249,8 @@ export default {
default: '',
},
body: {
- type: String,
- default: '',
+ type: Object,
+ default: () => html(''),
},
draft: {
type: Function,
@@ -260,10 +260,6 @@ export default {
type: Function,
required: true,
},
- isPlainText: {
- type: Boolean,
- default: true,
- },
},
data() {
return {
@@ -272,21 +268,21 @@ export default {
autocompleteRecipients: this.to.concat(this.cc).concat(this.bcc),
newRecipients: [],
subjectVal: this.subject,
- bodyVal: this.isPlainText ? textToSimpleHtml(this.body) : this.body,
+ bodyVal: toHtml(this.body).value,
attachments: [],
noReply: this.to.some((to) => to.email.startsWith('noreply@') || to.email.startsWith('no-reply@')),
submitButtonTitle: t('mail', 'Send'),
draftsPromise: Promise.resolve(),
attachmentsPromise: Promise.resolve(),
savingDraft: undefined,
- saveDraftDebounced: debounce(700)(this.saveDraft),
+ saveDraftDebounced: debounce(700, this.saveDraft),
state: STATES.EDITING,
errorText: undefined,
STATES,
selectTo: this.to,
selectCc: this.cc,
selectBcc: this.bcc,
- editorPlainText: this.isPlainText,
+ editorPlainText: false,
bus: new Vue(),
}
},
@@ -302,9 +298,6 @@ export default {
isReply() {
return this.to.length > 0
},
- noSubject() {
- return this.subjectVal === '' && this.bodyVal !== ''
- },
canSend() {
return this.selectTo.length > 0 || this.selectCc.length > 0 || this.selectBcc.length > 0
},
@@ -350,7 +343,7 @@ export default {
}
},
initBody() {
- this.bodyVal = this.bodyWithSignature(this.selectedAlias, this.bodyVal)
+ this.bodyVal = this.bodyWithSignature(this.selectedAlias, this.bodyVal).value
},
recipientToRfc822(recipient) {
if (recipient.email === recipient.label) {
@@ -375,7 +368,7 @@ export default {
bcc: this.selectBcc.map(this.recipientToRfc822).join(', '),
draftUID: uid,
subject: this.subjectVal,
- body: this.editorPlainText ? htmlToText(this.bodyVal) : this.bodyVal,
+ body: html(this.bodyVal),
attachments: this.attachments,
folderId: this.replyTo ? this.replyTo.folderId : undefined,
messageId: this.replyTo ? this.replyTo.messageId : undefined,
@@ -513,10 +506,12 @@ export default {
},
bodyWithSignature(alias, body) {
if (!alias || !alias.signature) {
- return body
+ return html(body)
}
- return `${body} <br>--<br> ${textToSimpleHtml(alias.signature)}`
+ return html(body)
+ .append('<br>--<br>')
+ .append(toHtml(detect(alias.signature)))
},
},
}
diff --git a/src/components/NewMessageDetail.vue b/src/components/NewMessageDetail.vue
index 39be8ae6c..cc0f55b3c 100644
--- a/src/components/NewMessageDetail.vue
+++ b/src/components/NewMessageDetail.vue
@@ -18,7 +18,6 @@
:body="composerData.body"
:draft="saveDraft"
:send="sendMessage"
- :is-plain-text="composerData.isPlainText"
/>
</AppContentDetails>
</template>
@@ -30,14 +29,14 @@ import {generateUrl} from '@nextcloud/router'
import {
buildForwardSubject,
- buildHtmlReplyBody,
buildReplyBody,
buildReplySubject,
buildRecipients as buildReplyRecipients,
} from '../ReplyBuilder'
import Composer from './Composer'
-import {getRandomMessageErrorMessage} from '../util/ErrorMessageFactory'
import Error from './Error'
+import {getRandomMessageErrorMessage} from '../util/ErrorMessageFactory'
+import {detect, html, plain, toPlain} from '../util/text'
import Loading from './Loading'
import Logger from '../logger'
import {saveDraft, sendMessage} from '../service/MessageService'
@@ -69,8 +68,7 @@ export default {
cc: this.draft.cc,
bcc: this.draft.bcc,
subject: this.draft.subject,
- body: this.draft.body,
- isPlainText: !this.draft.hasHtmlBody,
+ body: this.draft.hasHtmlBody ? html(this.draft.body) : plain(this.draft.body),
}
} else if (this.$route.query.uid !== undefined) {
// Forward or reply to a message
@@ -118,7 +116,7 @@ export default {
to: this.stringToRecipients(this.$route.query.to),
cc: this.stringToRecipients(this.$route.query.cc),
subject: this.$route.query.subject || '',
- body: this.$route.query.body || '',
+ body: this.$route.query.body ? detect(this.$route.query.body) : html(''),
}
}
},
@@ -193,53 +191,47 @@ export default {
}
})
},
- fetchOriginalMessage(uid) {
+ async fetchOriginalMessage(uid) {
this.loading = true
this.error = undefined
this.errorMessage = ''
- this.$store
- .dispatch('fetchMessage', uid)
- .then((message) => {
- if (message.uid !== this.$route.query.uid) {
- Logger.debug("User navigated away, loaded original message won't be used")
- return
- }
+ try {
+ const message = await this.$store.dispatch('fetchMessage', uid)
+ if (message.uid !== this.$route.query.uid) {
+ Logger.debug("User navigated away, loaded original message won't be used")
+ return
+ }
- Logger.debug('original message fetched', {message})
- this.original = message
+ Logger.debug('original message fetched', {message})
+ this.original = message
- if (message.hasHtmlBody) {
- Logger.debug('original message has HTML body')
- return Axios.get(
- generateUrl('/apps/mail/api/accounts/{accountId}/folders/{folderId}/messages/{id}/html', {
- accountId: message.accountId,
- folderId: message.folderId,
- id: message.id,
- })
- ).then((resp) => resp.data)
- } else {
- return message.body
- }
- })
- .then((body) => {
- this.originalBody = body
+ let body = plain(message.body || '')
+ if (message.hasHtmlBody) {
+ Logger.debug('original message has HTML body')
+ const resp = await Axios.get(
+ generateUrl('/apps/mail/api/accounts/{accountId}/folders/{folderId}/messages/{id}/html', {
+ accountId: message.accountId,
+ folderId: message.folderId,
+ id: message.id,
+ })
+ )
+ body = html(resp.data)
+ }
+ this.originalBody = body
+ } catch (error) {
+ Logger.error('could not load original message ' + uid, {error})
+ if (error.isError) {
+ this.errorMessage = t('mail', 'Could not load original message')
+ this.error = error
this.loading = false
- })
- .catch((error) => {
- Logger.error('could not load original message ' + uid, {error})
- if (error.isError) {
- this.errorMessage = t('mail', 'Could not load original message')
- this.error = error
- this.loading = false
- }
- })
+ }
+ } finally {
+ this.loading = false
+ }
},
getForwardReplyBody() {
- if (this.original.hasHtmlBody) {
- return buildHtmlReplyBody(this.originalBody, this.original.from[0], this.original.dateInt)
- }
return buildReplyBody(this.originalBody, this.original.from[0], this.original.dateInt)
},
saveDraft(data) {
@@ -247,7 +239,11 @@ export default {
Logger.debug('draft data does not have a draftUID, adding one')
data.draftUID = this.draft.id
}
- return saveDraft(data.account, data).then(({uid}) => {
+ const dataForServer = {
+ ...data,
+ body: data.isHtml ? data.body.value : toPlain(data.body).value,
+ }
+ return saveDraft(data.account, dataForServer).then(({uid}) => {
if (this.draft === undefined) {
return uid
}
@@ -275,7 +271,11 @@ export default {
})
},
sendMessage(data) {
- return sendMessage(data.account, data)
+ const dataForServer = {
+ ...data,
+ body: data.isHtml ? data.body.value : toPlain(data.body).value,
+ }
+ return sendMessage(data.account, dataForServer)
},
},
}
diff --git a/src/components/SignatureSettings.vue b/src/components/SignatureSettings.vue
index fe0f2e4eb..0df62c1c3 100644
--- a/src/components/SignatureSettings.vue
+++ b/src/components/SignatureSettings.vue
@@ -47,7 +47,7 @@
<script>
import logger from '../logger'
import TextEditor from './TextEditor'
-import {textToSimpleHtml} from '../util/HtmlHelper'
+import {detect, html, toHtml} from '../util/text'
import Vue from 'vue'
export default {
@@ -62,17 +62,9 @@ export default {
},
},
data() {
- let signature = this.account.signature || ''
-
- if ((signature.includes('\n') || signature.includes('\r')) && !signature.includes('>')) {
- // Looks like a plain text signature -> convert to HTML
- signature = textToSimpleHtml(signature)
- logger.info('Converted plain text signature to HTML')
- }
-
return {
loading: false,
- signature,
+ signature: this.account.signature ? toHtml(detect(this.account.signature)) : html(''),
bus: new Vue(),
}
},
diff --git a/src/components/TextEditor.vue b/src/components/TextEditor.vue
index 5cc5a79b9..af79f7c2d 100644
--- a/src/components/TextEditor.vue
+++ b/src/components/TextEditor.vue
@@ -45,7 +45,6 @@ import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph'
import {getLanguage} from '@nextcloud/l10n'
import logger from '../logger'
-import {htmlToText} from '../util/HtmlHelper'
export default {
name: 'TextEditor',
diff --git a/src/service/MessageService.js b/src/service/MessageService.js
index 0be155b33..f9cb85ddc 100644
--- a/src/service/MessageService.js
+++ b/src/service/MessageService.js
@@ -121,12 +121,12 @@ export function fetchMessage(accountId, folderId, id) {
})
}
-export function saveDraft(accountId, data) {
+export async function saveDraft(accountId, data) {
const url = generateUrl('/apps/mail/api/accounts/{accountId}/draft', {
accountId,
})
- return axios.post(url, data).then((resp) => resp.data)
+ return (await axios.post(url, data)).data
}
export function sendMessage(accountId, data) {
diff --git a/src/tests/unit/ReplyBuilder.spec.js b/src/tests/unit/ReplyBuilder.spec.js
index 7191dc309..1534826b9 100644
--- a/src/tests/unit/ReplyBuilder.spec.js
+++ b/src/tests/unit/ReplyBuilder.spec.js
@@ -21,20 +21,21 @@
*/
import {buildReplyBody, buildRecipients, buildReplySubject} from '../../ReplyBuilder'
+import {html, plain} from '../../util/text'
describe('ReplyBuilder', () => {
it('creates a reply body without any sender', () => {
- const body = 'Newsletter\nhello\ncheers'
- const expectedReply = '<p></p><p></p><br>&gt; Newsletter<br>&gt; hello<br>&gt; cheers'
+ const body = plain('Newsletter\nhello\ncheers')
+ const expectedReply = html('<p></p><p></p><br>&gt; Newsletter<br>&gt; hello<br>&gt; cheers')
const replyBody = buildReplyBody(body)
- expect(replyBody).to.equal(expectedReply)
+ expect(replyBody).to.deep.equal(expectedReply)
})
it('creates a reply body', () => {
- const body = 'Newsletter\nhello'
- const expectedReply = '<p></p><p></p>"Test User" test@user.ru – November 5, 2018 '
+ const body = plain('Newsletter\nhello')
+ const expectedReply = html('<p></p><p></p>"Test User" test@user.ru – November 5, 2018 ')
const replyBody = buildReplyBody(
body,
@@ -45,7 +46,7 @@ describe('ReplyBuilder', () => {
1541426237
)
- expect(replyBody.startsWith(expectedReply)).to.be.true
+ expect(replyBody.value.startsWith(expectedReply.value)).to.be.true
})
let envelope
diff --git a/src/tests/unit/util/HtmlHelper.spec.js b/src/tests/unit/util/HtmlHelper.spec.js
deleted file mode 100644
index f3bafd93a..000000000
--- a/src/tests/unit/util/HtmlHelper.spec.js
+++ /dev/null
@@ -1,133 +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/>.
- *
- */
-
-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('breaks on divs', () => {
- const html = '<div>one</div><div>two</div>'
- const expected = 'one\ntwo'
-
- const actual = htmlToText(html)
-
- expect(actual).to.equal(expected)
- })
-
- it('produces a line break for each ending div element', () => {
- const html = '<div>' + ' <div>' + ' line1' + ' </div>' + '</div>' + '<div>line2</div>'
- const expected = ' line1\n\nline2'
-
- 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 paragraph to text', () => {
- const html = '<p>hello</p>'
- const expected = 'hello'
-
- const actual = htmlToText(html)
-
- expect(actual).to.equal(expected)
- })
-
- it('converts paragraphs to text', () => {
- const html = '<p>hello</p><p>world</p>'
- const expected = 'hello\n\nworld'
-
- 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)
- })
-
- it('preserves quotes', () => {
- const html = `<blockquote><div><b>yes.</b></div><div><br /></div><div>Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:</div><blockquote style="margin:0 0 0 .8ex;border-left:2px #729fcf solid;padding-left:1ex;"><div>ok cool</div><div><br /></div><div>Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:</div><blockquote style="margin:0 0 0 .8ex;border-left:2px #729fcf solid;padding-left:1ex;"><div>Hello</div><div><br /></div><div>this is some t<i>e</i>xt</div><div><br /></div><div>yes</div><div><br /></div><div>cheers</div><br></blockquote><br></blockquote></blockquote>`
- const expected = `> yes.
->
-> Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:
-> > ok cool
-> >
-> > Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:
-> > > Hello
-> > >
-> > > this is some text
-> > >
-> > > yes
-> > >
-> > > cheers
-> > >
-> > >
-> >`
-
- const actual = htmlToText(html)
-
- expect(actual).to.equal(expected)
- })
-})
diff --git a/src/tests/unit/util/text.spec.js b/src/tests/unit/util/text.spec.js
new file mode 100644
index 000000000..97e1c4a39
--- /dev/null
+++ b/src/tests/unit/util/text.spec.js
@@ -0,0 +1,156 @@
+/**
+ * @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 {html, toPlain, plain, detect} from '../../../util/text'
+
+describe('text', () => {
+ describe('toPlain', () => {
+ it('preserves breaks', () => {
+ const source = html('line1<br>line2')
+ const expected = plain('line1\nline2')
+
+ const actual = toPlain(source)
+
+ expect(actual).to.deep.equal(expected)
+ })
+
+ it('breaks on divs', () => {
+ const source = html('<div>one</div><div>two</div>')
+ const expected = plain('one\ntwo')
+
+ const actual = toPlain(source)
+
+ expect(actual).to.deep.equal(expected)
+ })
+
+ it('produces a line break for each ending div element', () => {
+ const source = html('<div>' + ' <div>' + ' line1' + ' </div>' + '</div>' + '<div>line2</div>')
+ const expected = plain(' line1\n\nline2')
+
+ const actual = toPlain(source)
+
+ expect(actual).to.deep.equal(expected)
+ })
+
+ it('converts blocks to text', () => {
+ const source = html('<div>hello</div>')
+ const expected = plain('hello')
+
+ const actual = toPlain(source)
+
+ expect(actual).to.deep.equal(expected)
+ })
+
+ it('converts paragraph to text', () => {
+ const source = html('<p>hello</p>')
+ const expected = plain('hello')
+
+ const actual = toPlain(source)
+
+ expect(actual).to.deep.equal(expected)
+ })
+
+ it('converts paragraphs to text', () => {
+ const source = html('<p>hello</p><p>world</p>')
+ const expected = plain('hello\n\nworld')
+
+ const actual = toPlain(source)
+
+ expect(actual).to.deep.equal(expected)
+ })
+
+ it('converts lists to text', () => {
+ const source = html('<ul><li>one</li><li>two</li><li>three</li></ul>')
+ const expected = plain(' * one\n * two\n * three')
+
+ const actual = toPlain(source)
+
+ expect(actual).to.deep.equal(expected)
+ })
+
+ it('converts deeply nested elements to text', () => {
+ const source = html(
+ '<html>' +
+ '<body><p>Hello!</p><p>this <i>is</i> <b>some</b> random <strong>text</strong></p></body>' +
+ '</html>'
+ )
+ const expected = plain('Hello!\n\nthis is some random text')
+
+ const actual = toPlain(source)
+
+ expect(actual).to.deep.equal(expected)
+ })
+
+ it('does not leak internal redirection URLs', () => {
+ const source = html('<a href="https://localhost/apps/mail/redirect?src=domain.tld">domain.tld</a>')
+ const expected = plain('domain.tld')
+
+ const actual = toPlain(source)
+
+ expect(actual).to.deep.equal(expected)
+ })
+
+ it('preserves quotes', () => {
+ const source = html(
+ `<blockquote><div><b>yes.</b></div><div><br /></div><div>Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:</div><blockquote style="margin:0 0 0 .8ex;border-left:2px #729fcf solid;padding-left:1ex;"><div>ok cool</div><div><br /></div><div>Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:</div><blockquote style="margin:0 0 0 .8ex;border-left:2px #729fcf solid;padding-left:1ex;"><div>Hello</div><div><br /></div><div>this is some t<i>e</i>xt</div><div><br /></div><div>yes</div><div><br /></div><div>cheers</div><br></blockquote><br></blockquote></blockquote>`
+ )
+ const expected = plain(`> yes.
+>
+> Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:
+> > ok cool
+> >
+> > Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:
+> > > Hello
+> > >
+> > > this is some text
+> > >
+> > > yes
+> > >
+> > > cheers
+> > >
+> > >
+> >`)
+
+ const actual = toPlain(source)
+
+ expect(actual).to.deep.equal(expected)
+ })
+ })
+
+ describe('detect', () => {
+ it('detects plain text', () => {
+ const text = 'hello world\nsecond line'
+
+ const detected = detect(text)
+
+ expect(detected).to.deep.equal(plain(text))
+ })
+
+ it('detects html', () => {
+ const text = '<p>hello world</p><p>second line</p>'
+
+ const detected = detect(text)
+
+ expect(detected).to.deep.equal(html(text))
+ })
+ })
+})
diff --git a/src/util/HtmlHelper.js b/src/util/HtmlHelper.js
deleted file mode 100644
index a1206d421..000000000
--- a/src/util/HtmlHelper.js
+++ /dev/null
@@ -1,41 +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
- */
-
-import {fromString} from 'html-to-text'
-
-export const htmlToText = (html) => {
- const withBlockBreaks = html.replace(/<\/div>/gi, '</div><br>')
-
- const text = fromString(withBlockBreaks, {
- noLinkBrackets: true,
- ignoreHref: true,
- ignoreImage: true,
- wordwrap: false,
- format: {
- blockquote: function (element, fn, options) {
- return fn(element.children, options)
- .replace(/\n\n\n/g, '\n\n') // remove triple line breaks
- .replace(/^/gm, '> ') // add > quotation to each line
- },
- paragraph: function (element, fn, options) {
- return fn(element.children, options) + '\n'
- },
- },
- })
-
- return text
- .replace(/\n\n\n/g, '\n\n') // remove triple line breaks
- .replace(/^[\n\r]+/g, '') // trim line breaks at beginning and end
- .replace(/ $/gm, '') // trim white space at end of each line
-}
-
-export const textToSimpleHtml = (text) => {
- return text.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br>$2')
-}
diff --git a/src/util/text.js b/src/util/text.js
new file mode 100644
index 000000000..f1b7f245a
--- /dev/null
+++ b/src/util/text.js
@@ -0,0 +1,149 @@
+/*
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 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 {curry} from 'ramda'
+import {fromString} from 'html-to-text'
+
+/**
+ * @type {Text}
+ */
+class Text {
+ constructor(format, value) {
+ this.format = format
+ this.value = value
+ }
+
+ /**
+ * @param {Text} other
+ * @return {Text}
+ */
+ append(other) {
+ if (this.format !== other.format) {
+ throw new Error("can't append two different formats")
+ }
+
+ return new Text(this.format, this.value + other.value)
+ }
+}
+
+/**
+ * @param {string} format
+ * @param {string} value
+ *
+ * @return {object}
+ */
+const wrap = curry((format, value) => {
+ return new Text(format, value)
+})
+
+/**
+ * @param {string} value
+ * @return {Text}
+ */
+export const plain = wrap('plain')
+
+/**
+ * @function
+ * @param {string} value
+ * @return {Text}
+ */
+export const html = wrap('html')
+
+export const detect = (str) => {
+ if (!str.includes('>')) {
+ return plain(str)
+ } else {
+ return html(str)
+ }
+}
+
+/**
+ * @function
+ * @param {string} format
+ * @param {Text} text
+ */
+const isFormat = curry((format, text) => {
+ return text.format === format
+})
+
+/**
+ * @function
+ * @param {Text} text
+ * @return {bool}
+ */
+export const isPlain = isFormat('plain')
+
+/**
+ * @function
+ * @param {Text} text
+ * @return {bool}
+ */
+export const isHtml = isFormat('html')
+
+/**
+ * @param {Text} text
+ * @return {Text}
+ */
+export const toPlain = (text) => {
+ if (text.format === 'plain') {
+ return text
+ }
+ const withBlockBreaks = text.value.replace(/<\/div>/gi, '</div><br>')
+
+ const converted = fromString(withBlockBreaks, {
+ noLinkBrackets: true,
+ ignoreHref: true,
+ ignoreImage: true,
+ wordwrap: false,
+ format: {
+ blockquote: function (element, fn, options) {
+ return fn(element.children, options)
+ .replace(/\n\n\n/g, '\n\n') // remove triple line breaks
+ .replace(/^/gm, '> ') // add > quotation to each line
+ },
+ paragraph: function (element, fn, options) {
+ return fn(element.children, options) + '\n\n'
+ },
+ },
+ })
+
+ return plain(
+ converted
+ .replace(/\n\n\n/g, '\n\n') // remove triple line breaks
+ .replace(/^[\n\r]+/g, '') // trim line breaks at beginning and end
+ .replace(/ $/gm, '') // trim white space at end of each line
+ )
+}
+
+/**
+ * @param {Text} text
+ * @return {Text}
+ */
+export const toHtml = (text) => {
+ if (text.format === 'html') {
+ return text
+ }
+ if (text.format === 'plain') {
+ return html(text.value.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br>$2'))
+ }
+
+ throw new Error(`Unknown format ${text.format}`)
+}