mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-25 18:19:22 +03:00
Merge branch 'master' into UserMailTemplates
# Conflicts: # dev/App/User.js # dev/Common/Enums.js # dev/Screen/User/Settings.js # dev/Settings/Admin/General.js # snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php # snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php # snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php
This commit is contained in:
commit
f7f56b3789
794 changed files with 96444 additions and 74188 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { isArray } from 'Common/Utils';
|
||||
import { isArray, forEachObjectEntry } from 'Common/Utils';
|
||||
|
||||
export class AbstractCollectionModel extends Array
|
||||
{
|
||||
|
|
@ -24,8 +24,7 @@ export class AbstractCollectionModel extends Array
|
|||
const result = new this();
|
||||
if (json) {
|
||||
if ('Collection/'+this.name.replace('Model', '') === json['@Object']) {
|
||||
Object.entries(json).forEach(([key, value]) => '@' !== key[0] && (result[key] = value));
|
||||
// json[@Count]
|
||||
forEachObjectEntry(json, (key, value) => '@' !== key[0] && (result[key] = value));
|
||||
json = json['@Collection'];
|
||||
}
|
||||
if (isArray(json)) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { change } from 'Common/Links';
|
||||
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
export class AccountModel extends AbstractModel {
|
||||
|
|
@ -8,23 +6,16 @@ export class AccountModel extends AbstractModel {
|
|||
* @param {boolean=} canBeDelete = true
|
||||
* @param {number=} count = 0
|
||||
*/
|
||||
constructor(email, canBeDelete = true, count = 0) {
|
||||
constructor(email/*, count = 0*/, isAdditional = true) {
|
||||
super();
|
||||
|
||||
this.email = email;
|
||||
|
||||
this.addObservables({
|
||||
count: count,
|
||||
deleteAccess: false,
|
||||
canBeDeleted: !!canBeDelete
|
||||
// count: count || 0,
|
||||
askDelete: false,
|
||||
isAdditional: isAdditional
|
||||
});
|
||||
this.canBeEdit = this.canBeDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
changeAccountLink() {
|
||||
return change(this.email);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,17 +21,20 @@ export class AttachmentModel extends AbstractModel {
|
|||
this.fileNameExt = '';
|
||||
this.fileType = FileType.Unknown;
|
||||
this.friendlySize = '';
|
||||
this.isInline = false;
|
||||
this.isLinked = false;
|
||||
this.isThumbnail = false;
|
||||
this.cid = '';
|
||||
this.cidWithoutTags = '';
|
||||
this.contentLocation = '';
|
||||
this.download = '';
|
||||
this.folder = '';
|
||||
this.uid = '';
|
||||
this.url = '';
|
||||
this.mimeIndex = '';
|
||||
this.framed = false;
|
||||
|
||||
this.addObservables({
|
||||
isInline: false,
|
||||
isLinked: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -43,7 +46,6 @@ export class AttachmentModel extends AbstractModel {
|
|||
const attachment = super.reviveFromJson(json);
|
||||
if (attachment) {
|
||||
attachment.friendlySize = FileInfo.friendlySize(json.EstimatedSize);
|
||||
attachment.cidWithoutTags = attachment.cid.replace(/^<+/, '').replace(/>+$/, '');
|
||||
|
||||
attachment.fileNameExt = FileInfo.getExtension(attachment.fileName);
|
||||
attachment.fileType = FileInfo.getType(attachment.fileNameExt, attachment.mimeType);
|
||||
|
|
@ -51,6 +53,10 @@ export class AttachmentModel extends AbstractModel {
|
|||
return attachment;
|
||||
}
|
||||
|
||||
contentId() {
|
||||
return this.cid.replace(/^<+|>+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
|
@ -122,29 +128,21 @@ export class AttachmentModel extends AbstractModel {
|
|||
* @returns {string}
|
||||
*/
|
||||
linkDownload() {
|
||||
return attachmentDownload(this.download);
|
||||
return this.url || attachmentDownload(this.download);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
linkPreview() {
|
||||
return serverRequestRaw('View', this.download);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
linkThumbnail() {
|
||||
return this.hasThumbnail() ? serverRequestRaw('ViewThumbnail', this.download) : '';
|
||||
return this.url || serverRequestRaw('View', this.download);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
linkThumbnailPreviewStyle() {
|
||||
const link = this.linkThumbnail();
|
||||
return link ? 'background:url(' + link + ')' : '';
|
||||
return this.hasThumbnail() ? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -175,7 +173,7 @@ export class AttachmentModel extends AbstractModel {
|
|||
const localEvent = event.originalEvent || event;
|
||||
if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) {
|
||||
let link = this.linkDownload();
|
||||
if ('http' !== link.substr(0, 4)) {
|
||||
if ('http' !== link.slice(0, 4)) {
|
||||
link = location.protocol + '//' + location.host + location.pathname + link;
|
||||
}
|
||||
localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link);
|
||||
|
|
|
|||
|
|
@ -11,13 +11,20 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
|||
*/
|
||||
static reviveFromJson(items) {
|
||||
return super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
||||
/*
|
||||
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
||||
if (attachments) {
|
||||
attachments.InlineCount = attachments.reduce((accumulator, a) => accumulator + (a.isInline ? 1 : 0), 0);
|
||||
}
|
||||
return attachments;
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasVisible() {
|
||||
return !!this.find(item => !item.isLinked);
|
||||
return !!this.filter(item => !item.isLinked()).length;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -25,7 +32,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
|||
* @returns {*}
|
||||
*/
|
||||
findByCid(cid) {
|
||||
cid = cid.replace(/^<+|>+$/, '');
|
||||
return this.find(item => cid === item.cidWithoutTags);
|
||||
cid = cid.replace(/^<+|>+$/g, '');
|
||||
return this.find(item => cid === item.contentId());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,8 +65,8 @@ export class ComposeAttachmentModel extends AbstractModel {
|
|||
item.download,
|
||||
item.fileName,
|
||||
item.estimatedSize,
|
||||
item.isInline,
|
||||
item.isLinked,
|
||||
item.isInline(),
|
||||
item.isLinked(),
|
||||
item.cid,
|
||||
item.contentLocation
|
||||
);
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ class Tokenizer
|
|||
}
|
||||
}
|
||||
|
||||
class EmailModel extends AbstractModel {
|
||||
export class EmailModel extends AbstractModel {
|
||||
/**
|
||||
* @param {string=} email = ''
|
||||
* @param {string=} name = ''
|
||||
|
|
@ -430,5 +430,3 @@ class EmailModel extends AbstractModel {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { EmailModel, EmailModel as default };
|
||||
|
|
|
|||
|
|
@ -1,276 +0,0 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { arrayLength, pString } from 'Common/Utils';
|
||||
import { delegateRunOnDestroy } from 'Common/UtilsUser';
|
||||
import { i18n } from 'Common/Translator';
|
||||
import { getFolderFromCacheList } from 'Common/Cache';
|
||||
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
|
||||
import { FilterConditionModel } from 'Model/FilterCondition';
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const FilterAction = {
|
||||
None: 'None',
|
||||
MoveTo: 'MoveTo',
|
||||
Discard: 'Discard',
|
||||
Vacation: 'Vacation',
|
||||
Reject: 'Reject',
|
||||
Forward: 'Forward'
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
const FilterRulesType = {
|
||||
All: 'All',
|
||||
Any: 'Any'
|
||||
};
|
||||
|
||||
export class FilterModel extends AbstractModel {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.id = '';
|
||||
|
||||
this.addObservables({
|
||||
enabled: true,
|
||||
deleteAccess: false,
|
||||
canBeDeleted: true,
|
||||
|
||||
name: '',
|
||||
nameError: false,
|
||||
nameFocused: false,
|
||||
|
||||
conditionsType: FilterRulesType.Any,
|
||||
|
||||
// Actions
|
||||
actionValue: '',
|
||||
actionValueError: false,
|
||||
|
||||
actionValueSecond: '',
|
||||
actionValueThird: '',
|
||||
|
||||
actionValueFourth: '',
|
||||
actionValueFourthError: false,
|
||||
|
||||
actionMarkAsRead: false,
|
||||
|
||||
actionKeep: true,
|
||||
actionNoStop: false,
|
||||
|
||||
actionType: FilterAction.MoveTo
|
||||
});
|
||||
|
||||
this.conditions = ko.observableArray();
|
||||
|
||||
const fGetRealFolderName = (folderFullNameRaw) => {
|
||||
const folder = getFolderFromCacheList(folderFullNameRaw);
|
||||
return folder ? folder.fullName.replace('.' === folder.delimiter ? /\./ : /[\\/]+/, ' / ') : folderFullNameRaw;
|
||||
};
|
||||
|
||||
this.addComputables({
|
||||
nameSub: () => {
|
||||
let result = '';
|
||||
const actionValue = this.actionValue(), root = 'SETTINGS_FILTERS/SUBNAME_';
|
||||
|
||||
switch (this.actionType()) {
|
||||
case FilterAction.MoveTo:
|
||||
result = i18n(root + 'MOVE_TO', {
|
||||
FOLDER: fGetRealFolderName(actionValue)
|
||||
});
|
||||
break;
|
||||
case FilterAction.Forward:
|
||||
result = i18n(root + 'FORWARD_TO', {
|
||||
EMAIL: actionValue
|
||||
});
|
||||
break;
|
||||
case FilterAction.Vacation:
|
||||
result = i18n(root + 'VACATION_MESSAGE');
|
||||
break;
|
||||
case FilterAction.Reject:
|
||||
result = i18n(root + 'REJECT');
|
||||
break;
|
||||
case FilterAction.Discard:
|
||||
result = i18n(root + 'DISCARD');
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
return result ? '(' + result + ')' : '';
|
||||
},
|
||||
|
||||
actionTemplate: () => {
|
||||
const result = 'SettingsFiltersAction';
|
||||
switch (this.actionType()) {
|
||||
case FilterAction.Forward:
|
||||
return result + 'Forward';
|
||||
case FilterAction.Vacation:
|
||||
return result + 'Vacation';
|
||||
case FilterAction.Reject:
|
||||
return result + 'Reject';
|
||||
case FilterAction.None:
|
||||
return result + 'None';
|
||||
case FilterAction.Discard:
|
||||
return result + 'Discard';
|
||||
case FilterAction.MoveTo:
|
||||
default:
|
||||
return result + 'MoveToFolder';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.addSubscribables({
|
||||
name: sValue => this.nameError(!sValue),
|
||||
actionValue: sValue => this.actionValueError(!sValue),
|
||||
actionType: () => {
|
||||
this.actionValue('');
|
||||
this.actionValueError(false);
|
||||
this.actionValueSecond('');
|
||||
this.actionValueThird('');
|
||||
this.actionValueFourth('');
|
||||
this.actionValueFourthError(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateID() {
|
||||
this.id = Jua.randomId();
|
||||
}
|
||||
|
||||
verify() {
|
||||
if (!this.name()) {
|
||||
this.nameError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.conditions.length && this.conditions.find(cond => cond && !cond.verify())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.actionValue()) {
|
||||
if ([
|
||||
FilterAction.MoveTo,
|
||||
FilterAction.Forward,
|
||||
FilterAction.Reject,
|
||||
FilterAction.Vacation
|
||||
].includes(this.actionType())
|
||||
) {
|
||||
this.actionValueError(true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (FilterAction.Forward === this.actionType() && !this.actionValue().includes('@')) {
|
||||
this.actionValueError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
FilterAction.Vacation === this.actionType() &&
|
||||
this.actionValueFourth() &&
|
||||
!this.actionValueFourth().includes('@')
|
||||
) {
|
||||
this.actionValueFourthError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.nameError(false);
|
||||
this.actionValueError(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
// '@Object': 'Object/Filter',
|
||||
ID: this.id,
|
||||
Enabled: this.enabled() ? 1 : 0,
|
||||
Name: this.name(),
|
||||
Conditions: this.conditions.map(item => item.toJson()),
|
||||
ConditionsType: this.conditionsType(),
|
||||
|
||||
ActionType: this.actionType(),
|
||||
ActionValue: this.actionValue(),
|
||||
ActionValueSecond: this.actionValueSecond(),
|
||||
ActionValueThird: this.actionValueThird(),
|
||||
ActionValueFourth: this.actionValueFourth(),
|
||||
|
||||
Keep: this.actionKeep() ? 1 : 0,
|
||||
Stop: this.actionNoStop() ? 0 : 1,
|
||||
MarkAsRead: this.actionMarkAsRead() ? 1 : 0
|
||||
};
|
||||
}
|
||||
|
||||
addCondition() {
|
||||
this.conditions.push(new FilterConditionModel());
|
||||
}
|
||||
|
||||
removeCondition(oConditionToDelete) {
|
||||
this.conditions.remove(oConditionToDelete);
|
||||
delegateRunOnDestroy(oConditionToDelete);
|
||||
}
|
||||
|
||||
setRecipients() {
|
||||
this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', '));
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @param {FetchJsonFilter} json
|
||||
* @returns {?FilterModel}
|
||||
*/
|
||||
static reviveFromJson(json) {
|
||||
const filter = super.reviveFromJson(json);
|
||||
if (filter) {
|
||||
filter.id = pString(json.ID);
|
||||
|
||||
filter.conditions([]);
|
||||
|
||||
if (arrayLength(json.Conditions)) {
|
||||
filter.conditions(
|
||||
json.Conditions.map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v)
|
||||
);
|
||||
}
|
||||
|
||||
filter.actionKeep(0 != json.Keep);
|
||||
filter.actionNoStop(0 == json.Stop);
|
||||
filter.actionMarkAsRead(1 == json.MarkAsRead);
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
cloneSelf() {
|
||||
const filter = new FilterModel();
|
||||
|
||||
filter.id = this.id;
|
||||
|
||||
filter.enabled(this.enabled());
|
||||
|
||||
filter.name(this.name());
|
||||
filter.nameError(this.nameError());
|
||||
|
||||
filter.conditionsType(this.conditionsType());
|
||||
|
||||
filter.actionMarkAsRead(this.actionMarkAsRead());
|
||||
|
||||
filter.actionType(this.actionType());
|
||||
|
||||
filter.actionValue(this.actionValue());
|
||||
filter.actionValueError(this.actionValueError());
|
||||
|
||||
filter.actionValueSecond(this.actionValueSecond());
|
||||
filter.actionValueThird(this.actionValueThird());
|
||||
filter.actionValueFourth(this.actionValueFourth());
|
||||
|
||||
filter.actionKeep(this.actionKeep());
|
||||
filter.actionNoStop(this.actionNoStop());
|
||||
|
||||
filter.conditions(this.conditions.map(item => item.cloneSelf()));
|
||||
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const FilterConditionField = {
|
||||
From: 'From',
|
||||
Recipient: 'Recipient',
|
||||
Subject: 'Subject',
|
||||
Header: 'Header',
|
||||
Body: 'Body',
|
||||
Size: 'Size'
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const FilterConditionType = {
|
||||
Contains: 'Contains',
|
||||
NotContains: 'NotContains',
|
||||
EqualTo: 'EqualTo',
|
||||
NotEqualTo: 'NotEqualTo',
|
||||
Regex: 'Regex',
|
||||
Over: 'Over',
|
||||
Under: 'Under',
|
||||
Text: 'Text',
|
||||
Raw: 'Raw'
|
||||
};
|
||||
|
||||
export class FilterConditionModel extends AbstractModel {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.addObservables({
|
||||
field: FilterConditionField.From,
|
||||
type: FilterConditionType.Contains,
|
||||
value: '',
|
||||
valueError: false,
|
||||
|
||||
valueSecond: '',
|
||||
valueSecondError: false
|
||||
});
|
||||
|
||||
this.template = ko.computed(() => {
|
||||
const template = 'SettingsFiltersCondition';
|
||||
switch (this.field()) {
|
||||
case FilterConditionField.Body:
|
||||
return template + 'Body';
|
||||
case FilterConditionField.Size:
|
||||
return template + 'Size';
|
||||
case FilterConditionField.Header:
|
||||
return template + 'More';
|
||||
default:
|
||||
return template + 'Default';
|
||||
}
|
||||
});
|
||||
|
||||
this.addSubscribables({
|
||||
field: () => {
|
||||
this.value('');
|
||||
this.valueSecond('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
verify() {
|
||||
if (!this.value()) {
|
||||
this.valueError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FilterConditionField.Header === this.field() && !this.valueSecond()) {
|
||||
this.valueSecondError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// static reviveFromJson(json) {}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
// '@Object': 'Object/FilterCondition',
|
||||
Field: this.field(),
|
||||
Type: this.type(),
|
||||
Value: this.value(),
|
||||
ValueSecond: this.valueSecond()
|
||||
};
|
||||
}
|
||||
|
||||
cloneSelf() {
|
||||
const filterCond = new FilterConditionModel();
|
||||
|
||||
filterCond.field(this.field());
|
||||
filterCond.type(this.type());
|
||||
filterCond.value(this.value());
|
||||
filterCond.valueSecond(this.valueSecond());
|
||||
|
||||
return filterCond;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +1,112 @@
|
|||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||
|
||||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||
import { isArray, pInt } from 'Common/Utils';
|
||||
import { ClientSideKeyName, FolderType } from 'Common/EnumsUser';
|
||||
import * as Cache from 'Common/Cache';
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
||||
import { getFolderFromCacheList, setFolder, setFolderInboxName, setFolderHash } from 'Common/Cache';
|
||||
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
||||
|
||||
import * as Local from 'Storage/Client';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
import ko from 'ko';
|
||||
|
||||
import { isPosNumeric } from 'Common/UtilsUser';
|
||||
import { sortFolders } from 'Common/Folders';
|
||||
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
|
||||
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
const
|
||||
normalizeFolder = sFolderFullNameRaw => ('' === sFolderFullNameRaw
|
||||
|| UNUSED_OPTION_VALUE === sFolderFullNameRaw
|
||||
|| null !== Cache.getFolderFromCacheList(sFolderFullNameRaw))
|
||||
? sFolderFullNameRaw
|
||||
: '';
|
||||
import { koComputable } from 'External/ko';
|
||||
|
||||
// index is FolderType value
|
||||
let SystemFolders = [];
|
||||
//import { mailBox } from 'Common/Links';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
const
|
||||
isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
|
||||
|
||||
normalizeFolder = sFolderFullName => ('' === sFolderFullName
|
||||
|| UNUSED_OPTION_VALUE === sFolderFullName
|
||||
|| null !== getFolderFromCacheList(sFolderFullName))
|
||||
? sFolderFullName
|
||||
: '',
|
||||
|
||||
SystemFolders = {
|
||||
Inbox: 0,
|
||||
Sent: 0,
|
||||
Drafts: 0,
|
||||
Spam: 0,
|
||||
Trash: 0,
|
||||
Archive: 0
|
||||
},
|
||||
|
||||
kolabTypes = {
|
||||
configuration: 'CONFIGURATION',
|
||||
event: 'CALENDAR',
|
||||
contact: 'CONTACTS',
|
||||
task: 'TASKS',
|
||||
note: 'NOTES',
|
||||
file: 'FILES',
|
||||
journal: 'JOURNAL'
|
||||
},
|
||||
|
||||
getKolabFolderName = type => kolabTypes[type] ? 'Kolab ' + i18n('SETTINGS_FOLDERS/TYPE_' + kolabTypes[type]) : '',
|
||||
|
||||
getSystemFolderName = (type, def) => {
|
||||
switch (type) {
|
||||
case FolderType.Inbox:
|
||||
case FolderType.Sent:
|
||||
case FolderType.Drafts:
|
||||
case FolderType.Trash:
|
||||
case FolderType.Archive:
|
||||
return i18n('FOLDER_LIST/' + getKeyByValue(FolderType, type).toUpperCase() + '_NAME');
|
||||
case FolderType.Spam:
|
||||
return i18n('GLOBAL/SPAM');
|
||||
// no default
|
||||
}
|
||||
return def;
|
||||
};
|
||||
|
||||
export const
|
||||
/**
|
||||
* @param {string} sFullName
|
||||
* @param {boolean} bExpanded
|
||||
*/
|
||||
setExpandedFolder = (sFullName, bExpanded) => {
|
||||
let aExpandedList = Local.get(ClientSideKeyNameExpandedFolders);
|
||||
if (!isArray(aExpandedList)) {
|
||||
aExpandedList = [];
|
||||
}
|
||||
|
||||
if (bExpanded) {
|
||||
aExpandedList.includes(sFullName) || aExpandedList.push(sFullName);
|
||||
} else {
|
||||
aExpandedList = aExpandedList.filter(value => value !== sFullName);
|
||||
}
|
||||
|
||||
Local.set(ClientSideKeyNameExpandedFolders, aExpandedList);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
*/
|
||||
loadFolders = fCallback => {
|
||||
// clearTimeout(this.foldersTimeout);
|
||||
Remote.abort('Folders')
|
||||
.post('Folders', FolderUserStore.foldersLoading)
|
||||
.then(data => {
|
||||
data = FolderCollectionModel.reviveFromJson(data.Result);
|
||||
data && data.storeIt();
|
||||
fCallback && fCallback(true);
|
||||
// Repeat every 15 minutes?
|
||||
// this.foldersTimeout = setTimeout(loadFolders, 900000);
|
||||
})
|
||||
.catch(() => fCallback && setTimeout(fCallback, 1, false));
|
||||
};
|
||||
|
||||
export class FolderCollectionModel extends AbstractCollectionModel
|
||||
{
|
||||
|
|
@ -36,11 +114,11 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
constructor() {
|
||||
super();
|
||||
this.CountRec
|
||||
this.FoldersHash
|
||||
this.IsThreadsSupported
|
||||
this.Namespace;
|
||||
this.Optimized
|
||||
this.SystemFolders
|
||||
this.Capabilities
|
||||
}
|
||||
*/
|
||||
|
||||
|
|
@ -49,53 +127,40 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
* @returns {FolderCollectionModel}
|
||||
*/
|
||||
static reviveFromJson(object) {
|
||||
const expandedFolders = Local.get(ClientSideKeyName.ExpandedFolders);
|
||||
const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders);
|
||||
if (object && object.SystemFolders) {
|
||||
let sf = object.SystemFolders;
|
||||
SystemFolders = [
|
||||
/* USER */ 0,
|
||||
/* INBOX */ sf[1],
|
||||
SettingsGet('SentFolder') || sf[2],
|
||||
SettingsGet('DraftFolder') || sf[3],
|
||||
SettingsGet('SpamFolder') || sf[4],
|
||||
SettingsGet('TrashFolder') || sf[5],
|
||||
SettingsGet('ArchiveFolder') || sf[12]
|
||||
// IMPORTANT: sf[10],
|
||||
// FLAGGED: sf[11],
|
||||
// ALL: sf[13]
|
||||
];
|
||||
forEachObjectEntry(SystemFolders, key =>
|
||||
SystemFolders[key] = SettingsGet(key+'Folder') || object.SystemFolders[FolderType[key]]
|
||||
);
|
||||
}
|
||||
|
||||
return super.reviveFromJson(object, oFolder => {
|
||||
let oCacheFolder = Cache.getFolderFromCacheList(oFolder.FullNameRaw);
|
||||
const result = super.reviveFromJson(object, oFolder => {
|
||||
let oCacheFolder = getFolderFromCacheList(oFolder.FullName),
|
||||
type = FolderType[getKeyByValue(SystemFolders, oFolder.FullName)];
|
||||
|
||||
if (oCacheFolder) {
|
||||
oFolder.SubFolders = FolderCollectionModel.reviveFromJson(oFolder.SubFolders);
|
||||
oFolder.SubFolders && oCacheFolder.subFolders(oFolder.SubFolders);
|
||||
} else {
|
||||
if (!oCacheFolder) {
|
||||
oCacheFolder = FolderModel.reviveFromJson(oFolder);
|
||||
if (!oCacheFolder)
|
||||
return null;
|
||||
|
||||
if (1 == SystemFolders.indexOf(oFolder.FullNameRaw)) {
|
||||
if (1 == type) {
|
||||
oCacheFolder.type(FolderType.Inbox);
|
||||
Cache.setFolderInboxName(oFolder.FullNameRaw);
|
||||
setFolderInboxName(oFolder.FullName);
|
||||
}
|
||||
Cache.setFolder(oCacheFolder.fullNameHash, oFolder.FullNameRaw, oCacheFolder);
|
||||
setFolder(oCacheFolder);
|
||||
}
|
||||
|
||||
let type = SystemFolders.indexOf(oFolder.FullNameRaw);
|
||||
if (1 < type) {
|
||||
oCacheFolder.type(type);
|
||||
}
|
||||
|
||||
oCacheFolder.collapsed(!expandedFolders
|
||||
|| !isArray(expandedFolders)
|
||||
|| !expandedFolders.includes(oCacheFolder.fullNameHash));
|
||||
|| !expandedFolders.includes(oCacheFolder.fullName));
|
||||
|
||||
if (oFolder.Extended) {
|
||||
if (oFolder.Extended.Hash) {
|
||||
Cache.setFolderHash(oCacheFolder.fullNameRaw, oFolder.Extended.Hash);
|
||||
setFolderHash(oCacheFolder.fullName, oFolder.Extended.Hash);
|
||||
}
|
||||
|
||||
if (null != oFolder.Extended.MessageCount) {
|
||||
|
|
@ -108,89 +173,77 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
}
|
||||
return oCacheFolder;
|
||||
});
|
||||
|
||||
let i = result.length;
|
||||
if (i) {
|
||||
sortFolders(result);
|
||||
try {
|
||||
while (i--) {
|
||||
let folder = result[i], parent = getFolderFromCacheList(folder.parentName);
|
||||
if (parent) {
|
||||
parent.subFolders.unshift(folder);
|
||||
result.splice(i,1);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
storeIt() {
|
||||
const cnt = pInt(this.CountRec);
|
||||
FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
|
||||
|
||||
FolderUserStore.displaySpecSetting(0 >= cnt
|
||||
|| Math.max(10, Math.min(100, pInt(Settings.app('folderSpecLimit')))) < cnt);
|
||||
|
||||
if (SystemFolders &&
|
||||
!('' +
|
||||
if (!(
|
||||
SettingsGet('SentFolder') +
|
||||
SettingsGet('DraftFolder') +
|
||||
SettingsGet('DraftsFolder') +
|
||||
SettingsGet('SpamFolder') +
|
||||
SettingsGet('TrashFolder') +
|
||||
SettingsGet('ArchiveFolder'))
|
||||
SettingsGet('ArchiveFolder')
|
||||
)
|
||||
) {
|
||||
FolderUserStore.saveSystemFolders({
|
||||
SentFolder: SystemFolders[FolderType.SENT] || null,
|
||||
DraftFolder: SystemFolders[FolderType.DRAFTS] || null,
|
||||
SpamFolder: SystemFolders[FolderType.SPAM] || null,
|
||||
TrashFolder: SystemFolders[FolderType.TRASH] || null,
|
||||
ArchiveFolder: SystemFolders[FolderType.ARCHIVE] || null
|
||||
});
|
||||
FolderUserStore.saveSystemFolders(SystemFolders);
|
||||
}
|
||||
|
||||
FolderUserStore.folderList(this);
|
||||
|
||||
if (undefined !== this.Namespace) {
|
||||
FolderUserStore.namespace = this.Namespace;
|
||||
}
|
||||
FolderUserStore.namespace = this.Namespace;
|
||||
|
||||
AppUserStore.threadsAllowed(!!(Settings.app('useImapThread') && this.IsThreadsSupported));
|
||||
|
||||
FolderUserStore.folderListOptimized(!!this.Optimized);
|
||||
FolderUserStore.sortSupported(!!this.IsSortSupported);
|
||||
FolderUserStore.quotaUsage(this.quotaUsage);
|
||||
FolderUserStore.quotaLimit(this.quotaLimit);
|
||||
FolderUserStore.capabilities(this.Capabilities);
|
||||
|
||||
FolderUserStore.sentFolder(normalizeFolder(SettingsGet('SentFolder')));
|
||||
FolderUserStore.draftFolder(normalizeFolder(SettingsGet('DraftFolder')));
|
||||
FolderUserStore.spamFolder(normalizeFolder(SettingsGet('SpamFolder')));
|
||||
FolderUserStore.trashFolder(normalizeFolder(SettingsGet('TrashFolder')));
|
||||
FolderUserStore.archiveFolder(normalizeFolder(SettingsGet('ArchiveFolder')));
|
||||
FolderUserStore.sentFolder(normalizeFolder(SystemFolders.Sent));
|
||||
FolderUserStore.draftsFolder(normalizeFolder(SystemFolders.Drafts));
|
||||
FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Spam));
|
||||
FolderUserStore.trashFolder(normalizeFolder(SystemFolders.Trash));
|
||||
FolderUserStore.archiveFolder(normalizeFolder(SystemFolders.Archive));
|
||||
|
||||
// FolderUserStore.folderList.valueHasMutated();
|
||||
|
||||
Local.set(ClientSideKeyName.FoldersLashHash, this.FoldersHash);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getSystemFolderName(type, def)
|
||||
{
|
||||
switch (type) {
|
||||
case FolderType.Inbox:
|
||||
return i18n('FOLDER_LIST/INBOX_NAME');
|
||||
case FolderType.Sent:
|
||||
return i18n('FOLDER_LIST/SENT_NAME');
|
||||
case FolderType.Drafts:
|
||||
return i18n('FOLDER_LIST/DRAFTS_NAME');
|
||||
case FolderType.Spam:
|
||||
return i18n('GLOBAL/SPAM');
|
||||
case FolderType.Trash:
|
||||
return i18n('FOLDER_LIST/TRASH_NAME');
|
||||
case FolderType.Archive:
|
||||
return i18n('FOLDER_LIST/ARCHIVE_NAME');
|
||||
// no default
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
export class FolderModel extends AbstractModel {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.fullName = '';
|
||||
this.fullNameRaw = '';
|
||||
this.fullNameHash = '';
|
||||
this.delimiter = '';
|
||||
this.namespace = '';
|
||||
this.deep = 0;
|
||||
this.expires = 0;
|
||||
this.metadata = {};
|
||||
|
||||
this.exists = true;
|
||||
|
||||
// this.hash = '';
|
||||
// this.uidNext = 0;
|
||||
|
||||
this.addObservables({
|
||||
name: '',
|
||||
type: FolderType.User,
|
||||
|
|
@ -200,21 +253,36 @@ export class FolderModel extends AbstractModel {
|
|||
selected: false,
|
||||
edited: false,
|
||||
subscribed: true,
|
||||
checkable: false,
|
||||
deleteAccess: false,
|
||||
checkable: false, // Check for new messages
|
||||
askDelete: false,
|
||||
|
||||
nameForEdit: '',
|
||||
errorMsg: '',
|
||||
|
||||
privateMessageCountAll: 0,
|
||||
privateMessageCountUnread: 0,
|
||||
|
||||
collapsedPrivate: true
|
||||
kolabType: null,
|
||||
|
||||
collapsed: true
|
||||
});
|
||||
|
||||
this.addSubscribables({
|
||||
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue
|
||||
});
|
||||
|
||||
this.subFolders = ko.observableArray(new FolderCollectionModel);
|
||||
this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* For url safe '/#/mailbox/...' path
|
||||
*/
|
||||
get fullNameHash() {
|
||||
return this.fullName.replace(/[^a-z0-9._-]+/giu, b64EncodeJSONSafe);
|
||||
// return /^[a-z0-9._-]+$/iu.test(this.fullName) ? this.fullName : b64EncodeJSONSafe(this.fullName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @param {FetchJsonFolder} json
|
||||
|
|
@ -223,9 +291,19 @@ export class FolderModel extends AbstractModel {
|
|||
static reviveFromJson(json) {
|
||||
const folder = super.reviveFromJson(json);
|
||||
if (folder) {
|
||||
folder.deep = json.FullNameRaw.split(folder.delimiter).length - 1;
|
||||
const path = folder.fullName.split(folder.delimiter),
|
||||
type = (folder.metadata[FolderMetadataKeys.KolabFolderType]
|
||||
|| folder.metadata[FolderMetadataKeys.KolabFolderTypeShared]
|
||||
|| ''
|
||||
).split('.')[0];
|
||||
|
||||
folder.messageCountAll = ko.computed({
|
||||
folder.deep = path.length - 1;
|
||||
path.pop();
|
||||
folder.parentName = path.join(folder.delimiter);
|
||||
|
||||
type && 'mail' != type && folder.kolabType(type);
|
||||
|
||||
folder.messageCountAll = koComputable({
|
||||
read: folder.privateMessageCountAll,
|
||||
write: (iValue) => {
|
||||
if (isPosNumeric(iValue)) {
|
||||
|
|
@ -237,7 +315,7 @@ export class FolderModel extends AbstractModel {
|
|||
})
|
||||
.extend({ notify: 'always' });
|
||||
|
||||
folder.messageCountUnread = ko.computed({
|
||||
folder.messageCountUnread = koComputable({
|
||||
read: folder.privateMessageCountUnread,
|
||||
write: (value) => {
|
||||
if (isPosNumeric(value)) {
|
||||
|
|
@ -254,29 +332,45 @@ export class FolderModel extends AbstractModel {
|
|||
isInbox: () => FolderType.Inbox === folder.type(),
|
||||
|
||||
isFlagged: () => FolderUserStore.currentFolder() === folder
|
||||
&& MessageUserStore.listSearch().trim().includes('is:flagged'),
|
||||
&& MessagelistUserStore.listSearch().includes('flagged'),
|
||||
|
||||
hasSubscribedSubfolders:
|
||||
() => !!folder.subFolders().find(
|
||||
hasVisibleSubfolders: () => !!folder.subFolders().find(folder => folder.visible()),
|
||||
|
||||
hasSubscriptions: () => folder.subscribed() | !!folder.subFolders().find(
|
||||
oFolder => {
|
||||
const subscribed = oFolder.hasSubscriptions();
|
||||
return !oFolder.isSystemFolder() && subscribed;
|
||||
}
|
||||
),
|
||||
|
||||
hasSubscriptions: () => folder.subscribed() | folder.hasSubscribedSubfolders(),
|
||||
|
||||
canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/,
|
||||
|
||||
visible: () => folder.hasSubscriptions() | !SettingsUserStore.hideUnsubscribed(),
|
||||
isSystemFolder: () => FolderType.User !== folder.type() | !!folder.kolabType(),
|
||||
|
||||
isSystemFolder: () => FolderType.User !== folder.type(),
|
||||
canBeSelected: () => folder.selectable() && !folder.isSystemFolder(),
|
||||
|
||||
hidden: () => {
|
||||
let hasSubFolders = folder.hasSubscribedSubfolders();
|
||||
return (folder.isSystemFolder() | !folder.selectable()) && !hasSubFolders;
|
||||
canBeDeleted: () => folder.canBeSelected() && folder.exists,
|
||||
|
||||
canBeSubscribed: () => folder.selectable()
|
||||
&& !(folder.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
|
||||
|
||||
/**
|
||||
* Folder is visible when:
|
||||
* - hasVisibleSubfolders()
|
||||
* Or when all below conditions are true:
|
||||
* - selectable()
|
||||
* - subscribed() OR hideUnsubscribed = false
|
||||
* - FolderType.User
|
||||
* - not kolabType()
|
||||
*/
|
||||
visible: () => {
|
||||
const selectable = folder.canBeSelected(),
|
||||
visible = (folder.subscribed() | !SettingsUserStore.hideUnsubscribed()) && selectable;
|
||||
return folder.hasVisibleSubfolders() | visible;
|
||||
},
|
||||
|
||||
hidden: () => !folder.selectable() && (folder.isSystemFolder() | !folder.hasVisibleSubfolders()),
|
||||
|
||||
printableUnreadCount: () => {
|
||||
const count = folder.messageCountAll(),
|
||||
unread = folder.messageCountUnread(),
|
||||
|
|
@ -299,13 +393,6 @@ export class FolderModel extends AbstractModel {
|
|||
return null;
|
||||
},
|
||||
|
||||
canBeDeleted: () => !(folder.isSystemFolder() | !folder.selectable()),
|
||||
|
||||
canBeSubscribed: () => Settings.app('useImapSubscribe')
|
||||
&& !(folder.isSystemFolder() | !SettingsUserStore.hideUnsubscribed() | !folder.selectable()),
|
||||
|
||||
canBeSelected: () => !(folder.isSystemFolder() | !folder.selectable()),
|
||||
|
||||
localName: () => {
|
||||
let name = folder.name();
|
||||
if (folder.isSystemFolder()) {
|
||||
|
|
@ -318,28 +405,22 @@ export class FolderModel extends AbstractModel {
|
|||
manageFolderSystemName: () => {
|
||||
if (folder.isSystemFolder()) {
|
||||
translatorTrigger();
|
||||
let suffix = getSystemFolderName(folder.type(), '');
|
||||
let suffix = getSystemFolderName(folder.type(), getKolabFolderName(folder.kolabType()));
|
||||
if (folder.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
|
||||
return '(' + suffix + ')';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
collapsed: {
|
||||
read: () => folder.isInbox() && FolderUserStore.singleRootFolder()
|
||||
? false
|
||||
: !folder.hidden() && folder.collapsedPrivate(),
|
||||
write: value => folder.collapsedPrivate(value)
|
||||
},
|
||||
|
||||
hasUnreadMessages: () => 0 < folder.messageCountUnread() && folder.printableUnreadCount(),
|
||||
|
||||
hasSubscribedUnreadMessagesSubfolders: () =>
|
||||
!!folder.subFolders().find(
|
||||
folder => folder.hasUnreadMessages() || folder.hasSubscribedUnreadMessagesSubfolders()
|
||||
)
|
||||
!!folder.subFolders().find(
|
||||
folder => folder.hasUnreadMessages() | folder.hasSubscribedUnreadMessagesSubfolders()
|
||||
)
|
||||
|
||||
// ,href: () => folder.canBeSelected() && mailBox(folder.fullNameHash)
|
||||
});
|
||||
|
||||
folder.addSubscribables({
|
||||
|
|
@ -349,7 +430,7 @@ export class FolderModel extends AbstractModel {
|
|||
|
||||
messageCountUnread: unread => {
|
||||
if (FolderType.Inbox === folder.type()) {
|
||||
dispatchEvent(new CustomEvent('mailbox.inbox-unread-count', {detail:unread}));
|
||||
fireEvent('mailbox.inbox-unread-count', unread);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -361,16 +442,9 @@ export class FolderModel extends AbstractModel {
|
|||
* @returns {string}
|
||||
*/
|
||||
collapsedCss() {
|
||||
return 'e-collapsed-sign ' + (this.hasSubscribedSubfolders()
|
||||
return 'e-collapsed-sign ' + (this.hasVisibleSubfolders()
|
||||
? (this.collapsed() ? 'icon-right-mini' : 'icon-down-mini')
|
||||
: 'icon-none'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
printableFullName() {
|
||||
return this.fullName.replace(this.delimiter, ' / ');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import ko from 'ko';
|
||||
import { koComputable } from 'External/ko';
|
||||
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
|
|
@ -21,10 +21,10 @@ export class IdentityModel extends AbstractModel {
|
|||
signature: '',
|
||||
signatureInsertBefore: false,
|
||||
|
||||
deleteAccess: false
|
||||
askDelete: false
|
||||
});
|
||||
|
||||
this.canBeDeleted = ko.computed(() => !!this.id());
|
||||
this.canBeDeleted = koComputable(() => !!this.id());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -34,6 +34,6 @@ export class IdentityModel extends AbstractModel {
|
|||
const name = this.name(),
|
||||
email = this.email();
|
||||
|
||||
return name ? name + ' (' + email + ')' : email;
|
||||
return name ? name + ' <' + email + '>' : email;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ import ko from 'ko';
|
|||
import { MessagePriority } from 'Common/EnumsUser';
|
||||
import { i18n } from 'Common/Translator';
|
||||
|
||||
import { encodeHtml } from 'Common/Html';
|
||||
import { isArray, arrayLength } from 'Common/Utils';
|
||||
|
||||
import { doc } from 'Common/Globals';
|
||||
import { encodeHtml, plainToHtml, cleanHtml } from 'Common/Html';
|
||||
import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils';
|
||||
import { serverRequestRaw } from 'Common/Links';
|
||||
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
import { FileInfo } from 'Common/File';
|
||||
import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
|
||||
|
|
@ -18,13 +19,17 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
|||
import PreviewHTML from 'Html/PreviewMessage.html';
|
||||
|
||||
const
|
||||
SignedVerifyStatus = {
|
||||
UnknownPublicKeys: -4,
|
||||
UnknownPrivateKey: -3,
|
||||
Unverified: -2,
|
||||
Error: -1,
|
||||
None: 0,
|
||||
Success: 1
|
||||
// eslint-disable-next-line max-len
|
||||
url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
|
||||
// eslint-disable-next-line max-len
|
||||
email = /(^|[\s\n]|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi,
|
||||
|
||||
hcont = Element.fromHTML('<div area="hidden" style="position:absolute;left:-5000px"></div>'),
|
||||
getRealHeight = el => {
|
||||
hcont.innerHTML = el.outerHTML;
|
||||
const result = hcont.clientHeight;
|
||||
hcont.innerHTML = '';
|
||||
return result;
|
||||
},
|
||||
|
||||
replyHelper = (emails, unic, localEmails) => {
|
||||
|
|
@ -36,6 +41,8 @@ const
|
|||
});
|
||||
};
|
||||
|
||||
doc.body.append(hcont);
|
||||
|
||||
export class MessageModel extends AbstractModel {
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -44,6 +51,8 @@ export class MessageModel extends AbstractModel {
|
|||
|
||||
this.addObservables({
|
||||
subject: '',
|
||||
plain: '',
|
||||
html: '',
|
||||
size: 0,
|
||||
spamScore: 0,
|
||||
spamResult: '',
|
||||
|
|
@ -56,25 +65,20 @@ export class MessageModel extends AbstractModel {
|
|||
senderClearEmailsString: '',
|
||||
|
||||
deleted: false,
|
||||
isDeleted: false,
|
||||
isUnseen: false,
|
||||
isFlagged: false,
|
||||
isAnswered: false,
|
||||
isForwarded: false,
|
||||
isReadReceipt: false,
|
||||
|
||||
focused: false,
|
||||
selected: false,
|
||||
checked: false,
|
||||
hasAttachments: false,
|
||||
|
||||
isHtml: false,
|
||||
hasImages: false,
|
||||
hasExternals: false,
|
||||
|
||||
isPgpSigned: false,
|
||||
isPgpEncrypted: false,
|
||||
pgpSignedVerifyStatus: SignedVerifyStatus.None,
|
||||
pgpSignedVerifyUser: '',
|
||||
pgpSigned: null,
|
||||
pgpVerified: null,
|
||||
|
||||
pgpEncrypted: null,
|
||||
pgpDecrypted: false,
|
||||
|
||||
readReceipt: '',
|
||||
|
||||
|
|
@ -83,14 +87,24 @@ export class MessageModel extends AbstractModel {
|
|||
});
|
||||
|
||||
this.attachments = ko.observableArray(new AttachmentCollectionModel);
|
||||
this.attachmentsSpecData = ko.observableArray();
|
||||
this.threads = ko.observableArray();
|
||||
this.unsubsribeLinks = ko.observableArray();
|
||||
this.flags = ko.observableArray();
|
||||
|
||||
this.addComputables({
|
||||
attachmentIconClass: () => FileInfo.getCombinedIconClass(this.hasAttachments() ? this.attachmentsSpecData() : []),
|
||||
attachmentIconClass: () => FileInfo.getAttachmentsIconClass(this.attachments()),
|
||||
threadsLen: () => this.threads().length,
|
||||
isImportant: () => MessagePriority.High === this.priority(),
|
||||
hasAttachments: () => this.attachments().hasVisible(),
|
||||
|
||||
isDeleted: () => this.flags().includes('\\deleted'),
|
||||
isUnseen: () => !this.flags().includes('\\seen') /* || this.flags().includes('\\unseen')*/,
|
||||
isFlagged: () => this.flags().includes('\\flagged'),
|
||||
isAnswered: () => this.flags().includes('\\answered'),
|
||||
isForwarded: () => this.flags().includes('$forwarded'),
|
||||
isReadReceipt: () => this.flags().includes('$mdnsent')
|
||||
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
|
||||
// isPhishing: () => this.flags().includes('$phishing')
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +113,6 @@ export class MessageModel extends AbstractModel {
|
|||
this.uid = 0;
|
||||
this.hash = '';
|
||||
this.requestHash = '';
|
||||
this.externalProxy = false;
|
||||
this.emails = [];
|
||||
this.from = new EmailCollectionModel;
|
||||
this.to = new EmailCollectionModel;
|
||||
|
|
@ -117,6 +130,8 @@ export class MessageModel extends AbstractModel {
|
|||
clear() {
|
||||
this._reset();
|
||||
this.subject('');
|
||||
this.html('');
|
||||
this.plain('');
|
||||
this.size(0);
|
||||
this.spamScore(0);
|
||||
this.spamResult('');
|
||||
|
|
@ -129,26 +144,20 @@ export class MessageModel extends AbstractModel {
|
|||
this.senderClearEmailsString('');
|
||||
|
||||
this.deleted(false);
|
||||
this.isDeleted(false);
|
||||
this.isUnseen(false);
|
||||
this.isFlagged(false);
|
||||
this.isAnswered(false);
|
||||
this.isForwarded(false);
|
||||
this.isReadReceipt(false);
|
||||
|
||||
this.selected(false);
|
||||
this.checked(false);
|
||||
this.hasAttachments(false);
|
||||
this.attachmentsSpecData([]);
|
||||
|
||||
this.isHtml(false);
|
||||
this.hasImages(false);
|
||||
this.hasExternals(false);
|
||||
this.attachments(new AttachmentCollectionModel);
|
||||
|
||||
this.isPgpSigned(false);
|
||||
this.isPgpEncrypted(false);
|
||||
this.pgpSignedVerifyStatus(SignedVerifyStatus.None);
|
||||
this.pgpSignedVerifyUser('');
|
||||
this.pgpSigned(null);
|
||||
this.pgpVerified(null);
|
||||
|
||||
this.pgpEncrypted(null);
|
||||
this.pgpDecrypted(false);
|
||||
|
||||
this.priority(MessagePriority.Normal);
|
||||
this.readReceipt('');
|
||||
|
|
@ -160,6 +169,11 @@ export class MessageModel extends AbstractModel {
|
|||
this.hasFlaggedSubMessage(false);
|
||||
}
|
||||
|
||||
spamStatus() {
|
||||
let spam = this.spamResult();
|
||||
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} properties
|
||||
* @returns {Array}
|
||||
|
|
@ -178,7 +192,7 @@ export class MessageModel extends AbstractModel {
|
|||
}
|
||||
|
||||
computeSenderEmail() {
|
||||
const list = [FolderUserStore.sentFolder(), FolderUserStore.draftFolder()].includes(this.folder) ? 'to' : 'from';
|
||||
const list = [FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from';
|
||||
this.senderEmailsString(this[list].toString(true));
|
||||
this.senderClearEmailsString(this[list].toStringClear());
|
||||
}
|
||||
|
|
@ -192,8 +206,8 @@ export class MessageModel extends AbstractModel {
|
|||
json.Priority = MessagePriority.Normal;
|
||||
}
|
||||
if (super.revivePropertiesFromJson(json)) {
|
||||
// this.foundedCIDs = isArray(json.FoundedCIDs) ? json.FoundedCIDs : [];
|
||||
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundedCIDs));
|
||||
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
|
||||
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundCIDs));
|
||||
|
||||
this.computeSenderEmail();
|
||||
}
|
||||
|
|
@ -275,7 +289,7 @@ export class MessageModel extends AbstractModel {
|
|||
*/
|
||||
lineAsCss() {
|
||||
let classes = [];
|
||||
Object.entries({
|
||||
forEachObjectEntry({
|
||||
deleted: this.deleted(),
|
||||
'deleted-mark': this.isDeleted(),
|
||||
selected: this.selected(),
|
||||
|
|
@ -286,12 +300,12 @@ export class MessageModel extends AbstractModel {
|
|||
forwarded: this.isForwarded(),
|
||||
focused: this.focused(),
|
||||
important: this.isImportant(),
|
||||
withAttachments: this.hasAttachments(),
|
||||
withAttachments: !!this.attachments().length,
|
||||
emptySubject: !this.subject(),
|
||||
// hasChildrenMessage: 1 < this.threadsLen(),
|
||||
hasUnseenSubMessage: this.hasUnseenSubMessage(),
|
||||
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
|
||||
}).forEach(([key, value]) => value && classes.push(key));
|
||||
}, (key, value) => value && classes.push(key));
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
|
|
@ -364,35 +378,138 @@ export class MessageModel extends AbstractModel {
|
|||
return [toResult, ccResult];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean=} print = false
|
||||
*/
|
||||
viewHtml() {
|
||||
const body = this.body;
|
||||
if (body && this.html()) {
|
||||
const contentLocationUrls = {},
|
||||
oAttachments = this.attachments();
|
||||
|
||||
// Get contentLocationUrls
|
||||
oAttachments.forEach(oAttachment => {
|
||||
if (oAttachment.cid && oAttachment.contentLocation) {
|
||||
contentLocationUrls[oAttachment.contentId()] = oAttachment.contentLocation;
|
||||
}
|
||||
});
|
||||
|
||||
let result = cleanHtml(this.html(), contentLocationUrls, SettingsUserStore.removeColors());
|
||||
this.hasExternals(result.hasExternals);
|
||||
// this.hasInternals = result.foundCIDs.length || result.foundContentLocationUrls.length;
|
||||
this.hasImages(body.rlHasImages = !!result.hasExternals);
|
||||
|
||||
// Hide valid inline attachments in message view 'attachments' section
|
||||
oAttachments.forEach(oAttachment => {
|
||||
let cid = oAttachment.contentId(),
|
||||
found = result.foundCIDs.includes(cid);
|
||||
oAttachment.isInline(found);
|
||||
oAttachment.isLinked(found || result.foundContentLocationUrls.includes(oAttachment.contentLocation));
|
||||
});
|
||||
|
||||
body.innerHTML = result.html;
|
||||
|
||||
body.classList.toggle('html', 1);
|
||||
body.classList.toggle('plain', 0);
|
||||
|
||||
// showInternalImages
|
||||
const findAttachmentByCid = cid => this.attachments().findByCid(cid);
|
||||
body.querySelectorAll('[data-x-src-cid],[data-x-src-location],[data-x-style-cid]').forEach(el => {
|
||||
const data = el.dataset;
|
||||
if (data.xSrcCid) {
|
||||
const attachment = findAttachmentByCid(data.xSrcCid);
|
||||
if (attachment && attachment.download) {
|
||||
el.src = attachment.linkPreview();
|
||||
}
|
||||
} else if (data.xSrcLocation) {
|
||||
const attachment = this.attachments.find(item => data.xSrcLocation === item.contentLocation)
|
||||
|| findAttachmentByCid(data.xSrcLocation);
|
||||
if (attachment && attachment.download) {
|
||||
el.loading = 'lazy';
|
||||
el.src = attachment.linkPreview();
|
||||
}
|
||||
} else if (data.xStyleCid) {
|
||||
forEachObjectEntry(JSON.parse(data.xStyleCid), (name, cid) => {
|
||||
const attachment = findAttachmentByCid(cid);
|
||||
if (attachment && attachment.linkPreview && name) {
|
||||
el.style[name] = "url('" + attachment.linkPreview() + "')";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (SettingsUserStore.showImages()) {
|
||||
this.showExternalImages();
|
||||
}
|
||||
|
||||
this.isHtml(true);
|
||||
this.initView();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
viewPlain() {
|
||||
const body = this.body;
|
||||
if (body && this.plain()) {
|
||||
body.classList.toggle('html', 0);
|
||||
body.classList.toggle('plain', 1);
|
||||
body.innerHTML = plainToHtml(
|
||||
this.plain()
|
||||
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[a-z][^\r\n]+)+|SIGNATURE-----[\s\S]*)/, '')
|
||||
.trim()
|
||||
)
|
||||
.replace(url, '$1<a href="$2" target="_blank">$2</a>')
|
||||
.replace(email, '$1<a href="mailto:$2">$2</a>');
|
||||
this.isHtml(false);
|
||||
this.hasImages(false);
|
||||
this.initView();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
initView() {
|
||||
// init BlockquoteSwitcher
|
||||
this.body.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => {
|
||||
node.removeAttribute('style')
|
||||
if (node.textContent.trim() && !node.parentNode.closest('blockquote')) {
|
||||
let h = node.clientHeight || getRealHeight(node);
|
||||
if (0 === h || 100 < h) {
|
||||
const el = Element.fromHTML('<span class="rlBlockquoteSwitcher">•••</span>');
|
||||
node.classList.add('rl-bq-switcher','hidden-bq');
|
||||
node.before(el);
|
||||
el.addEventListener('click', () => node.classList.toggle('hidden-bq'));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
viewPopupMessage(print) {
|
||||
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
|
||||
ccLine = this.ccToLine(false),
|
||||
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
|
||||
win = open(''),
|
||||
doc = win.document;
|
||||
doc.write(PreviewHTML
|
||||
.replace(/{{subject}}/g, encodeHtml(this.subject()))
|
||||
.replace('{{date}}', encodeHtml(m ? m.format('LLL') : ''))
|
||||
.replace('{{fromCreds}}', encodeHtml(this.fromToLine(false)))
|
||||
.replace('{{toCreds}}', encodeHtml(this.toToLine(false)))
|
||||
.replace('{{toLabel}}', encodeHtml(i18n('GLOBAL/TO')))
|
||||
.replace('{{ccHide}}', ccLine ? '' : 'hidden=""')
|
||||
.replace('{{ccCreds}}', encodeHtml(ccLine))
|
||||
.replace('{{ccLabel}}', encodeHtml(i18n('GLOBAL/CC')))
|
||||
.replace('{{bodyClass}}', this.isHtml() ? 'html' : 'plain')
|
||||
.replace('{{html}}', this.bodyAsHTML())
|
||||
sdoc = win.document;
|
||||
let subject = encodeHtml(this.subject()),
|
||||
mode = this.isHtml() ? 'div' : 'pre',
|
||||
cc = ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '',
|
||||
style = getComputedStyle(doc.querySelector('.messageView')),
|
||||
prop = property => style.getPropertyValue(property);
|
||||
sdoc.write(PreviewHTML
|
||||
.replace('<title>', '<title>'+subject)
|
||||
// eslint-disable-next-line max-len
|
||||
.replace('<body>', `<body style="background-color:${prop('background-color')};color:${prop('color')}"><header><h1>${subject}</h1><time>${encodeHtml(m ? m.format('LLL') : '')}</time><div>${encodeHtml(this.fromToLine(false))}</div><div>${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.toToLine(false))}</div>${cc}</header><${mode}>${this.bodyAsHTML()}</${mode}>`)
|
||||
);
|
||||
|
||||
doc.close();
|
||||
sdoc.close();
|
||||
|
||||
if (print) {
|
||||
setTimeout(() => win.print(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean=} print = false
|
||||
*/
|
||||
popupMessage() {
|
||||
this.viewPopupMessage(false);
|
||||
}
|
||||
|
||||
printMessage() {
|
||||
this.viewPopupMessage(true);
|
||||
}
|
||||
|
|
@ -408,71 +525,61 @@ export class MessageModel extends AbstractModel {
|
|||
* @param {MessageModel} message
|
||||
* @returns {MessageModel}
|
||||
*/
|
||||
populateByMessageListItem(message) {
|
||||
if (message) {
|
||||
this.folder = message.folder;
|
||||
this.uid = message.uid;
|
||||
this.hash = message.hash;
|
||||
this.requestHash = message.requestHash;
|
||||
this.subject(message.subject());
|
||||
|
||||
this.size(message.size());
|
||||
this.spamScore(message.spamScore());
|
||||
this.spamResult(message.spamResult());
|
||||
this.isSpam(message.isSpam());
|
||||
this.hasVirus(message.hasVirus());
|
||||
this.dateTimeStampInUTC(message.dateTimeStampInUTC());
|
||||
this.priority(message.priority());
|
||||
|
||||
this.externalProxy = message.externalProxy;
|
||||
|
||||
this.emails = message.emails;
|
||||
|
||||
this.from = message.from;
|
||||
this.to = message.to;
|
||||
this.cc = message.cc;
|
||||
this.bcc = message.bcc;
|
||||
this.replyTo = message.replyTo;
|
||||
this.deliveredTo = message.deliveredTo;
|
||||
this.unsubsribeLinks(message.unsubsribeLinks);
|
||||
|
||||
this.isUnseen(message.isUnseen());
|
||||
this.isFlagged(message.isFlagged());
|
||||
this.isAnswered(message.isAnswered());
|
||||
this.isForwarded(message.isForwarded());
|
||||
this.isReadReceipt(message.isReadReceipt());
|
||||
this.isDeleted(message.isDeleted());
|
||||
|
||||
this.priority(message.priority());
|
||||
|
||||
this.selected(message.selected());
|
||||
this.checked(message.checked());
|
||||
this.hasAttachments(message.hasAttachments());
|
||||
this.attachmentsSpecData(message.attachmentsSpecData());
|
||||
}
|
||||
|
||||
this.body = null;
|
||||
|
||||
this.draftInfo = [];
|
||||
this.messageId = '';
|
||||
this.inReplyTo = '';
|
||||
this.references = '';
|
||||
static fromMessageListItem(message) {
|
||||
let self = new MessageModel();
|
||||
|
||||
if (message) {
|
||||
this.threads(message.threads());
|
||||
self.folder = message.folder;
|
||||
self.uid = message.uid;
|
||||
self.hash = message.hash;
|
||||
self.requestHash = message.requestHash;
|
||||
self.subject(message.subject());
|
||||
self.plain(message.plain());
|
||||
self.html(message.html());
|
||||
|
||||
self.size(message.size());
|
||||
self.spamScore(message.spamScore());
|
||||
self.spamResult(message.spamResult());
|
||||
self.isSpam(message.isSpam());
|
||||
self.hasVirus(message.hasVirus());
|
||||
self.dateTimeStampInUTC(message.dateTimeStampInUTC());
|
||||
self.priority(message.priority());
|
||||
|
||||
self.hasExternals(message.hasExternals());
|
||||
|
||||
self.emails = message.emails;
|
||||
|
||||
self.from = message.from;
|
||||
self.to = message.to;
|
||||
self.cc = message.cc;
|
||||
self.bcc = message.bcc;
|
||||
self.replyTo = message.replyTo;
|
||||
self.deliveredTo = message.deliveredTo;
|
||||
self.unsubsribeLinks(message.unsubsribeLinks);
|
||||
|
||||
self.flags(message.flags());
|
||||
|
||||
self.priority(message.priority());
|
||||
|
||||
self.selected(message.selected());
|
||||
self.checked(message.checked());
|
||||
self.attachments(message.attachments());
|
||||
|
||||
self.threads(message.threads());
|
||||
}
|
||||
|
||||
this.computeSenderEmail();
|
||||
self.computeSenderEmail();
|
||||
|
||||
return this;
|
||||
return self;
|
||||
}
|
||||
|
||||
showExternalImages() {
|
||||
if (this.body && this.body.rlHasImages) {
|
||||
const body = this.body;
|
||||
if (body && this.hasImages()) {
|
||||
this.hasImages(false);
|
||||
this.body.rlHasImages = false;
|
||||
body.rlHasImages = false;
|
||||
|
||||
let body = this.body, attr = this.externalProxy ? 'data-x-additional-src' : 'data-x-src';
|
||||
let attr = 'data-x-src';
|
||||
body.querySelectorAll('[' + attr + ']').forEach(node => {
|
||||
if (node.matches('img')) {
|
||||
node.loading = 'lazy';
|
||||
|
|
@ -480,74 +587,28 @@ export class MessageModel extends AbstractModel {
|
|||
node.src = node.getAttribute(attr);
|
||||
});
|
||||
|
||||
attr = this.externalProxy ? 'data-x-additional-style-url' : 'data-x-style-url';
|
||||
body.querySelectorAll('[' + attr + ']').forEach(node => {
|
||||
node.setAttribute('style', ((node.getAttribute('style')||'')
|
||||
+ ';' + node.getAttribute(attr))
|
||||
.replace(/^[;\s]+/,''));
|
||||
body.querySelectorAll('[data-x-style-url]').forEach(node => {
|
||||
forEachObjectEntry(JSON.parse(node.dataset.xStyleUrl), (name, url) => node.style[name] = "url('" + url + "')");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showInternalImages() {
|
||||
const body = this.body;
|
||||
if (body && !body.rlInitInternalImages) {
|
||||
const findAttachmentByCid = cid => this.attachments().findByCid(cid);
|
||||
|
||||
body.rlInitInternalImages = true;
|
||||
|
||||
body.querySelectorAll('[data-x-src-cid],[data-x-src-location],[data-x-style-cid]').forEach(el => {
|
||||
const data = el.dataset;
|
||||
if (data.xSrcCid) {
|
||||
const attachment = findAttachmentByCid(data.xSrcCid);
|
||||
if (attachment && attachment.download) {
|
||||
el.src = attachment.linkPreview();
|
||||
}
|
||||
} else if (data.xSrcLocation) {
|
||||
const attachment = this.attachments.find(item => data.xSrcLocation === item.contentLocation)
|
||||
|| findAttachmentByCid(data.xSrcLocation);
|
||||
if (attachment && attachment.download) {
|
||||
el.loading = 'lazy';
|
||||
el.src = attachment.linkPreview();
|
||||
}
|
||||
} else if (data.xStyleCid) {
|
||||
const name = data.xStyleCidName,
|
||||
attachment = findAttachmentByCid(data.xStyleCid);
|
||||
if (attachment && attachment.linkPreview && name) {
|
||||
el.setAttribute('style', name + ": url('" + attachment.linkPreview() + "');"
|
||||
+ (el.getAttribute('style') || ''));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetchDataFromDom() {
|
||||
if (this.body) {
|
||||
this.isHtml(!!this.body.rlIsHtml);
|
||||
this.hasImages(!!this.body.rlHasImages);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
bodyAsHTML() {
|
||||
if (this.body) {
|
||||
let clone = this.body.cloneNode(true),
|
||||
attr = 'data-html-editor-font-wrapper';
|
||||
// if (this.body && !this.body.querySelector('iframe[src*=decrypt]')) {
|
||||
if (this.body && !this.body.querySelector('iframe')) {
|
||||
let clone = this.body.cloneNode(true);
|
||||
clone.querySelectorAll('blockquote.rl-bq-switcher').forEach(
|
||||
node => node.classList.remove('rl-bq-switcher','hidden-bq')
|
||||
);
|
||||
clone.querySelectorAll('.rlBlockquoteSwitcher').forEach(
|
||||
node => node.remove()
|
||||
);
|
||||
clone.querySelectorAll('['+attr+']').forEach(
|
||||
node => node.removeAttribute(attr)
|
||||
);
|
||||
return clone.innerHTML;
|
||||
}
|
||||
return '';
|
||||
return this.html() || plainToHtml(this.plain());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -564,4 +625,5 @@ export class MessageModel extends AbstractModel {
|
|||
this.isReadReceipt()
|
||||
].join(',');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { arrayLength } from 'Common/Utils';
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
||||
|
||||
export class OpenPgpKeyModel extends AbstractModel {
|
||||
/**
|
||||
* @param {string} index
|
||||
* @param {string} guID
|
||||
* @param {string} ID
|
||||
* @param {array} IDs
|
||||
* @param {array} userIDs
|
||||
* @param {array} emails
|
||||
* @param {boolean} isPrivate
|
||||
* @param {string} armor
|
||||
* @param {string} userID
|
||||
*/
|
||||
constructor(index, guID, ID, IDs, userIDs, emails, isPrivate, armor, userID) {
|
||||
super();
|
||||
|
||||
this.index = index;
|
||||
this.id = ID;
|
||||
this.ids = arrayLength(IDs) ? IDs : [ID];
|
||||
this.guid = guID;
|
||||
this.user = '';
|
||||
this.users = userIDs;
|
||||
this.email = '';
|
||||
this.emails = emails;
|
||||
this.armor = armor;
|
||||
this.isPrivate = !!isPrivate;
|
||||
|
||||
this.selectUser(userID);
|
||||
|
||||
this.deleteAccess = ko.observable(false);
|
||||
}
|
||||
|
||||
getNativeKey() {
|
||||
let key = null;
|
||||
try {
|
||||
key = PgpUserStore.openpgp.key.readArmored(this.armor);
|
||||
if (key && !key.err && key.keys && key.keys[0]) {
|
||||
return key;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getNativeKeys() {
|
||||
const key = this.getNativeKey();
|
||||
return key && key.keys ? key.keys : null;
|
||||
}
|
||||
|
||||
select(pattern, property) {
|
||||
if (this[property]) {
|
||||
const index = this[property].indexOf(pattern);
|
||||
if (-1 !== index) {
|
||||
this.user = this.users[index];
|
||||
this.email = this.emails[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectUser(user) {
|
||||
this.select(user, 'users');
|
||||
}
|
||||
|
||||
selectEmail(email) {
|
||||
this.select(email, 'emails');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,344 +0,0 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
import { FilterModel } from 'Model/Filter';
|
||||
import { arrayLength, pString } from 'Common/Utils';
|
||||
|
||||
const SIEVE_FILE_NAME = 'rainloop.user';
|
||||
|
||||
// collectionToFileString
|
||||
function filtersToSieveScript(filters)
|
||||
{
|
||||
let eol = '\r\n',
|
||||
split = /.{0,74}/g,
|
||||
require = {},
|
||||
parts = [
|
||||
'# This is SnappyMail sieve script.',
|
||||
'# Please don\'t change anything here.',
|
||||
'# RAINLOOP:SIEVE',
|
||||
''
|
||||
];
|
||||
|
||||
const quote = string => '"' + string.trim().replace(/(\\|")/, '\\\\$1') + '"';
|
||||
const StripSpaces = string => string.replace(/\s+/, ' ').trim();
|
||||
|
||||
// conditionToSieveScript
|
||||
const conditionToString = (condition, require) =>
|
||||
{
|
||||
let result = '',
|
||||
type = condition.type(),
|
||||
field = condition.field(),
|
||||
value = condition.value().trim(),
|
||||
valueSecond = condition.valueSecond().trim();
|
||||
|
||||
if (value.length && ('Header' !== field || valueSecond.length)) {
|
||||
switch (type)
|
||||
{
|
||||
case 'NotEqualTo':
|
||||
result += 'not ';
|
||||
type = ':is';
|
||||
break;
|
||||
case 'EqualTo':
|
||||
type = ':is';
|
||||
break;
|
||||
case 'NotContains':
|
||||
result += 'not ';
|
||||
type = ':contains';
|
||||
break;
|
||||
case 'Text':
|
||||
case 'Raw':
|
||||
case 'Over':
|
||||
case 'Under':
|
||||
case 'Contains':
|
||||
type = ':' + type.toLowerCase();
|
||||
break;
|
||||
case 'Regex':
|
||||
type = ':regex';
|
||||
require.regex = 1;
|
||||
break;
|
||||
default:
|
||||
return '/* @Error: unknown type value ' + type + '*/ false';
|
||||
}
|
||||
|
||||
switch (field)
|
||||
{
|
||||
case 'From':
|
||||
result += 'header ' + type + ' ["From"]';
|
||||
break;
|
||||
case 'Recipient':
|
||||
result += 'header ' + type + ' ["To", "CC"]';
|
||||
break;
|
||||
case 'Subject':
|
||||
result += 'header ' + type + ' ["Subject"]';
|
||||
break;
|
||||
case 'Header':
|
||||
result += 'header ' + type + ' [' + quote(valueSecond) + ']';
|
||||
break;
|
||||
case 'Body':
|
||||
// :text :raw :content
|
||||
result += 'body ' + type + ' :contains';
|
||||
require.body = 1;
|
||||
break;
|
||||
case 'Size':
|
||||
result += 'size ' + type;
|
||||
break;
|
||||
default:
|
||||
return '/* @Error: unknown field value ' + field + ' */ false';
|
||||
}
|
||||
|
||||
if (('From' === field || 'Recipient' === field) && value.includes(',')) {
|
||||
result += ' [' + value.split(',').map(value => quote(value)).join(', ').trim() + ']';
|
||||
} else if ('Size' === field) {
|
||||
result += ' ' + value;
|
||||
} else {
|
||||
result += ' ' + quote(value);
|
||||
}
|
||||
|
||||
return StripSpaces(result);
|
||||
}
|
||||
|
||||
return '/* @Error: empty condition value */ false';
|
||||
};
|
||||
|
||||
// filterToSieveScript
|
||||
const filterToString = (filter, require) =>
|
||||
{
|
||||
let sTab = ' ',
|
||||
block = true,
|
||||
result = [],
|
||||
conditions = filter.conditions();
|
||||
|
||||
const errorAction = type => result.push(sTab + '# @Error (' + type + '): empty action value');
|
||||
|
||||
// Conditions
|
||||
if (1 < conditions.length) {
|
||||
result.push('Any' === filter.conditionsType()
|
||||
? 'if anyof('
|
||||
: 'if allof('
|
||||
);
|
||||
result.push(conditions.map(condition => sTab + conditionToString(condition, require)).join(',' + eol));
|
||||
result.push(')');
|
||||
} else if (conditions.length) {
|
||||
result.push('if ' + conditionToString(conditions[0], require));
|
||||
} else {
|
||||
block = false;
|
||||
}
|
||||
|
||||
// actions
|
||||
block ? result.push('{') : (sTab = '');
|
||||
|
||||
if (filter.actionMarkAsRead() && ['None','MoveTo','Forward'].includes(filter.actionType())) {
|
||||
require.imap4flags = 1;
|
||||
result.push(sTab + 'addflag "\\\\Seen";');
|
||||
}
|
||||
|
||||
let value = filter.actionValue().trim();
|
||||
value = value.length ? quote(value) : 0;
|
||||
switch (filter.actionType())
|
||||
{
|
||||
case 'None':
|
||||
break;
|
||||
case 'Discard':
|
||||
result.push(sTab + 'discard;');
|
||||
break;
|
||||
case 'Vacation':
|
||||
if (value) {
|
||||
require.vacation = 1;
|
||||
|
||||
let days = 1,
|
||||
subject = '',
|
||||
addresses = '',
|
||||
paramValue = filter.actionValueSecond().trim();
|
||||
|
||||
if (paramValue.length) {
|
||||
subject = ':subject ' + quote(StripSpaces(paramValue)) + ' ';
|
||||
}
|
||||
|
||||
paramValue = pString(filter.actionValueThird()).trim();
|
||||
if (paramValue.length) {
|
||||
days = Math.max(1, parseInt(paramValue, 10));
|
||||
}
|
||||
|
||||
paramValue = pString(filter.actionValueFourth()).trim()
|
||||
if (paramValue.length) {
|
||||
paramValue = paramValue.split(',').map(email =>
|
||||
email.trim().length ? quote(email) : ''
|
||||
).filter(email => email.length);
|
||||
if (paramValue.length) {
|
||||
addresses = ':addresses [' + paramValue.join(', ') + '] ';
|
||||
}
|
||||
}
|
||||
|
||||
result.push(sTab + 'vacation :days ' + days + ' ' + addresses + subject + value + ';');
|
||||
} else {
|
||||
errorAction('vacation');
|
||||
}
|
||||
break;
|
||||
case 'Reject': {
|
||||
if (value) {
|
||||
require.reject = 1;
|
||||
result.push(sTab + 'reject ' + value + ';');
|
||||
} else {
|
||||
errorAction('reject');
|
||||
}
|
||||
break; }
|
||||
case 'Forward':
|
||||
if (value) {
|
||||
if (filter.actionKeep()) {
|
||||
require.fileinto = 1;
|
||||
result.push(sTab + 'fileinto "INBOX";');
|
||||
}
|
||||
result.push(sTab + 'redirect ' + value + ';');
|
||||
} else {
|
||||
errorAction('redirect');
|
||||
}
|
||||
break;
|
||||
case 'MoveTo':
|
||||
if (value) {
|
||||
require.fileinto = 1;
|
||||
result.push(sTab + 'fileinto ' + value + ';');
|
||||
} else {
|
||||
errorAction('fileinto');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
filter.actionNoStop() || result.push(sTab + 'stop;');
|
||||
|
||||
block && result.push('}');
|
||||
|
||||
return result.join(eol);
|
||||
};
|
||||
|
||||
filters.forEach(filter => {
|
||||
parts.push([
|
||||
'/*',
|
||||
'BEGIN:FILTER:' + filter.id,
|
||||
'BEGIN:HEADER',
|
||||
btoa(unescape(encodeURIComponent(JSON.stringify(filter.toJson())))).match(split).join(eol) + 'END:HEADER',
|
||||
'*/',
|
||||
filter.enabled() ? '' : '/* @Filter is disabled ',
|
||||
filterToString(filter, require),
|
||||
filter.enabled() ? '' : '*/',
|
||||
'/* END:FILTER */',
|
||||
''
|
||||
].join(eol));
|
||||
});
|
||||
|
||||
require = Object.keys(require);
|
||||
return (require.length ? 'require ' + JSON.stringify(require) + ';' + eol : '') + eol + parts.join(eol);
|
||||
}
|
||||
|
||||
// fileStringToCollection
|
||||
function sieveScriptToFilters(script)
|
||||
{
|
||||
let regex = /BEGIN:HEADER([\s\S]+?)END:HEADER/gm,
|
||||
filters = [],
|
||||
json,
|
||||
filter;
|
||||
if (script.length && script.includes('RAINLOOP:SIEVE')) {
|
||||
while ((json = regex.exec(script))) {
|
||||
json = decodeURIComponent(escape(atob(json[1].replace(/\s+/g, ''))));
|
||||
if (json && json.length && (json = JSON.parse(json))) {
|
||||
json['@Object'] = 'Object/Filter';
|
||||
json.Conditions.forEach(condition => condition['@Object'] = 'Object/FilterCondition');
|
||||
filter = FilterModel.reviveFromJson(json);
|
||||
filter && filters.push(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
// TODO: branch sieveparser
|
||||
// https://github.com/the-djmaze/snappymail/tree/sieveparser/dev/Sieve
|
||||
else if (script.length && window.Sieve) {
|
||||
let tree = window.Sieve.parseScript(script);
|
||||
}
|
||||
*/
|
||||
return filters;
|
||||
}
|
||||
|
||||
export class SieveScriptModel extends AbstractModel
|
||||
{
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.addObservables({
|
||||
name: '',
|
||||
active: false,
|
||||
body: '',
|
||||
|
||||
exists: false,
|
||||
nameError: false,
|
||||
bodyError: false,
|
||||
deleteAccess: false,
|
||||
canBeDeleted: true,
|
||||
hasChanges: false
|
||||
});
|
||||
|
||||
this.filters = ko.observableArray();
|
||||
// this.saving = ko.observable(false).extend({ debounce: 200 });
|
||||
|
||||
this.addSubscribables({
|
||||
name: () => this.hasChanges(true),
|
||||
filters: () => this.hasChanges(true),
|
||||
body: () => this.hasChanges(true)
|
||||
});
|
||||
}
|
||||
|
||||
filtersToRaw() {
|
||||
return filtersToSieveScript(this.filters);
|
||||
// this.body(filtersToSieveScript(this.filters));
|
||||
}
|
||||
|
||||
rawToFilters() {
|
||||
return sieveScriptToFilters(this.body());
|
||||
// this.filters(sieveScriptToFilters(this.body()));
|
||||
}
|
||||
|
||||
verify() {
|
||||
this.nameError(!this.name().trim());
|
||||
this.bodyError(this.allowFilters() ? !this.filters.length : !this.body().trim());
|
||||
return !this.nameError() && !this.bodyError();
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
name: this.name(),
|
||||
active: this.active() ? 1 : 0,
|
||||
body: this.body(),
|
||||
filters: this.filters.map(item => item.toJson())
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Only 'rainloop.user' script supports filters
|
||||
*/
|
||||
allowFilters() {
|
||||
return SIEVE_FILE_NAME === this.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @param {FetchJsonScript} json
|
||||
* @returns {?SieveScriptModel}
|
||||
*/
|
||||
static reviveFromJson(json) {
|
||||
const script = super.reviveFromJson(json);
|
||||
if (script) {
|
||||
if (script.allowFilters()) {
|
||||
script.filters(
|
||||
arrayLength(json.filters)
|
||||
? json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v)
|
||||
: sieveScriptToFilters(script.body())
|
||||
);
|
||||
} else {
|
||||
script.filters([]);
|
||||
}
|
||||
script.canBeDeleted(SIEVE_FILE_NAME !== json.name);
|
||||
script.exists(true);
|
||||
script.hasChanges(false);
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue