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:
djmaze 2022-03-15 15:00:14 +01:00
commit f7f56b3789
794 changed files with 96444 additions and 74188 deletions

View file

@ -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)) {

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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());
}
}

View file

@ -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
);

View file

@ -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 };

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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, ' / ');
}
}

View file

@ -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;
}
}

View file

@ -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(',');
}
}

View file

@ -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');
}
}

View file

@ -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;
}
}