Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
Christoph Wurst 2018-08-17 10:38:40 +02:00
Родитель 1ecf67c6af
Коммит 0ff788a14d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: CC42AC2A7F0E56D8
148 изменённых файлов: 8809 добавлений и 14469 удалений

3
.browserslistrc Normal file
Просмотреть файл

@ -0,0 +1,3 @@
> 1%
last 2 versions
not ie <= 8

17
.eslintrc.js Normal file
Просмотреть файл

@ -0,0 +1,17 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'eslint:recommended'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: 'babel-eslint'
}
}

5
babel.config.js Normal file
Просмотреть файл

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

30
js/App.vue Normal file
Просмотреть файл

@ -0,0 +1,30 @@
<template>
<div id="content" class="app-mail">
<div id="app-navigation" class="icon-loading">
<div id="mail-new-message-fixed"
class="app-navigation-new">
</div>
<ul id="usergrouplist"></ul>
<div id="app-settings">
<div id="app-settings-header">
<button class="settings-button"
data-apps-slide-toggle="#app-settings-content"><?php p($l->
t('Settings'));?>
</button>
</div>
<div id="app-settings-content"></div>
</div>
</div>
<div id="app-content">
<div id="app-content-wrapper"></div>
</div>
</div>
</template>
<script>
export default {}
</script>
<style>
</style>

153
js/app.js
Просмотреть файл

@ -1,153 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function(require) {
'use strict';
// Enable ES6 promise polyfill
require('es6-promise').polyfill();
var $ = require('jquery');
var Backbone = require('backbone');
var Marionette = require('backbone.marionette');
var OC = require('OC');
var AppView = require('views/appview');
var Search = require('search');
var Radio = require('radio');
var Router = require('router');
var AccountController = require('controller/accountcontroller');
var RouteController = require('routecontroller');
// Load controllers/services
require('controller/foldercontroller');
require('controller/messagecontroller');
require('service/accountservice');
require('service/avatarservice');
require('service/aliasesservice');
require('service/attachmentservice');
require('service/backgroundsyncservice');
require('service/davservice');
require('service/folderservice');
require('service/foldersyncservice');
require('service/messageservice');
require('service/preferenceservice');
require('util/notificationhandler');
var Mail = Marionette.Application.extend({
_useExternalAvatars: false,
getUseExternalAvatars: function() {
return this._useExternalAvatars;
},
/**
* Register the mailto protocol handler
*/
registerProtocolHandler: function() {
if (window.navigator.registerProtocolHandler) {
var url = window.location.protocol + '//' +
window.location.host +
OC.generateUrl('apps/mail/compose?uri=%s');
try {
window.navigator
.registerProtocolHandler('mailto', url, OC.theme.name + ' Mail');
} catch (e) {
}
}
},
requestNotificationPermissions: function() {
Radio.ui.trigger('notification:request');
},
/**
* Register the actual search module in the search proxy
*/
setUpSearch: function() {
new OCA.Search(Search.filter, function() {
Search.filter('');
});
},
/**
* Start syncing accounts in the background
*
* @param {AccountCollection} accounts
*/
startBackgroundSync: function(accounts) {
Radio.sync.trigger('start', accounts);
}
});
Mail = new Mail();
Mail.on('start', function() {
this._useExternalAvatars = $('#external-avatars').val() === 'true';
this.view = new AppView();
Radio.ui.trigger('content:loading', t('mail', 'Loading accounts'));
this.registerProtocolHandler();
this.requestNotificationPermissions();
this.setUpSearch();
var _this = this;
AccountController.loadAccounts().then(function(accounts) {
_this.router = new Router({
controller: new RouteController(accounts)
});
Backbone.history.start();
_this.startBackgroundSync(accounts);
});
/**
* Detects pasted text by browser plugins, and other software.
* Check for changes in message bodies every second.
*/
setInterval((function() {
// Begin the loop.
return function() {
// Define which elements hold the message body.
var MessageBody = $('.message-body');
/**
* If the message body is displayed and has content:
* Prepare the message body content for processing.
* If there is new message body content to process:
* Resize the text area.
* Toggle the send button, based on whether the message is ready or not.
* Prepare the new message body content for future processing.
*/
if (MessageBody.val()) {
var OldMessageBody = MessageBody.val();
var NewMessageBody = MessageBody.val();
if (NewMessageBody !== OldMessageBody) {
MessageBody.trigger('autosize.resize');
OldMessageBody = NewMessageBody;
}
}
};
})(), 1000);
});
return Mail;
});

Просмотреть файл

@ -1,57 +0,0 @@
/* global Promise */
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2016, 2017
*/
define(function(require) {
'use strict';
var CrashReport = require('crashreport');
var FolderController = require('controller/foldercontroller');
var Radio = require('radio');
/**
* Load all accounts
*
* @returns {Promise}
*/
function loadAccounts() {
// Do not show sidebar content until everything has been loaded
Radio.ui.trigger('sidebar:loading');
return Radio.account.request('entities').then(function(accounts) {
if (accounts.length === 0) {
Radio.navigation.trigger('setup');
Radio.ui.trigger('sidebar:accounts');
return Promise.resolve(accounts);
}
return Promise.all(accounts.map(function(account) {
return FolderController.loadAccountFolders(account);
})).then(function() {
return accounts;
});
}).then(function(accounts) {
// Show accounts regardless of the result of
// loading the folders
Radio.ui.trigger('sidebar:accounts');
return accounts;
}, function(e) {
console.error(e);
CrashReport.report(e);
Radio.ui.trigger('error:show', t('mail', 'Error while loading the accounts.'));
});
}
return {
loadAccounts: loadAccounts
};
});

Просмотреть файл

@ -1,250 +0,0 @@
/* global Promise */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function(require) {
'use strict';
var $ = require('jquery');
var _ = require('underscore');
var CrashReport = require('crashreport');
var Radio = require('radio');
var ErrorMessageFactory = require('util/errormessagefactory');
Radio.folder.reply('message:delete', deleteMessage);
/**
* @param {Account} account
* @returns {Promise}
*/
function loadFolders(account) {
return Radio.folder.request('entities', account)
.catch(CrashReport.report);
}
/**
* @param {Account} account
* @param {Folder} folder
* @param {string} searchQuery
* @param {bool} openFirstMessage
* @returns {Promise}
*/
function loadFolderMessages(account, folder, searchQuery, openFirstMessage) {
openFirstMessage = openFirstMessage !== false;
// Set folder active
Radio.folder.trigger('setactive', account, folder);
if (folder.get('noSelect')) {
Radio.ui.trigger('content:error', t('mail', 'Can not load this folder.'));
require('state').currentAccount = account;
require('state').currentFolder = folder;
Radio.ui.trigger('messagesview:message:setactive', null);
require('state').currentlyLoading = null;
return Promise.resolve();
} else {
return Radio.message.request('entities', account, folder, {
cache: true,
filter: searchQuery,
replace: true
}).then(function(messages) {
Radio.ui.trigger('foldercontent:show', account, folder, {
searchQuery: searchQuery
});
require('state').currentlyLoading = null;
require('state').currentAccount = account;
require('state').currentFolder = folder;
Radio.ui.trigger('messagesview:message:setactive', null);
// Fade out the message composer
$('#mail_new_message').prop('disabled', false);
if (messages.length > 0) {
if (openFirstMessage) {
var message = messages.first();
Radio.message.trigger('load', message.folder.account, message.folder, message);
}
}
}, function(error) {
console.error('error while loading messages: ', error);
var icon;
if (folder.get('specialRole')) {
icon = 'icon-' + folder.get('specialRole');
}
Radio.ui.trigger('content:error', ErrorMessageFactory.getRandomFolderErrorMessage(folder), icon);
// Set the old folder as being active
var oldFolder = require('state').currentFolder;
Radio.folder.trigger('setactive', account, oldFolder);
}).catch(CrashReport.report);
}
}
var loadFolderMessagesDebounced = _.debounce(loadFolderMessages, 1000);
/**
* @param {Account} account
* @param {Folder} folder
* @param {bool} loadFirstMessage
* @returns {Promise}
*/
function showFolder(account, folder, loadFirstMessage) {
loadFirstMessage = loadFirstMessage !== false;
Radio.ui.trigger('search:set', '');
Radio.ui.trigger('content:loading', folder.get('name'));
return new Promise(function(resolve, reject) {
_.defer(function() {
var loading = loadFolderMessages(account, folder, undefined, loadFirstMessage);
// Save current folder
Radio.folder.trigger('setactive', account, folder);
require('state').currentAccount = account;
require('state').currentFolder = folder;
loading.then(resolve).catch(reject);
});
});
}
/**
* @param {Account} account
* @param {Folder} folder
* @param {string} query
* @returns {Promise}
*/
function searchFolder(account, folder, query) {
// If this was triggered by a URL change, we set the search input manually
Radio.ui.trigger('search:set', query);
Radio.ui.trigger('content:loading', t('mail', 'Searching for {query}', {
query: query
}));
_.defer(function() {
loadFolderMessagesDebounced(account, folder, query);
});
}
/**
* @param {Folder} folder
* @param {Folder} currentFolder
* @returns {Array} array of two folders, the first one is the individual
*/
function getSpecificAndUnifiedFolder(folder, currentFolder) {
// Case 1: we're currently in a unified folder
if (currentFolder.account.get('accountId') === -1) {
return [folder, currentFolder];
}
// Locate unified folder if existent
var unifiedAccount = require('state').accounts.get(-1);
var unifiedFolder = unifiedAccount ? unifiedAccount.folders.first() : null;
// Case 2: we're in a specific folder and a unified one is available too
if (currentFolder.get('specialRole') === 'inbox' && unifiedFolder) {
return [folder, unifiedFolder];
}
// Case 3: we're in a specific folder, but there's no unified one
return [folder, null];
}
/**
* Call supplied function with folder as first parameter, if
* the folder is not undefined
*
* @param {Array<Folder>} folders
* @param {Function} fn
* @returns {mixed}
*/
function applyOnFolders(folders, fn) {
folders.forEach(function(folder) {
if (!folder) {
// ignore
return;
}
return fn(folder);
});
}
/**
* @param {Message} message
* @param {Folder} currentFolder
* @returns {Promise}
*/
function deleteMessage(message, currentFolder) {
var folders = getSpecificAndUnifiedFolder(message.folder, currentFolder);
applyOnFolders(folders, function(folder) {
// Update total counter and prevent negative values
folder.set('total', Math.max(0, folder.get('total')-1));
if(message.get('flags').get('unseen') === true) {
folder.set('unseen', Math.max(0, folder.get('unseen')-1));
}
});
var searchCollection = currentFolder.messages;
var index = searchCollection.indexOf(message);
// Select previous or first
if (index === 0) {
index = 1;
} else {
index = index - 1;
}
var nextMessage = searchCollection.at(index);
// Remove message
applyOnFolders(folders, function(folder) {
folder.messages.remove(message);
});
if (require('state').currentMessage && require('state').currentMessage.get('id') === message.id) {
if (nextMessage) {
Radio.message.trigger('load', nextMessage.folder.account, nextMessage.folder, nextMessage);
} else {
Radio.ui.trigger('message:empty');
}
}
return Radio.message.request('delete', message)
.catch(function(err) {
console.error(err);
Radio.ui.trigger('error:show', t('mail', 'Error while deleting message.'));
applyOnFolders(folders, function(folder) {
// Restore counter
folder.set('total', folder.previousAttributes.total);
// Add the message to the collection again
folder.addMessage(message);
});
});
}
return {
loadAccountFolders: loadFolders,
showFolder: showFolder,
searchFolder: searchFolder
};
});

Просмотреть файл

@ -1,209 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function(require) {
'use strict';
var $ = require('jquery');
var _ = require('underscore');
var OC = require('OC');
var Radio = require('radio');
var ErrorMessageFactory = require('util/errormessagefactory');
Radio.message.on('load', load);
Radio.message.on('forward', openForwardComposer);
Radio.message.on('flag', flagMessage);
Radio.message.on('move', moveMessage);
/**
* @param {Account} account
* @param {Folder} folder
* @param {Message} message
* @param {object} options
*/
function load(account, folder, message, options) {
options = options || {};
var defaultOptions = {
force: false
};
_.defaults(options, defaultOptions);
// Do not reload email when clicking same again
if (require('state').currentMessage && require('state').currentMessage.get('id') === message.get('id')) {
return;
}
// TODO: expression is useless?
if (!options.force && false) {
return;
}
// check if message is a draft
var draft = require('state').currentFolder.get('specialRole') === 'drafts';
// close email first
// Check if message is open
if (require('state').currentMessage !== null) {
var lastMessage = require('state').currentMessage;
Radio.ui.trigger('messagesview:message:setactive', null);
if (lastMessage.get('id') === message.get('id')) {
return;
}
}
Radio.ui.trigger('message:loading');
// Set current Message as active
Radio.ui.trigger('messagesview:message:setactive', message);
require('state').currentMessageBody = '';
// Fade out the message composer
$('#mail_new_message').prop('disabled', false);
Radio.message.request('entity', account, folder, message.get('id')).then(function(messageBody) {
if (draft) {
Radio.ui.trigger('composer:show', messageBody, true);
} else {
Radio.ui.trigger('message:show', message, messageBody);
}
}, function() {
Radio.ui.trigger('message:error', ErrorMessageFactory.getRandomMessageErrorMessage());
});
}
function openForwardComposer() {
var header = '\n\n\n\n-------- ' +
t('mail', 'Forwarded message') +
' --------\n';
// TODO: find a better way to get the current message body
var data = {
subject: 'Fwd: ' + require('state').currentMessageSubject,
body: header + require('state').currentMessageBody.replace(/<br \/>/g, '\n')
};
if (require('state').currentAccount.get('accountId') !== -1) {
data.accountId = require('state').currentAccount.get('accountId');
}
Radio.ui.trigger('composer:show', data);
}
/**
* @param {Message} message
* @param {number} attachmentId
* @param {function} callback
* @returns {Promise}
*/
function saveAttachmentToFiles(message, attachmentId, callback) {
var saveAll = _.isUndefined(attachmentId);
return new Promise(function(resolve, reject) {
OC.dialogs.filepicker(
t('mail', 'Choose a folder to store the attachment in'),
function(path) {
if (typeof callback === 'function') {
callback();
}
Radio.message.request('save:cloud', message, attachmentId, path).then(function() {
if (saveAll) {
Radio.ui.trigger('error:show', t('mail', 'Attachments saved to Files.'));
} else {
Radio.ui.trigger('error:show', t('mail', 'Attachment saved to Files.'));
}
resolve();
}, function() {
if (saveAll) {
Radio.ui.trigger('error:show', t('mail', 'Error while saving attachments to Files.'));
} else {
Radio.ui.trigger('error:show', t('mail', 'Error while saving attachment to Files.'));
}
reject();
});
}, false, 'httpd/unix-directory', true);
});
}
function flagMessage(message, flag, value) {
var folder = message.folder;
var account = folder.account;
var prevUnseen = folder.get('unseen');
if (message.get('flags').get(flag) === value) {
// Nothing to do
return;
}
message.get('flags').set(flag, value);
// Update folder counter
if (flag === 'unseen') {
var unseen = Math.max(0, prevUnseen + (value ? 1 : -1));
folder.set('unseen', unseen);
}
// Update the folder to reflect the new unread count
Radio.ui.trigger('title:update');
Radio.message.request('flag', account, folder, message, flag, value).
catch(function() {
Radio.ui.trigger('error:show', t('mail', 'Message flag could not be set.'));
// Restore previous state
message.get('flags').set(flag, !value);
folder.set('unseen', prevUnseen);
Radio.ui.trigger('title:update');
});
}
function moveMessage(sourceAccount, sourceFolder, message, destAccount,
destFolder) {
if (sourceAccount.get('accountId') === destAccount.get('accountId')
&& sourceFolder.get('id') === destFolder.get('id')) {
// Nothing to move
return;
}
sourceFolder.messages.remove(message);
destFolder.addMessage(message);
Radio.message.request('move', sourceAccount, sourceFolder, message, destAccount, destFolder).
then(function() {
// TODO: update counters
}, function() {
Radio.ui.trigger('error:show', t('mail', 'Could not move message.'));
sourceFolder.addMessage(message);
});
}
/**
* @param {Account} account
* @param {Folder} folder
* @param {number} messageId
* @param {function} callback
* @returns {Promise}
*/
function saveAttachmentsToFiles(message, callback) {
return saveAttachmentToFiles(message, null, callback);
}
return {
saveAttachmentToFiles: saveAttachmentToFiles,
saveAttachmentsToFiles: saveAttachmentsToFiles
};
});

Просмотреть файл

@ -1,37 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function(require) {
var $ = require('jquery');
var OC = require('OC');
var App = require('app');
$(function() {
// Conigure CSRF token
$.ajaxSetup({
headers: {
requesttoken: OC.requestToken
}
});
// Start app when the page is ready
console.log('Starting Mail …');
App.start();
});
});

12
js/main.js Normal file
Просмотреть файл

@ -0,0 +1,12 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#content')

Просмотреть файл

@ -1,105 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2015, 2017
*/
define(function(require) {
'use strict';
var _ = require('underscore');
var Backbone = require('backbone');
var FolderCollection = require('models/foldercollection');
var AliasesCollection = require('models/aliasescollection');
var OC = require('OC');
/**
* @class Account
*/
var Account = Backbone.Model.extend({
defaults: {
aliases: [],
specialFolders: [],
isUnified: false
},
idAttribute: 'accountId',
url: function() {
return OC.generateUrl('apps/mail/api/accounts');
},
initialize: function() {
this.folders = new FolderCollection();
this.set('aliases', new AliasesCollection(this.get('aliases')));
},
_getFolderByIdRecursively: function(folder, folderId) {
if (!folder) {
return null;
}
if (folder.get('id') === folderId) {
return folder;
}
var subFolders = folder.folders;
if (!subFolders) {
return null;
}
for (var i = 0; i < subFolders.length; i++) {
var subFolder = this._getFolderByIdRecursively(subFolders.at(i), folderId);
if (subFolder) {
return subFolder;
}
}
return null;
},
/**
* @param {Folder} folder
*/
addFolder: function(folder) {
folder.account = this;
this.folders.add(folder);
},
getFolderById: function(folderId) {
if (!this.folders) {
return undefined;
}
for (var i = 0; i < this.folders.length; i++) {
var result = this._getFolderByIdRecursively(this.folders.at(i), folderId);
if (result) {
return result;
}
}
return undefined;
},
getSpecialFolder: function() {
if (!this.folders) {
return undefined;
}
return _.find(this.folders, function(folder) {
// TODO: handle special folders in subfolder properly
if (folder.get('specialRole') === 'draft') {
return true;
}
});
},
toJSON: function() {
var data = Backbone.Model.prototype.toJSON.call(this);
if (data.folders && data.folders.toJSON) {
data.folders = data.folders.toJSON();
}
if (data.aliases && data.aliases.toJSON) {
data.aliases = data.aliases.toJSON();
}
if (!data.id) {
data.id = this.cid;
}
return data;
}
});
return Account;
});

Просмотреть файл

@ -1,32 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2015, 2016
*/
define(function(require) {
'use strict';
var Backbone = require('backbone');
var Account = require('models/account');
var OC = require('OC');
/**
* @class AccountCollection
*/
var AccountCollection = Backbone.Collection.extend({
model: Account,
url: function() {
return OC.generateUrl('apps/mail/api/accounts');
},
comparator: function(account) {
return account.get('accountId');
}
});
return AccountCollection;
});

Просмотреть файл

@ -1,38 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Tahaa Karim <tahaalibra@gmail.com>
* @copyright Tahaa Karim 2016
*/
define(function(require) {
'use strict';
var Backbone = require('backbone');
/**
* @class Alias
*/
var Alias = Backbone.Model.extend({
defaults: {
},
initialize: function() {
},
toJSON: function() {
var data = Backbone.Model.prototype.toJSON.call(this);
if (data.alias && data.alias.toJSON) {
data.alias = data.alias.toJSON();
}
if (!data.id) {
data.id = this.cid;
}
return data;
}
});
return Alias;
});

Просмотреть файл

@ -1,25 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Tahaa Karim <tahaalibra@gmail.com>
* @copyright Tahaa Karim 2016
*/
define(function(require) {
'use strict';
var Backbone = require('backbone');
var Alias = require('models/alias');
/**
* @class AliasesCollection
*/
var AliasesCollection = Backbone.Collection.extend({
model: Alias
});
return AliasesCollection;
});

Просмотреть файл

@ -1,45 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2017
*/
define(function(require) {
'use strict';
var Backbone = require('backbone');
var _ = require('underscore');
/**
* @class Attachment
*/
var Attachment = Backbone.Model.extend({
defaults: {
isLocal: false
},
initialize: function() {
if (_.isUndefined(this.get('id'))) {
this.set('id', _.uniqueId());
}
var s = this.get('fileName');
if (_.isUndefined(s)) {
return;
}
if (s.charAt(0) === '/') {
s = s.substr(1);
}
this.set('displayName', s);
}
});
return Attachment;
});

Просмотреть файл

@ -1,25 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2015
*/
define(function(require) {
'use strict';
var Backbone = require('backbone');
var Attachment = require('models/attachment');
/**
* @class AttachmentCollection
*/
var AttachmentCollection = Backbone.Collection.extend({
model: Attachment
});
return AttachmentCollection;
});

Просмотреть файл

@ -1,26 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function(require) {
'use strict';
var Backbone = require('backbone');
return Backbone.Model.extend({});
});

Просмотреть файл

@ -1,95 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2015
*/
define(function(require) {
'use strict';
var _ = require('underscore');
var Backbone = require('backbone');
/**
* @class Folder
*/
var Folder = Backbone.Model.extend({
messages: undefined,
account: undefined,
folder: undefined,
folders: undefined,
defaults: {
open: false,
folders: [],
messagesLoaded: false
},
initialize: function() {
var FolderCollection = require('models/foldercollection');
var MessageCollection = require('models/messagecollection');
var UnifiedMessageCollection = require('models/unifiedmessagecollection');
this.account = this.get('account');
this.unset('account');
this.folders = new FolderCollection(this.get('folders') || []);
this.folders.forEach(_.bind(function(folder) {
folder.account = this.account;
}, this));
this.unset('folders');
if (this.account && this.account.get('isUnified') === true) {
this.messages = new UnifiedMessageCollection();
} else {
this.messages = new MessageCollection();
}
},
toggleOpen: function() {
this.set({open: !this.get('open')});
},
/**
* @param {Message} message
*/
addMessage: function(message) {
if (this.account.id !== -1) {
// Non-unified folder messages should keep their source folder
message.folder = this;
}
message = this.messages.add(message);
if (this.account.id === -1) {
message.set('unifiedId', this.messages.getUnifiedId(message));
}
return message;
},
/**
* @param {Array<Message>} messages
*/
addMessages: function(messages) {
var _this = this;
return _.map(messages, _this.addMessage, this);
},
/**
* @param {Folder} folder
*/
addFolder: function(folder) {
folder = this.folders.add(folder);
folder.account = this.account;
},
toJSON: function() {
var data = Backbone.Model.prototype.toJSON.call(this);
if (!data.id) {
data.id = this.cid;
}
return data;
}
});
return Folder;
});

Просмотреть файл

@ -1,25 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2015, 2016
*/
define(function(require) {
'use strict';
var Backbone = require('backbone');
var Folder = require('models/folder');
/**
* @class FolderCollection
*/
var FolderCollection = Backbone.Collection.extend({
model: Folder
});
return FolderCollection;
});

Просмотреть файл

@ -1,36 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Luc Calaresu <dev@calaresu.com>
*/
define(function(require) {
'use strict';
var Attachment = require('models/attachment');
var LocalAttachment = Attachment.extend({
defaults: {
progress: 0,
uploadStatus: 0, /* 0=pending, 1=ongoing, 2=error, 3=success */
isLocal: true
},
/**
* @param {Event} evt
*/
onProgress: function(evt) {
if (evt.lengthComputable) {
this.set('uploadStatus', 1);
this.set('progress', evt.loaded / evt.total);
}
}
});
return LocalAttachment;
});

Просмотреть файл

@ -1,57 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2015
*/
define(function(require) {
'use strict';
var Backbone = require('backbone');
var MessageFlags = require('models/messageflags');
/**
* @class Message
*/
var Message = Backbone.Model.extend({
folder: undefined,
defaults: {
flags: [],
active: false,
from: [],
to: [],
cc: [],
bcc: []
},
initialize: function() {
this.set('flags', new MessageFlags(this.get('flags')));
if (this.get('folder')) {
// Folder should be a simple property
this.folder = this.get('folder');
this.unset('folder');
}
this.listenTo(this.get('flags'), 'change', this._transformEvent);
this.set('dateMicro', this.get('dateInt') * 1000);
},
_transformEvent: function() {
this.trigger('change');
this.trigger('change:flags', this);
},
toJSON: function() {
var data = Backbone.Model.prototype.toJSON.call(this);
if (data.flags && data.flags.toJSON) {
data.flags = data.flags.toJSON();
}
if (!data.id) {
data.id = this.cid;
}
return data;
}
});
return Message;
});

Просмотреть файл

@ -1,28 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2015
*/
define(function(require) {
'use strict';
var Backbone = require('backbone');
var Message = require('models/message');
/**
* @class MessageCollection
*/
var MessageCollection = Backbone.Collection.extend({
model: Message,
comparator: function(message) {
return message.get('dateInt') * -1;
}
});
return MessageCollection;
});

Просмотреть файл

@ -1,26 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2015
*/
define(function(require) {
'use strict';
var Backbone = require('backbone');
/**
* @class MessageFlags
*/
var MessageFlags = Backbone.Model.extend({
defaults: {
answered: false
}
});
return MessageFlags;
});

Просмотреть файл

@ -1,32 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2017
*/
define(function(require) {
'use strict';
var MessageCollection = require('models/messagecollection');
/**
* @class UnifiedMessageCollection
*/
var UnifiedMessageCollection = MessageCollection.extend({
modelId: function(attrs) {
return attrs.unifiedId;
},
getUnifiedId: function(message) {
return message.id + '-' + message.folder.id + '-' + message.folder.account.id;
}
});
return UnifiedMessageCollection;
});

Просмотреть файл

@ -1,42 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2016
*/
define(function(require) {
'use strict';
var _ = require('underscore');
var Radio = require('backbone.radio');
var channelNames = [
'account',
'aliases',
'attachment',
'avatar',
'folder',
'dav',
'keyboard',
'message',
'navigation',
'notification',
'preference',
'state',
'sync',
'ui'
];
var channels = {};
_.each(channelNames, function(channelName) {
channels[channelName] = Radio.channel(channelName);
// Uncomment the following line for debugging
// Radio.tuneIn(channelName);
});
return channels;
});

Просмотреть файл

@ -1,193 +0,0 @@
/* global Promise */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function(require) {
'use strict';
var _ = require('underscore');
var Backbone = require('backbone');
var CrashReport = require('crashreport');
var Radio = require('radio');
var FolderController = require('controller/foldercontroller');
/**
* @class RoutingController
*/
var RoutingController = function(accounts) {
this.initialize(accounts);
};
RoutingController.prototype = {
accounts: undefined,
initialize: function(accounts) {
this.accounts = accounts;
Radio.navigation.on('folder', _.bind(this.showFolder, this));
Radio.navigation.on('search', _.bind(this.searchFolder, this));
Radio.navigation.on('setup', _.bind(this.showSetup, this));
Radio.navigation.on('accountsettings', _.bind(this.showAccountSettings, this));
Radio.navigation.on('keyboardshortcuts', _.bind(this.showKeyboardShortcuts, this));
},
_navigate: function(route, options) {
options = options || {};
Backbone.history.navigate(route, options);
},
/**
* Handle mailto links
*
* @returns {Promise}
*/
_handleMailto: function(params) {
var composerOptions = {};
params = params.split('&');
_.each(params, function(param) {
param = param.split('=');
var key = param[0];
var value = param[1];
value = decodeURIComponent((value).replace(/\+/g, '%20'));
switch (key) {
case 'mailto':
case 'to':
composerOptions.to = value;
break;
case 'cc':
composerOptions.cc = value;
break;
case 'bcc':
composerOptions.bcc = value;
break;
case 'subject':
composerOptions.subject = value;
break;
case 'body':
composerOptions.body = value;
break;
}
});
return this.default(true).then(function() {
Radio.ui.trigger('composer:show', composerOptions);
}).catch(CrashReport.report);
},
/**
* @param {bool} showComposer
* @returns {Promise}
*/
default: function(showComposer) {
this._navigate('');
var _this = this;
if (this.accounts.isEmpty()) {
// No account configured -> show setup
return _this.showSetup();
}
// Show first folder of first account
var firstAccount = this.accounts.at(0);
var firstFolder = firstAccount.folders.at(0);
return _this.showFolder(firstAccount.get('accountId'), firstFolder.get('id'), showComposer);
},
/**
* @param {int} accountId
* @param {string} folderId
* @param {bool} showComposer
* @returns {Promise}
*/
showFolder: function(accountId, folderId, showComposer) {
this._navigate('accounts/' + accountId + '/folders/' + folderId);
var _this = this;
var account = this.accounts.get(accountId);
if (_.isUndefined(account)) {
// Unknown account id -> redirect
Radio.ui.trigger('error:show', t('mail', 'Invalid account'));
return _this.default();
}
var folder = account.getFolderById(folderId);
if (_.isUndefined(folder)) {
folder = account.folders.at(0);
Radio.ui.trigger('error:show', t('mail', 'Invalid folder'));
this._navigate('accounts/' + accountId + '/folders/' + folder.get('id'));
return Promise.resolve();
}
return FolderController.showFolder(account, folder, !showComposer);
},
searchFolder: function(accountId, folderId, query) {
if (!query || query === '') {
this.showFolder(accountId, folderId);
return;
}
this._navigate('accounts/' + accountId + '/folders/' + folderId + '/search/' + query);
var account = this.accounts.get(accountId);
if (_.isUndefined(account)) {
// Unknown account id -> redirect
Radio.ui.trigger('error:show', t('mail', 'Invalid account'));
this.default();
return;
}
var folder = account.getFolderById(folderId);
if (_.isUndefined(folder)) {
folder = account.folders.at(0);
Radio.ui.trigger('error:show', t('mail', 'Invalid folder'));
this._navigate('accounts/' + accountId + '/folders/' + folder.get('id'));
}
FolderController.searchFolder(account, folder, query);
},
mailTo: function(params) {
this._handleMailto(params);
},
/**
* @returns {Promise}
*/
showSetup: function() {
this._navigate('setup');
Radio.ui.trigger('navigation:hide');
Radio.ui.trigger('setup:show');
return Promise.resolve();
},
showKeyboardShortcuts: function() {
this._navigate('shortcuts');
Radio.ui.trigger('keyboardShortcuts:show');
},
showAccountSettings: function(accountId) {
this._navigate('accounts/' + accountId + '/settings');
var account = this.accounts.get(accountId);
if (_.isUndefined(account)) {
// Unknown account id -> redirect
Radio.ui.trigger('error:show', t('mail', 'Invalid account'));
this.default();
return;
}
Radio.ui.trigger('navigation:hide');
Radio.ui.trigger('accountsettings:show', account);
}
};
return RoutingController;
});

Просмотреть файл

@ -1,41 +1,25 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
define(function(require) {
'use strict';
Vue.use(Router)
var Marionette = require('backbone.marionette');
/**
* @class Router
*/
var Router = Marionette.AppRouter.extend({
appRoutes: {
'': 'default',
'accounts/:accountId/folders/:folderId': 'showFolder',
'accounts/:accountId/folders/:folderId/search/:query': 'searchFolder',
'mailto(?:params)': 'mailTo',
'setup': 'showSetup',
'shortcuts': 'showKeyboardShortcuts',
'accounts/:accountId/settings': 'showAccountSettings'
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
}
});
return Router;
});
]
})

Просмотреть файл

@ -1,32 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2015, 2016
*/
define(function(require) {
'use strict';
var Radio = require('radio');
var lastQuery = '';
function filter(query) {
if (query !== lastQuery) {
lastQuery = query;
if (require('state').currentAccount && require('state').currentFolder) {
var accountId = require('state').currentAccount.get('accountId');
var folderId = require('state').currentFolder.get('id');
Radio.navigation.trigger('search', accountId, folderId, query);
}
}
}
return {
filter: filter
};
});

Просмотреть файл

@ -1,82 +0,0 @@
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2015
*/
define(function(require) {
'use strict';
var Radio = require('radio');
var AccountCollection = require('models/accountcollection');
var state = {};
var accounts = new AccountCollection();
var currentAccount = null;
var currentFolder = null;
var currentMessage = null;
var currentMessageSubject = null;
var currentMessageBody = '';
Object.defineProperties(state, {
accounts: {
get: function() {
return accounts;
},
set: function(acc) {
accounts = acc;
}
},
currentAccount: {
get: function() {
return currentAccount;
},
set: function(account) {
currentAccount = account;
}
},
currentFolder: {
get: function() {
return currentFolder;
},
set: function(newFolder) {
var oldFolder = currentFolder;
currentFolder = newFolder;
if (newFolder !== oldFolder) {
Radio.ui.trigger('folder:changed');
}
}
},
currentMessage: {
get: function() {
return currentMessage;
},
set: function(newMessage) {
currentMessage = newMessage;
}
},
currentMessageSubject: {
get: function() {
return currentMessageSubject;
},
set: function(subject) {
currentMessageSubject = subject;
}
},
currentMessageBody: {
get: function() {
return currentMessageBody;
},
set: function(body) {
currentMessageBody = body;
}
}
});
return state;
});

12
js/store.js Normal file
Просмотреть файл

@ -0,0 +1,12 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
accounts: []
},
mutations: {},
actions: {}
})

Просмотреть файл

@ -1,40 +0,0 @@
/* global md5 */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function(require) {
'use strict';
var Handlebars = require('handlebars');
return function(account) {
var hash = md5(account);
var hue = null;
if (typeof hash.toHsl === 'function') {
var hsl = hash.toHsl();
hue = Math.round(hsl[0] / 40) * 40;
return new Handlebars.SafeString('hsl(' + hue + ', ' + hsl[1] + '%, ' + hsl[2] + '%)');
} else {
var maxRange = parseInt('ffffffffffffffffffffffffffffffff', 16);
hue = parseInt(hash, 16) / maxRange * 256;
return new Handlebars.SafeString('hsl(' + hue + ', 90%, 65%)');
}
};
});

Просмотреть файл

@ -1,29 +0,0 @@
/* global formatDate */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function() {
'use strict';
return function(dateInt) {
var lastModified = new Date(dateInt * 1000);
return formatDate(lastModified);
};
});

Просмотреть файл

@ -1,28 +0,0 @@
/* global humanFileSize */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function() {
'use strict';
return function(size) {
return humanFileSize(size);
};
});

Просмотреть файл

@ -1,32 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function() {
'use strict';
return function(options) {
var hasCc = this.cc.length > 0;
var hasBcc = this.bcc.length > 0;
if (hasCc || hasBcc) {
return options.fn(this);
} else {
return options.inverse(this);
}
};
});

Просмотреть файл

@ -1,39 +0,0 @@
/**
* @author Jakob Sack <mail@jakobsack.de>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function() {
'use strict';
function isToNoreply(recipient) {
var user = recipient.email.substring(0, recipient.email.lastIndexOf('@'));
return ['noreply', 'no-reply'].indexOf(user) !== -1;
}
return function(options) {
var noreplyInTo = this.to.some(isToNoreply);
var noreplyInCc = this.cc.some(isToNoreply);
var noreplyInBcc = this.bcc.some(isToNoreply);
if (noreplyInTo || noreplyInCc || noreplyInBcc) {
return options.fn(this);
} else {
return options.inverse(this);
}
};
});

Просмотреть файл

@ -1,54 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function(require) {
'use strict';
var _ = require('underscore');
var Handlebars = require('handlebars');
return function(addressList) {
var currentAccount = require('state').currentAccount;
var str = _.reduce(addressList, function(memo, value, index) {
if (index !== 0) {
memo += ', ';
}
var label = value.label
.replace(/(^"|"$)/g, '')
.replace(/(^'|'$)/g, '');
label = Handlebars.Utils.escapeExpression(label);
var email = Handlebars.Utils.escapeExpression(value.email);
if (currentAccount && (email === currentAccount.get('emailAddress') ||
_.find(currentAccount.get('aliases').
toJSON(), function(alias) {
return alias.alias === email;
}))) {
label = t('mail', 'you');
}
var title = t('mail', 'Send message to {email}', {email: email});
memo += '<span class="tooltip-mailto" title="' + title + '">';
memo += '<a class="link-mailto" data-email="' + email + '" data-label="' + label + '">';
memo += label + '</a></span>';
return memo;
}, '');
return new Handlebars.SafeString(str);
};
});

Просмотреть файл

@ -1,44 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function(require) {
'use strict';
var _ = require('underscore');
var Handlebars = require('handlebars');
return function(addressList) {
var str = _.reduce(addressList, function(memo, value, index) {
if (index !== 0) {
memo += ', ';
}
var label = value.label
.replace(/(^"|"$)/g, '')
.replace(/(^'|'$)/g, '');
label = Handlebars.Utils.escapeExpression(label);
var email = Handlebars.Utils.escapeExpression(value.email);
if (label === email) {
return memo + email;
} else {
return memo + '"' + label + '" <' + email + '>';
}
}, '');
return str;
};
});

Просмотреть файл

@ -1,33 +0,0 @@
/* global relative_modified_date */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function() {
'use strict';
return function(dateInt) {
var lastModified = new Date(dateInt * 1000);
var lastModifiedTime = Math.round(lastModified.getTime() / 1000);
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
return relative_modified_date(lastModifiedTime);
// jscs:enable requireCamelCaseOrUpperCaseIdentifiers
};
});

Просмотреть файл

@ -1,26 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function() {
'use strict';
return function(text) {
return t('mail', text);
};
});

Просмотреть файл

@ -1,32 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(function() {
'use strict';
return function(options) {
var hasCc = this.cc.length > 0;
var hasBcc = this.bcc.length > 0;
if (!hasCc && !hasBcc) {
return options.fn(this);
} else {
return options.inverse(this);
}
};
});

Просмотреть файл

@ -1,113 +0,0 @@
<form method="post">
<div class="hidden-visually">
<!-- Hack for Safari and Chromium/Chrome which ignore autocomplete="off" -->
<input type="text" id="fake_user" name="fake_user"
autocomplete="off" tabindex="-1">
<input type="password" id="fake_password" name="fake_password"
autocomplete="off" tabindex="-1">
</div>
<fieldset>
<div id="emptycontent">
<div class="icon-mail"></div>
<h2>{{ t 'Connect your mail account' }}</h2>
</div>
<p class="grouptop">
<input type="text"
name="account-name"
placeholder="{{ t 'Name' }}"
value="{{ config.name }}"
autofocus />
</p>
<p class="groupmiddle">
<input type="email"
name="mail-address"
placeholder="{{ t 'Mail Address' }}"
value="{{ config.emailAddress }}"
required />
</p>
<p class="groupbottom">
<input type="password"
name="mail-password"
placeholder="{{ t 'Password' }}"
value="{{ config.password }}"
required />
</p>
<a class="toggle-manual-mode icon-caret-dark">{{ t 'Manual configuration' }}</a>
<div class="manual-inputs">
<p class="grouptop">
<input type="text"
name="imap-host"
placeholder="{{ t 'IMAP Host' }}"
value="{{ config.imapHost }}" />
</p>
<p class="groupmiddle" id="setup-imap-ssl">
<select id="setup-imap-ssl-mode"
name="imap-sslmode"
title="{{ t 'IMAP security' }}">
<option value="none">{{ t 'None' }}</option>
<option value="ssl">{{ t 'SSL/TLS' }}</option>
<option value="tls">{{ t 'STARTTLS' }}</option>
</select>
</p>
<p class="groupmiddle">
<input type="number"
name="imap-port"
placeholder="{{ t 'IMAP Port' }}"
value="{{ config.imapPort }}" />
</p>
<p class="groupmiddle">
<input type="text"
name="imap-user"
placeholder="{{ t 'IMAP User' }}"
value="{{ config.imapUser }}" />
</p>
<p class="groupbottom">
<input type="password"
name="imap-password"
placeholder="{{ t 'IMAP Password' }}"
value="{{ config.imapPassword }}"
required />
</p>
<p class="grouptop">
<input type="text"
name="smtp-host"
placeholder="{{ t 'SMTP Host' }}"
value="{{ config.smtpHost }}" />
</p>
<p class="groupmiddle" id="setup-smtp-ssl">
<select id="setup-smtp-ssl-mode"
name="mail-smtp-sslmode"
title="{{ t 'SMTP security' }}">
<option value="none">{{ t 'None' }}</option>
<option value="ssl">{{ t 'SSL/TLS' }}</option>
<option value="tls">{{ t 'STARTTLS' }}</option>
</select>
</p>
<p class="groupmiddle">
<input type="number"
name="smtp-port"
placeholder="{{ t 'SMTP Port' }}"
value="{{ config.smtpPort }}" />
</p>
<p class="groupmiddle">
<input type="text"
name="smtp-user"
placeholder="{{ t 'SMTP User' }}"
value="{{ config.smtpUser }}" />
</p>
<p class="groupbottom">
<input type="password"
name="smtp-password"
placeholder="{{ t 'SMTP Password' }}"
value="{{ config.smtpPassword }}"
required />
</p>
</div>
<input type="submit"
class="primary"
value="{{ t 'Connect' }}"/>
</fieldset>
</form>

Просмотреть файл

@ -1,43 +0,0 @@
{{#unless isUnifiedInbox}}
<li class="{{#if hasMenu}} with-menu {{/if}}">
{{#if emailAddress}}
<div class="app-navigation-entry-bullet" style="background-color: {{accountColor emailAddress}}"></div>
{{/if}}
<a>{{emailAddress}}</a>
{{#if hasMenu}}
<div class="app-navigation-entry-utils">
<ul>
<li class="app-navigation-entry-utils-menu-button svg"><button></button></li>
</ul>
</div>
<div class="app-navigation-entry-menu">
<ul>
<li>
<a href="#" class="menuitem action action-settings permanent">
<span class="icon icon-rename"></span>
<span>{{ t 'Settings' }}</span>
</a>
</li>
{{#if isDeletable}}
<li>
<a href="#" class="menuitem action action-delete permanent">
<span class="icon icon-delete"></span>
<span>{{ t 'Delete account' }}</span>
</a>
</li>
{{/if}}
</ul>
</div>
{{/if}}
</li>
{{/unless}}
<div class="folders"></div>
{{#unless isUnifiedInbox}}
{{#if hasFolders}}
<li class="account-toggle-collapse">
<a>{{toggleCollapseMessage}}</a>
</li>
{{/if}}
{{/unless}}

Просмотреть файл

@ -1,22 +0,0 @@
<div class="section" id="account-info">
<h2>{{ t 'Account Settings' }} - {{ email }}</h2>
</div>
<div class="section" id="aliases">
<h2>{{ t 'Aliases' }}</h2>
<div id="aliases-list"></div>
<form name="aliasForm" method="post">
<input type="text" name="alias" id="alias" placeholder="{{ t 'Mail address' }}">
<input type="text" name="alias-name" id="alias-name" placeholder="{{ t 'Display Name' }}">
<input type="submit" value="{{ t 'Save' }}">
</form>
</div>
<div class="section">
<h2>{{ t 'Account' }}</h2>
<div id="mail-settings-status">
<div id="mail-settings-loading" class="icon-loading-small" style="display: none; float: left; margin-right: 10px;"></div>
<span id="mail-settings-msg" class="msg success" style="display: none;">Saved</span>
</div>
<div id="mail-settings"></div>
</div>

Просмотреть файл

@ -1,14 +0,0 @@
<table class="grid">
<tbody>
<tr id="{{ aliases.id }}">
<td style="padding:0 5px 0 5px;">
{{ aliases.alias }}
</td>
<td style="padding:0 5px 0 5px;">
{{ aliases.name }}
</td>
<td style="padding:0 5px 0 5px;"> <button type="submit" value="{{ id }}" class="icon-delete"></button>
</td>
</tr>
</tbody>
</table>

Просмотреть файл

@ -1,2 +0,0 @@
<div class="new-message-attachment-name">{{displayName}}</div>
<div class="new-message-attachments-action svg icon-delete"></div>

Просмотреть файл

@ -1,8 +0,0 @@
<ul></ul>
<button type="button" id="add-local-attachment" style="display: inline-block;">
<span class="icon-upload"/> {{ t 'Upload attachment' }}
</button>
<button type="button" id="add-cloud-attachment" style="display: inline-block;">
<span class="icon-folder"/> {{ t 'Add attachment from Files' }}
</button>
<input type="file" multiple id="local-attachments" style="display: none;">

Просмотреть файл

@ -1 +0,0 @@
<a class="select-calendar" data-calendar-url="{{url}}">{{displayname}}</a>

Просмотреть файл

@ -1,50 +0,0 @@
<div class="message-composer">
<select class="mail-account">
{{#each aliases}}
<option value="{{id}}">{{ t 'from' }} {{name}} &lt;{{emailAddress}}&gt;</option>
{{/each}}
</select>
<div class="composer-fields">
<a href="#" class="composer-cc-bcc-toggle transparency
{{#ifHasCC}}
hidden
{{/ifHasCC}}">{{ t '+ cc/bcc' }}</a>
<input type="text" name="to"
value="{{printAddressListPlain to}}"
class="to recipient-autocomplete" />
<label class="to-label transparency" for="to">{{ t 'to' }}</label>
<div class="composer-cc-bcc
{{#unlessHasCC}}
hidden
{{/unlessHasCC}}">
<input type="text"
name="cc"
class="cc recipient-autocomplete"
value="{{printAddressListPlain cc}}"
/>
<label for="cc" class="cc-label transparency">{{ t 'cc' }}</label>
<input type="text"
name="bcc"
class="bcc recipient-autocomplete"
value="{{printAddressListPlain bcc}}"
/>
<label for="bcc" class="bcc-label transparency">{{ t 'bcc' }}</label>
</div>
{{#ifToNoreply}}
<div class="warning noreply-box">{{ t 'Note that the mail came from a noreply address so your reply will probably not be read.' }}</div>
{{/ifToNoreply}}
{{#unless isReply}}
<input type="text" name="subject" value="{{subject}}" class="subject" autocomplete="off"
placeholder="{{ t 'Subject' }}" />
{{/unless}}
<textarea name="body"
class="message-body"
placeholder="{{ t 'Message …' }}">{{message}}</textarea>
</div>
<div class="submit-message-wrapper">
<input class="submit-message send primary" type="submit" value="{{submitButtonTitle}}" disabled>
<div class="submit-message-wrapper-inside" ></div>
</div>
<div class="new-message-attachments">
</div>
</div>

Просмотреть файл

@ -1,2 +0,0 @@
<div class="icon-mail"></div>
<h2>{{ t 'No messages in this folder' }}</h2>

Просмотреть файл

@ -1 +0,0 @@
<div class="container hidden-mobile"></div>

Просмотреть файл

@ -1,8 +0,0 @@
<div class="">
{{#if icon}}<div class="{{icon}}"></div>{{/if}}
<h2>{{{ text }}}</h2>
{{# if canRetry }}
<br>
<button class="retry">{{ t 'Try again' }}</button>
{{/if}}
</div>

Просмотреть файл

@ -1,15 +0,0 @@
{{#if folders}}
<button class="collapse"></button>
{{/if}}
<a class="folder
{{#if specialRole}} icon-{{specialRole}} svg{{/if}}
{{#if noSelect}} no-select {{/if}}"
href="{{#if noSelect}}#{{else}}{{url}}{{/if}}">
{{name}}
</a>
<div class="app-navigation-entry-utils">
<ul>
<li class="app-navigation-entry-utils-counter">{{#if count}}{{count}}{{/if}}</li>
</ul>
</div>
<div class="folders"></div>

Просмотреть файл

@ -1,2 +0,0 @@
<div class="app-content-list"></div>
<div class="app-content-details"></div>

Просмотреть файл

@ -1,48 +0,0 @@
<div id="app-shortcuts" class="section">
<h2>{{t 'Keyboard shortcuts'}}</h2>
<p>{{t 'Speed up your Mail experience with these quick shortcuts.'}}</p>
<dl>
<div>
<dt><kbd>C</kbd></dt>
<dd>{{t 'Compose new message'}}</dd>
</div>
<div>
<dt><kbd>K</kbd> or <kbd></kbd></dt>
<dd>{{t 'Newer message'}}</dd>
</div>
<div>
<dt><kbd>J</kbd> or <kbd></kbd></dt>
<dd>{{t 'Older message'}}</dd>
</div>
<div>
<dt><kbd>S</kbd></dt>
<dd>{{ t 'Toggle star' }}</dd>
</div>
<div>
<dt><kbd>U</kbd></dt>
<dd>{{ t 'Toggle unread' }}</dd>
</div>
<div>
<dt><kbd>Del</kbd></dt>
<dd>{{ t 'Delete' }}</dd>
</div>
<div>
<dt><kbd>Ctrl</kbd> + <kbd>F</kbd></dt>
<dd>{{ t 'Search' }}</dd>
</div>
<div>
<dt><kbd>Ctrl</kbd> + <kbd>Enter</kbd></dt>
<dd>{{ t 'Send' }}</dd>
</div>
<div>
<dt><kbd>R</kbd></dt>
<dd>{{t 'Refresh'}}</dd>
</div>
</dl>
</div>

Просмотреть файл

@ -1,8 +0,0 @@
{{#if hint}}
<div class="emptycontent">
<a class="icon-loading"></a>
<h2>{{{ hint }}}</h2>
</div>
{{else}}
<div class="container icon-loading"></div>
{{/if}}

Просмотреть файл

@ -1,12 +0,0 @@
{{#if isImage}}
<img class="mail-attached-image" src="{{downloadUrl}}">
<br>
{{/if}}
<img class="attachment-icon" src="{{mimeUrl}}" />
<span class="attachment-name" title="{{fileName}} ({{humanFileSize size}})">{{fileName}} <span class="attachment-size">({{humanFileSize size}})</span></span>
{{#if isCalendarEvent}}
<button class="button icon-add attachment-import calendar" title="{{ t 'Import into calendar' }}"></button>
{{/if}}
<button class="button icon-download attachment-download" title="{{ t 'Download attachment' }}"></button>
<button class="icon-folder attachment-save-to-cloud" title="{{ t 'Save to Files' }}"></button>
<div class="popovermenu bubble attachment-import-popover hidden"></div>

Просмотреть файл

@ -1,8 +0,0 @@
<div class="attachments">
</div>
{{#if moreThanOne}}
<p>
<button class="icon-folder attachments-save-to-cloud">{{ t 'Save all to Files' }}</button>
</p>
{{/if}}

Просмотреть файл

@ -1,58 +0,0 @@
<div class="app-content-list-item {{#if flags.unseen}}unseen{{/if}} {{#if active}}active{{/if}}" data-message-id="{{id}}">
{{#if isUnified}}
<div class="mail-message-account-color" style="background-color: {{accountColor accountMail}}"></div>
{{/if}}
{{#if flags.flagged}}
<div class="star app-content-list-item-star icon-starred" data-starred="true"></div>
{{else}}
<div class="star app-content-list-item-star icon-star" data-starred="false"></div>
{{/if}}
<div class="app-content-list-item-icon sender-image avatardiv">
{{#if senderImage}}
<img src="{{senderImage}}" width="32px" height="32px" />
{{else}}
<div class="avatar" data-user="{{sender.label}}" data-size="32"></div>
{{/if}}
</div>
<div class="app-content-list-item-line-one" title="{{label}}">{{label}}</div>
<div class="app-content-list-item-line-two" title="{{subject}}">
{{#if flags.answered}}
<span class="icon-reply"></span>
{{/if}}
{{#if flags.hasAttachments}}
<span class="icon-public icon-attachment"></span>
{{/if}}
{{subject}}
</div>
<div class="app-content-list-item-details date">
<span class="modified live-relative-timestamp"
data-timestamp="{{dateMicro}}"
title="{{formatDate dateInt}}">
{{relativeModifiedDate dateInt}}
</span>
</div>
<div class="app-content-list-item-menu">
<div class="icon-more toggle-menu"></div>
<div class="popovermenu">
<ul>
<li>
<button class="action toggle-read">
<span class="icon-mail"></span>
<span>{{ t 'Toggle read' }}</span>
</button>
</li>
<li>
<button class="action delete">
<span class="icon-delete"></span>
<span>{{ t 'Delete' }}</span>
</button>
</li>
</ul>
</div>
</div>
</div>
</div>

Просмотреть файл

@ -1,5 +0,0 @@
<div id="mail-message-list-loading"
class="icon-loading-small"
style="display: none"></div>
<div id="mail-message-list"></div>
<div id="load-more-mail-messages"></div>

Просмотреть файл

@ -1,37 +0,0 @@
<div id="mail-message-close" class="icon-close"></div>
<div id="mail-message-header" class="section">
<h2 title="{{subject}}">{{subject}}</h2>
<p class="transparency">
{{printAddressList from}}
{{ t 'to' }}
{{printAddressList to}}
{{#if cc.length}}
({{ t 'cc' }} {{printAddressList cc}})
{{/if}}
</p>
</div>
<div class="mail-message-body">
<div id="mail-content">
{{#if hasHtmlBody}}
<div id="show-images-text">
{{ t 'The images have been blocked to protect your privacy.' }}
<button id="show-images-button">{{ t 'Show images from this sender' }}</button>
</div>
<div class="icon-loading">
<iframe src="{{htmlBodyUrl}}" seamless>
</iframe>
</div>
{{else}}
{{{body}}}
{{/if}}
</div>
{{#if signature}}
<div class="mail-signature">
{{{signature}}}
</div>
{{/if}}
<div class="mail-message-attachments"></div>
<div id="reply-composer"></div>
<input type="button" id="forward-button" value="{{ t 'Forward' }}">
</div>

Просмотреть файл

@ -1,3 +0,0 @@
<button type="button"
id="mail_new_message"
class="icon-add">{{ t 'New message' }}</button>

Просмотреть файл

@ -1,4 +0,0 @@
<div id="emptycontent" class="emptycontent-search">
<div class="icon-search"></div>
<h2>{{ t 'No search results for' }} {{ searchTerm }}</h2>
</div>

Просмотреть файл

@ -1,25 +0,0 @@
<div id="mailsettings">
<ul id="settings-accounts" class="mailaccount-list">
</ul>
<a id="new-mail-account"
class="button new-button"
href="{{addAccountUrl}}">{{ t 'Add mail account' }}</a>
<p>
<input class="checkbox"
id="gravatar-enabled"
{{#if useExternalAvatars}}checked="checked"{{/if}}
type="checkbox">
<label for="gravatar-enabled">{{ t 'Use Gravatar and favicon avatars' }}</label>
</p>
<p class="app-settings-hint">
<a id="keyboard-shortcuts"
href="{{keyboardShortcutUrl}}">{{ t 'Keyboard shortcuts' }}</a>
</p>
<p class="app-settings-hint">
{{{ t 'Looking to encrypt your emails? Install the <a href="https://www.mailvelope.com/" target="_blank">Mailvelope browser extension</a>!' }}}
</p>
</div>

Просмотреть файл

@ -1 +0,0 @@
<div class="setup-content container"></div>

Просмотреть файл

@ -0,0 +1,19 @@
// A custom Nightwatch assertion.
// The assertion name is the filename.
// Example usage:
//
// browser.assert.elementCount(selector, count)
//
// For more information on custom assertions see:
// http://nightwatchjs.org/guide#writing-custom-assertions
exports.assertion = function elementCount (selector, count) {
this.message = `Testing if element <${selector}> has count: ${count}`
this.expected = count
this.pass = val => val === count
this.value = res => res.value
function evaluator (_selector) {
return document.querySelectorAll(_selector).length
}
this.command = cb => this.api.execute(evaluator, [selector], cb)
}

Просмотреть файл

@ -0,0 +1,14 @@
// For authoring Nightwatch tests, see
// http://nightwatchjs.org/guide#usage
module.exports = {
'default e2e tests': browser => {
browser
.url(process.env.VUE_DEV_SERVER_URL)
.waitForElementVisible('#app', 5000)
.assert.elementPresent('.hello')
.assert.containsText('h1', 'Welcome to Your Vue.js App')
.assert.elementCount('img', 1)
.end()
}
}

Просмотреть файл

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Jasmine Spec Runner</title>
<link rel="stylesheet" type="text/css" href="../vendor/jasmine-core/lib/jasmine-core/jasmine.css">
<script type="text/javascript" src="../vendor/requirejs/require.js"></script>
<script type="text/javascript" src="Views_Attachments_Spec.js"></script>
</head>
<body>
<div id="sandbox" style="overflow: hidden; height: 1px;"></div>
</body>
</html>

Просмотреть файл

@ -1,103 +0,0 @@
/* global expect, spyOn */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'jquery',
'app',
'radio',
'backbone',
'controller/accountcontroller',
'models/accountcollection'
], function($, Mail, Radio, Backbone, AccountController,
AccountCollection) {
describe('App', function() {
beforeEach(function() {
jasmine.Ajax.install();
$('testcontainer').remove();
$('body')
.append('testcontainer')
.append(
'<input type="hidden" id="config-installed-version" value="0.6.1">'
+ '<input type="hidden" id="serialized-accounts" value="">'
+ '<div id="user-displayname">Jane Doe</div>'
+ '<div id="user-email">jane@doe.cz</div>'
+ '<div id="app">'
+ ' <div id="app-navigation" class="icon-loading">'
+ ' <div id="mail-new-message-fixed"></div>'
+ ' <div id="usergrouplist"></div>'
+ ' <div id="app-settings">'
+ ' <div id="app-settings-header">'
+ ' <button class="settings-button" data-apps-slide-toggle="#app-settings-content"><?php p($l->t("Settings"));?></button>'
+ ' </div>'
+ ' <div id="app-settings-content"></div>'
+ ' </div>'
+ ' </div>'
+ ' <div id="app-content">'
+ ' <div class="mail-content container">'
+ ' <div class="container icon-loading"></div>'
+ ' </div>'
+ ' </div>'
+ '</div>');
});
afterEach(function() {
jasmine.Ajax.uninstall();
});
it('starts', function(done) {
var resolve;
var accountsPromise = new Promise(function(res) {
resolve = res;
});
spyOn(Radio.ui, 'trigger');
spyOn(Backbone.history, 'start');
spyOn(AccountController, 'loadAccounts').and.callFake(function() {
return accountsPromise;
});
spyOn(Mail, 'startBackgroundSync');
// No ajax calls so far
expect(jasmine.Ajax.requests.count()).toBe(0);
// Let's go…
Mail.start();
expect(Radio.ui.trigger).toHaveBeenCalledWith('content:loading', 'Loading accounts');
var accounts = new AccountCollection([
{
accountId: 44,
name: 'Jane Doe',
email: 'jane@doe.se'
}
]);
resolve(accounts);
accountsPromise.then(function() {
// The promise is resolved asynchronously, so we have to use the
// promise here too
expect(Backbone.history.start).toHaveBeenCalled();
}).then(done).catch(done.fail);
});
});
});

Просмотреть файл

@ -1,64 +0,0 @@
/* global spyOn */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(['models/account',
'models/foldercollection',
'models/aliasescollection',
'models/folder',
'OC'
], function(Account, FolderCollection, AliasCollection, Folder, OC) {
describe('Account test', function() {
var account;
beforeEach(function() {
account = new Account();
});
it('has collections as default attributes', function() {
var folders = account.folders;
var aliases = account.get('aliases');
expect(folders instanceof FolderCollection).toBe(true);
expect(aliases instanceof AliasCollection).toBe(true);
});
it('uses accountId as id attribute', function() {
expect(account.idAttribute).toBe('accountId');
});
it('has the correct URL', function() {
spyOn(OC, 'generateUrl').and.returnValue('index.php/apps/mail/api/accounts');
var url = account.url();
expect(url).toBe('index.php/apps/mail/api/accounts');
});
it('adds folders to its collection', function() {
var folder = new Folder();
account.addFolder(folder);
expect(account.folders.length).toBe(1);
expect(folder.account).toBe(account);
});
});
});

Просмотреть файл

@ -1,55 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'models/accountcollection',
'models/account',
'OC'
], function(AccountCollection, Account, OC) {
describe('AccountCollection', function() {
var collection;
var account;
beforeEach(function() {
collection = new AccountCollection();
});
it('contains accounts', function() {
expect(collection.model).toBe(Account);
});
it('uses the right URL', function() {
spyOn(OC, 'generateUrl').and.returnValue('index.php/apps/mail/api/accounts');
var url = collection.url();
expect(OC.generateUrl).toHaveBeenCalled();
expect(url).toBe('index.php/apps/mail/api/accounts');
});
it('sorts accounts by accountId', function() {
account = new Account();
account.set('accountId', 12);
var cmp = collection.comparator(account);
expect(cmp).toBe(12);
});
});
});

Просмотреть файл

@ -1,41 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(['models/alias'], function(Alias) {
describe('Alias test', function() {
var alias;
beforeEach(function() {
alias = new Alias();
});
it('serializes the alias model', function() {
alias.set('id', 123);
alias.set('alias', 'alias@example.com');
var serialized = alias.toJSON();
expect(serialized).toEqual({
id: 123,
alias: 'alias@example.com'
});
});
});
});

Просмотреть файл

@ -1,35 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'models/aliasescollection',
'models/alias'
], function(AliasesCollection, Alias) {
describe('AliasesCollection', function() {
var collection;
beforeEach(function() {
collection = new AliasesCollection();
});
it('contains aliases', function() {
expect(collection.model).toBe(Alias);
});
});
});

Просмотреть файл

@ -1,56 +0,0 @@
/* global expect */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'models/attachment'
], function(Attachment) {
describe('Attachment', function() {
var attachment;
it('has an random id', function() {
var a1 = new Attachment({
fileName: '/file1.png'
});
var a2 = new Attachment({
fileName: '/cat.jpg'
});
expect(a1.get('id')).toBeDefined();
expect(a2.get('id')).toBeDefined();
expect(a1.get('id')).not.toBe(a2.get('id'));
});
it('ha no displayName if fileName is not set', function() {
attachment = new Attachment();
expect(attachment.get('displayName')).toBeUndefined();
});
it('removes leading slash from display name', function() {
attachment = new Attachment({
fileName: '/my/file.jpg'
});
expect(attachment.get('displayName')).toBe('my/file.jpg');
});
});
});

Просмотреть файл

@ -1,37 +0,0 @@
/* global expect */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'models/attachments',
'models/attachment'
], function(Attachents, Attachment) {
describe('Attachments', function() {
var attachments;
beforeEach(function() {
attachments = new Attachents();
});
it('contains attachments', function() {
expect(attachments.model).toBe(Attachment);
});
});
});

Просмотреть файл

@ -1,77 +0,0 @@
/* global expect */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'models/folder',
'models/account',
'models/messagecollection',
'models/message'
], function(Folder, Account, MessageCollection, Message) {
describe('Folder', function() {
var account;
var folder;
beforeEach(function() {
account = new Account();
folder = new Folder();
account.addFolder(folder);
});
it('has messages', function() {
expect(folder.messages instanceof MessageCollection).toBe(true);
});
it('toggles open', function() {
expect(folder.get('open')).toBe(false);
folder.toggleOpen();
expect(folder.get('open')).toBe(true);
folder.toggleOpen();
expect(folder.get('open')).toBe(false);
});
it('assigns itself to an added message', function() {
var message = new Message();
expect(folder.messages.length).toBe(0);
folder.addMessage(message);
expect(folder.messages.length).toBe(1);
expect(message.folder).toBe(folder);
});
it('assigns itself to added messages', function() {
var messages = [{}, {}];
expect(folder.messages.length).toBe(0);
folder.addMessages(messages);
expect(folder.messages.length).toBe(2);
expect(folder.messages.at(0).folder).toBe(folder);
expect(folder.messages.at(1).folder).toBe(folder);
});
});
});

Просмотреть файл

@ -1,35 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'models/foldercollection',
'models/folder'
], function(FolderCollection, Folder) {
describe('FolderCollection', function() {
var collection;
beforeEach(function() {
collection = new FolderCollection();
});
it('contains folders', function() {
expect(collection.model).toBe(Folder);
});
});
});

Просмотреть файл

@ -1,69 +0,0 @@
/* global expect */
/**
* @author Luc Calaresu <dev@calaresu.com>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'models/localattachment'
], function(LocalAttachment) {
/* LocalAttachment derivates from Attachment */
/* We just test the specifics since Attachment is already tested */
describe('LocalAttachment', function() {
var attachment;
it('has an initial progress and status equal to 0', function() {
var a1 = new LocalAttachment({
fileName: '/file1.png'
});
expect(a1.get('progress')).toBe(0);
expect(a1.get('uploadStatus')).toBe(0);
});
it('updates its attributes on upload progress', function() {
attachment = new LocalAttachment();
/* simulate a call to 'onProgress' with some example values */
var progressEvent = {
lengthComputable: true,
loaded: 500,
total: 1000
};
attachment.onProgress(progressEvent);
/* we expect the status to be 'ONGOING' and the value 0.5 (=500/1000) */
expect(attachment.get('progress')).toBe(0.5);
expect(attachment.get('uploadStatus')).toBe(1);
});
it('does not update its attributes if progress is not computable', function() {
attachment = new LocalAttachment();
/* simulate a call to 'onProgress' with some example values */
var progressEvent = {
lengthComputable: false,
loaded: 1000,
total: 1000
};
attachment.onProgress(progressEvent);
expect(attachment.get('progress')).toBe(0);
expect(attachment.get('uploadStatus')).toBe(0);
});
});
});

Просмотреть файл

@ -1,49 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'models/message',
'models/messageflags'
], function(Message, MessageFlags) {
describe('Message', function() {
var message;
beforeEach(function() {
message = new Message();
});
it('has flags and is inactive by default', function() {
expect(message.get('flags') instanceof MessageFlags).toBe(true);
expect(message.get('active')).toBe(false);
});
it('triggers a change event whenever flags change', function() {
var hnd1 = jasmine.createSpy('handler1');
var hnd2 = jasmine.createSpy('handler2');
message.on('change', hnd1);
message.on('change', hnd2);
message.get('flags').trigger('change');
expect(hnd1).toHaveBeenCalled();
expect(hnd2).toHaveBeenCalled();
});
});
});

Просмотреть файл

@ -1,43 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(['models/messagecollection',
'models/message'],
function(MessageCollection, Message) {
describe('MessageCollection', function() {
var collection;
beforeEach(function() {
collection = new MessageCollection();
});
it('contains messages', function() {
expect(collection.model).toBe(Message);
});
it('compares messages by date', function() {
var message = new Message();
message.set('dateInt', 12345);
var cmp = collection.comparator(message);
expect(cmp).toBe(-12345);
});
});
});

Просмотреть файл

@ -1,34 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'models/messageflags'
], function(MessageFlag) {
describe('MessageFlag', function() {
var messageFlag;
beforeEach(function() {
messageFlag = new MessageFlag();
});
it('is answered by default', function() {
expect(messageFlag.get('answered')).toBe(false);
});
});
});

Просмотреть файл

@ -1,191 +0,0 @@
/* global expect */
/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define([
'replybuilder',
'models/message',
'models/folder',
'models/account',
'backbone'
], function(ReplyBuilder, Message, Folder, Account, Backbone) {
describe('ReplyBuilder', function() {
var message, messageBody, folder, account;
beforeEach(function() {
message = new Message();
messageBody = new Backbone.Model();
folder = new Folder();
account = new Account();
account.addFolder(folder);
folder.addMessage(message);
});
var createAddress = function(addr) {
return {
label: addr,
email: addr
};
};
var setEmail = function(message, address) {
message.folder.account.set('emailAddress', address.email);
};
var assertSameAddressList = function(l1, l2) {
var rawL1 = l1.map(function(a) {
return a.email;
});
var rawL2 = l2.map(function(a) {
return a.email;
});
rawL1.sort();
rawL2.sort();
expect(rawL1).toEqual(rawL2);
};
// b -> a to a -as b
it('handles a one-on-one reply', function() {
var a = createAddress('a@domain.tld');
var b = createAddress('b@domain.tld');
messageBody.set('from', [b]);
messageBody.set('to', [a]);
messageBody.set('cc', []);
setEmail(message, a);
var reply = ReplyBuilder.buildReply(message, messageBody);
assertSameAddressList(reply.from, [a]);
assertSameAddressList(reply.to, [b]);
assertSameAddressList(reply.cc, []);
});
it('handles simple group reply', function() {
var a = createAddress('a@domain.tld');
var b = createAddress('b@domain.tld');
var c = createAddress('c@domain.tld');
messageBody.set('from', [a]);
messageBody.set('to', [b, c]);
messageBody.set('cc', []);
setEmail(message, b);
var reply = ReplyBuilder.buildReply(message, messageBody);
assertSameAddressList(reply.from, [b]);
assertSameAddressList(reply.to, [a, c]);
assertSameAddressList(reply.cc, []);
});
it('handles group reply with CC', function() {
var a = createAddress('a@domain.tld');
var b = createAddress('b@domain.tld');
var c = createAddress('c@domain.tld');
var d = createAddress('d@domain.tld');
messageBody.set('from', [a]);
messageBody.set('to', [b, c]);
messageBody.set('cc', [d]);
setEmail(message, b);
var reply = ReplyBuilder.buildReply(message, messageBody);
assertSameAddressList(reply.from, [b]);
assertSameAddressList(reply.to, [a, c]);
assertSameAddressList(reply.cc, [d]);
});
it('handles group reply of CC address', function() {
var a = createAddress('a@domain.tld');
var b = createAddress('b@domain.tld');
var c = createAddress('c@domain.tld');
var d = createAddress('d@domain.tld');
messageBody.set('from', [a]);
messageBody.set('to', [b, c]);
messageBody.set('cc', [d]);
setEmail(message, d);
var reply = ReplyBuilder.buildReply(message, messageBody);
assertSameAddressList(reply.from, [d]);
assertSameAddressList(reply.to, [a, b, c]);
assertSameAddressList(reply.cc, []);
});
it('handles group reply of CC address with many CCs', function() {
var a = createAddress('a@domain.tld');
var b = createAddress('b@domain.tld');
var c = createAddress('c@domain.tld');
var d = createAddress('d@domain.tld');
var e = createAddress('e@domain.tld');
messageBody.set('from', [a]);
messageBody.set('to', [b, c]);
messageBody.set('cc', [d, e]);
setEmail(message, e);
var reply = ReplyBuilder.buildReply(message, messageBody);
assertSameAddressList(reply.from, [e]);
assertSameAddressList(reply.to, [a, b, c]);
assertSameAddressList(reply.cc, [d]);
});
it('handles reply of message where the recipient is in the CC', function() {
var ali = createAddress('ali@domain.tld');
var bob = createAddress('bob@domain.tld');
var me = createAddress('c@domain.tld');
var dani = createAddress('d@domain.tld');
messageBody.set('from', [ali]);
messageBody.set('to', [bob]);
messageBody.set('cc', [me, dani]);
setEmail(message, me);
var reply = ReplyBuilder.buildReply(message, messageBody);
assertSameAddressList(reply.from, [me]);
assertSameAddressList(reply.to, [ali, bob]);
assertSameAddressList(reply.cc, [dani]);
});
it('handles jan\'s reply to nina\'s mesage to a mailing list', function() {
var nina = createAddress('nina@nc.com');
var list = createAddress('list@nc.com');
var jan = createAddress('jan@nc.com');
messageBody.set('from', [nina]);
messageBody.set('to', [list]);
messageBody.set('cc', []);
setEmail(message, jan);
var reply = ReplyBuilder.buildReply(message, messageBody);
assertSameAddressList(reply.from, [jan]);
assertSameAddressList(reply.to, [nina, list]);
assertSameAddressList(reply.cc, []);
});
});
});

Просмотреть файл

@ -1,78 +0,0 @@
/* global sinon, expect */
/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define([
'service/avatarservice'
], function(AvatarService) {
'use strict';
describe('AvatarService', function() {
var server;
beforeEach(function() {
server = sinon.fakeServer.create();
});
afterEach(function() {
server.restore();
});
it('does not load the image if no avatar is available', function(done) {
var loading = AvatarService.loadAvatar('user@domain.com');
expect(server.requests.length).toBe(1);
server.requests[0].respond(
404,
{
'Content-Type': 'application/json'
});
loading.then(function(url) {
expect(url).toBe(undefined);
done();
}).catch(done.fail);
});
it('does load the image if an avatar is available', function(done) {
var loading = AvatarService.loadAvatar('user@domain.com');
expect(server.requests.length).toBe(1);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
isExternal: true,
mime: 'image/jpeg',
url: 'https://domain.com/favicon.ico'
}));
loading.then(function(url) {
expect(url).not.toBe(undefined);
done();
}).catch(done.fail);
});
});
});

Просмотреть файл

@ -1,49 +0,0 @@
/* global expect, Promise, spyOn */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'radio',
'models/account',
'models/folder',
'service/backgroundsyncservice'
], function(Radio, Account, Folder, BackgroundSyncService) {
describe('Background sync service', function() {
it('fails', function(done) {
spyOn(Radio.sync, 'request').and.returnValue(Promise.resolve([]));
spyOn(Radio.ui, 'trigger');
var account = new Account({
accountId: -1,
isUnified: true
});
var folder = new Folder({
account: account
});
account.addFolder(folder);
BackgroundSyncService.sync(account).then(function() {
expect(Radio.sync.request).toHaveBeenCalledWith('sync:folder', folder);
expect(Radio.ui.trigger).
toHaveBeenCalledWith('notification:mail:show', []);
done();
}).catch(done.fail);
});
});
});

Просмотреть файл

@ -1,496 +0,0 @@
/* global sinon, expect */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'service/foldersyncservice',
'models/account',
'models/accountcollection',
'models/folder',
'models/message',
'state'
], function(FolderSyncService, Account, AccountCollection, Folder, Message,
State) {
describe('FolderSyncService', function() {
var server;
beforeEach(function() {
State.accounts = new AccountCollection();
server = sinon.fakeServer.create();
});
afterEach(function() {
State.accounts = new AccountCollection();
server.restore();
});
it('syncs the sync token of a single folder', function(done) {
var account = new Account({
accountId: 15
});
var folder = new Folder({
id: 'SU5CT1g=',
syncToken: 'oldToken',
account: account
});
folder.addMessage(new Message({
id: 123
}));
folder.addMessage(new Message({
id: 124
}));
var syncing = FolderSyncService.syncFolder(folder);
expect(server.requests.length).toBe(1);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
token: 'newToken',
newMessages: [
{
id: 125
}
],
changedMessages: [],
vanishedMessages: []
})
);
syncing.then(function() {
expect(folder.messages.pluck('id')).toEqual([123, 124, 125]);
done();
}).catch(function(e) {
console.error(e);
done.fail(e);
});
});
it('syncs new messages in a single folder', function(done) {
var account = new Account({
accountId: 15
});
var folder = new Folder({
id: 'SU5CT1g=',
syncToken: 'oldToken',
account: account
});
folder.addMessage(new Message({
id: 123
}));
folder.addMessage(new Message({
id: 124
}));
var syncing = FolderSyncService.syncFolder(folder);
expect(server.requests.length).toBe(1);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
token: 'newToken',
newMessages: [
{
id: 125
}
],
changedMessages: [],
vanishedMessages: []
})
);
syncing.then(function() {
expect(folder.messages.pluck('id')).toEqual([123, 124, 125]);
done();
}).catch(function(e) {
console.error(e);
done.fail(e);
});
});
it('syncs changed messages in a single folder', function(done) {
var account = new Account({
accountId: 15
});
var folder = new Folder({
id: 'SU5CT1g=',
syncToken: 'oldToken',
account: account
});
folder.addMessage(new Message({
id: 123,
subject: 'old subject'
}));
folder.addMessage(new Message({
id: 124
}));
var syncing = FolderSyncService.syncFolder(folder);
expect(server.requests.length).toBe(1);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
token: 'newToken',
newMessages: [],
changedMessages: [
{
id: 123,
subject: 'new subject'
}
],
vanishedMessages: []
})
);
syncing.then(function() {
expect(folder.messages.get(123).
get('subject')).toEqual('new subject');
done();
}).catch(function(e) {
console.error(e);
done.fail(e);
});
});
it('syncs vanished messages in a single folder', function(done) {
var account = new Account({
accountId: 15
});
var folder = new Folder({
id: 'SU5CT1g=',
syncToken: 'oldToken',
account: account
});
folder.addMessage(new Message({
id: 123,
subject: 'old subject'
}));
folder.addMessage(new Message({
id: 124
}));
var syncing = FolderSyncService.syncFolder(folder);
expect(server.requests.length).toBe(1);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
token: 'newToken',
newMessages: [],
changedMessages: [],
vanishedMessages: [
123
]
})
);
syncing.then(function() {
expect(folder.messages.pluck('id')).toEqual([124]);
done();
}).catch(function(e) {
console.error(e);
done.fail(e);
});
});
it('syncs the unified inbox, even if no accounts are configured', function(
done) {
var account = new Account({
accountId: -1,
isUnified: true
});
var folder = new Folder({
account: account
});
var syncing = FolderSyncService.syncFolder(folder);
expect(server.requests.length).toBe(0);
syncing.then(done).catch(done.fail);
});
describe('unified inbox with two accounts and three inboxes', function() {
var account, acc1, acc2;
var folder, folder11, folder12, folder21, folder22;
beforeEach(function() {
account = new Account({
accountId: -1,
isUnified: true
});
folder = new Folder({
account: account
});
acc1 = new Account({
accountId: 1
});
folder11 = new Folder({
id: 'inbox11',
specialRole: 'inbox'
});
folder12 = new Folder({
specialRole: 'sent'
});
acc2 = new Account({
accountId: 2
});
folder21 = new Folder({
id: 'inbox21',
specialRole: 'inbox'
});
folder22 = new Folder({
id: 'inbox22',
specialRole: 'inbox'
});
account.addFolder(folder);
acc1.addFolder(folder11);
acc1.addFolder(folder12);
acc2.addFolder(folder21);
acc2.addFolder(folder22);
State.accounts.add(account);
State.accounts.add(acc1);
State.accounts.add(acc2);
});
it('syncs all changes', function(done) {
// Add some messages
var message211 = new Message({
id: 234,
subject: 'old sub',
account: acc2
});
folder21.addMessage(message211);
folder.addMessage(folder21.messages.get(234));
var message221 = new Message({
id: 345,
account: acc2
});
folder22.addMessage(message221);
folder.addMessage(folder22.messages.get(345));
var syncing = FolderSyncService.syncFolder(folder);
expect(server.requests.length).toBe(3);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
token: 'newToken',
newMessages: [
{
id: 123
}
],
changedMessages: [],
vanishedMessages: []
})
);
server.requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
token: 'newToken',
newMessages: [],
changedMessages: [
{
id: 234,
subject: 'new sub'
}
],
vanishedMessages: []
})
);
server.requests[2].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
token: 'newToken',
newMessages: [],
changedMessages: [],
vanishedMessages: [
345
]
})
);
syncing.then(function() {
// New message saved to first inbox
expect(folder11.messages.pluck('id')).toEqual([123]);
// Update applied in second inbox
expect(folder21.messages.get(234).get('subject')).toEqual('new sub');
// Vanished message in third inbox is removed
expect(folder22.messages.pluck('id')).toEqual([]);
done();
}).catch(done.fail);
});
it('removes vanished messages', function(done) {
// Add some messages
var message211 = new Message({
id: 234,
subject: 'Message 1',
account: acc2
});
folder21.addMessage(message211);
folder.addMessage(message211);
// Check initial state
expect(folder21.messages.pluck('id')).toEqual([234]);
expect(folder.messages.pluck('id')).toEqual([234]);
var syncing = FolderSyncService.syncFolder(folder);
expect(server.requests.length).toBe(3);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
token: 'newToken',
newMessages: [],
changedMessages: [],
vanishedMessages: []
}));
server.requests[1].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
token: 'newToken',
newMessages: [],
changedMessages: [],
vanishedMessages: [
234
]
}));
server.requests[2].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
token: 'newToken',
newMessages: [],
changedMessages: [],
vanishedMessages: []
}));
syncing.then(function() {
// Vanished message in third inbox is removed
expect(folder22.messages.pluck('id')).toEqual([]);
// Message was also removed from the unified folder
expect(folder.messages.pluck('id')).toEqual([]);
done();
}).catch(done.fail);
});
});
it('syncs the unified inbox when an individual one changes', function(done) {
var account = new Account({
accountId: -1,
isUnified: true
});
var folder = new Folder({
account: account
});
var acc1 = new Account({
accountId: 1
});
var folder11 = new Folder({
specialRole: 'inbox'
});
var folder12 = new Folder({
specialRole: 'sent'
});
var acc2 = new Account({
accountId: 2
});
var folder21 = new Folder({
specialRole: 'inbox'
});
var folder22 = new Folder({
specialRole: 'inbox'
});
account.addFolder(folder);
acc1.addFolder(folder11);
acc1.addFolder(folder12);
acc2.addFolder(folder21);
acc2.addFolder(folder22);
State.accounts.add(account);
State.accounts.add(acc1);
State.accounts.add(acc2);
var syncing = FolderSyncService.syncFolder(folder11);
expect(server.requests.length).toBe(1);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
token: 'newToken',
newMessages: [
{
id: 123
}
],
changedMessages: [],
vanishedMessages: []
})
);
syncing.then(function() {
// New message saved to first inbox
expect(folder11.messages.pluck('id')).toEqual([123]);
// Unified inbox was updated too
expect(folder.messages.pluck('id')).toEqual([123]);
done();
}).catch(done.fail);
});
});
});

Просмотреть файл

@ -1,198 +0,0 @@
/* global sinon */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'underscore',
'service/messageservice',
'models/account',
'models/accountcollection',
'models/folder',
'models/message',
'state'
], function(_, MessageService, Account, AccountCollection, Folder, Message,
State) {
'use strict';
describe('MessageService', function() {
var server;
beforeEach(function() {
State.accounts = new AccountCollection();
server = sinon.fakeServer.create();
});
afterEach(function() {
State.accounts = new AccountCollection();
server.restore();
});
function getTestAccounts(nrAccounts) {
var unifiedAccount = new Account({
accountId: -1,
isUnified: true
});
var unifiedInbox = new Folder({
id: 'inbox',
specialRole: 'inbox',
account: unifiedAccount
});
unifiedAccount.addFolder(unifiedInbox);
State.accounts.add(unifiedAccount);
return _.range(1, nrAccounts + 1).map(function(id) {
var account = new Account({
accountId: id
});
var inbox = new Folder({
id: 'inbox',
specialRole: 'inbox',
account: account
});
var otherFolder = new Folder({
id: 'something',
account: account
});
account.addFolder(inbox);
account.addFolder(otherFolder);
State.accounts.add(account);
return account;
});
}
function createMessages(count) {
return _.range(count).map(function(id) {
return new Message({
id: id * 100,
dateInt: id * 1000
});
});
}
it('fetches the next page of an individual folder', function(done) {
var account = new Account();
var folder = new Folder({
id: 'XYZ'
});
account.addFolder(folder);
var fetching = MessageService.getNextMessagePage(account, folder);
expect(server.requests.length).toBe(1);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify([
{
id: 123,
subject: 'hello'
}
])
);
expect(server.requests.length).toBe(1);
fetching.then(function(messages) {
expect(messages.length).toBe(1);
done();
}).catch(done.fail);
});
it('propagates fetch errors', function(done) {
var account = new Account();
var folder = new Folder({
id: 'XYZ',
account: account
});
account.addFolder(folder);
var fetching = MessageService.getNextMessagePage(account, folder);
expect(server.requests.length).toBe(1);
server.requests[0].respond(
500,
{
'Content-Type': 'application/json'
});
fetching.then(done.catch).catch(done);
});
it('loads unified inbox page and uses local data if enough data is available', function(
done) {
var testAccounts = getTestAccounts(2);
var unifiedAccount = State.accounts.get(-1);
var unifiedInbox = unifiedAccount.folders.first();
var inbox1 = testAccounts[0].folders.first();
var inbox2 = testAccounts[1].folders.first();
inbox1.addMessages(createMessages(25));
inbox2.addMessages(createMessages(25));
var fetching = MessageService.getNextMessagePage(unifiedAccount, unifiedInbox);
expect(server.requests.length).toBe(0);
fetching.then(function() {
expect(unifiedInbox.messages.length).toBe(20);
done();
}).catch(done.fail);
});
it('loads unified inbox page and fetches pages where necessary', function(
done) {
var testAccounts = getTestAccounts(2);
var unifiedAccount = State.accounts.get(-1);
var unifiedInbox = unifiedAccount.folders.first();
var inbox1 = testAccounts[0].folders.first();
var inbox2 = testAccounts[1].folders.first();
inbox1.addMessages(createMessages(25));
inbox2.addMessages(createMessages(5));
var fetching = MessageService.getNextMessagePage(unifiedAccount, unifiedInbox);
expect(server.requests.length).toBe(1);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify([
{
id: 123,
subject: 'hello',
dateInt: 26000
}
])
);
fetching.then(function() {
expect(unifiedInbox.messages.length).toBe(20);
done();
}).catch(done.fail);
});
});
});

Просмотреть файл

@ -1,92 +0,0 @@
/* global sinon, expect */
/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define([
'service/preferenceservice'
], function(PreferenceService) {
'use strict';
describe('PreferenceService', function() {
var server;
beforeEach(function() {
server = sinon.fakeServer.create();
});
afterEach(function() {
server.restore();
});
it('retrieves a preference from the back-end', function(done) {
var retrieving = PreferenceService.getPreference('test');
expect(server.requests.length).toBe(1);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
value: '123'
})
);
retrieving.then(function(value) {
expect(value).toBe('123');
done();
}).catch(done.fail);
});
it('retrieves an error from the back-end', function(done) {
var retrieving = PreferenceService.getPreference('test');
expect(server.requests.length).toBe(1);
server.requests[0].respond(500, null, null);
retrieving.then(done.fail).catch(done);
});
it('stores a preference on the back-end', function(done) {
var storing = PreferenceService.savePreference('test', '123');
expect(server.requests.length).toBe(1);
server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
value: '123'
})
);
storing.then(function(value) {
expect(value).toBe('123');
done();
}).catch(done.fail);
});
});
});

Просмотреть файл

@ -1,82 +0,0 @@
/* global spyOn, expect */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'service/accountservice',
'OC'
], function(AccountService, OC) {
describe('AccountService', function() {
beforeEach(function() {
jasmine.Ajax.install();
});
afterEach(function() {
jasmine.Ajax.uninstall();
});
it('creates a new account on the server', function(done) {
spyOn(OC, 'generateUrl').and.returnValue('index.php/apps/mail/api/accounts');
var promise = AccountService.createAccount({
email: 'email@example.com',
password: '12345'
});
expect(OC.generateUrl).toHaveBeenCalledWith('apps/mail/api/accounts');
expect(jasmine.Ajax.requests.count())
.toBe(1);
expect(jasmine.Ajax.requests.mostRecent().url)
.toBe('index.php/apps/mail/api/accounts');
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
'contentType': 'application/json',
'responseText': '{}'
});
promise.then(done).catch(done.fail);
});
it('handle account creation errors correctly', function(done) {
spyOn(OC, 'generateUrl').and.returnValue('index.php/apps/mail/api/accounts');
var creating = AccountService.createAccount({
email: 'email@example.com',
password: '12345'
});
expect(OC.generateUrl).toHaveBeenCalledWith('apps/mail/api/accounts');
expect(jasmine.Ajax.requests.count()).toBe(1);
expect(jasmine.Ajax.requests.mostRecent().url)
.toBe('index.php/apps/mail/api/accounts');
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 500,
'contentType': 'application/json',
'responseText': '{}'
});
creating.catch(done);
});
});
});

Просмотреть файл

@ -1,124 +0,0 @@
/**
* @author Luc Calaresu <dev@calaresu.com>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'service/attachmentservice',
'OC',
'radio',
], function(AttachmentService, OC, Radio) {
describe('AttachmentService', function() {
var fakeModel;
var fakeFile;
beforeEach(function() {
jasmine.Ajax.install();
fakeModel = jasmine.createSpyObj('fakeModel', ['onProgress', 'set', 'get', 'unset']);
fakeFile = {filename: 'file.zip', size: '12345' };
});
afterEach(function() {
jasmine.Ajax.uninstall();
});
/* when everything went as expected on attachment upload */
it('uploads a new attachment on the server', function(done) {
spyOn(OC, 'generateUrl').and.returnValue('index.php/apps/mail/api/attachments');
jasmine.Ajax.stubRequest('index.php/apps/mail/api/attachments').andReturn({
status: 200,
contentType: 'application/json',
responseText: '{"id": "33", "fileName": "file.zip"}'
});
var promise = AttachmentService.uploadLocalAttachment(fakeFile, fakeModel);
expect(OC.generateUrl).toHaveBeenCalledWith('/apps/mail/api/attachments');
promise
.then(function() {
expect(jasmine.Ajax.requests.count()).toBe(1);
expect(jasmine.Ajax.requests.mostRecent().url).toBe('index.php/apps/mail/api/attachments');
done();
})
.catch(function(error) {
console.error(error);
done.fail('Attachment upload is not supposed to fail', error);
});
});
/* when an error occurred on server side on attachment upload */
it('handles errors during attachment uploads', function(done) {
spyOn(OC, 'generateUrl').and.returnValue('index.php/apps/mail/api/attachments');
jasmine.Ajax.stubRequest('index.php/apps/mail/api/attachments').andReturn({
status: 500,
contentType: 'text/plain'
});
var promise = AttachmentService.uploadLocalAttachment(fakeFile, fakeModel);
expect(OC.generateUrl).toHaveBeenCalledWith('/apps/mail/api/attachments');
promise
.then(function() {
done.fail('Attachment upload is supposed to fail');
})
.catch(function() {
expect(jasmine.Ajax.requests.count()).toBe(1);
expect(jasmine.Ajax.requests.mostRecent().url).toBe('index.php/apps/mail/api/attachments');
done();
});
});
/* when upload finished with success */
it('handles upload finished with success', function() {
// progress=1, for upload succeeded
fakeModel.progress = 1;
AttachmentService.uploadLocalAttachmentFinished(fakeModel, 33);
// the model ID and upload status are updated on succes
expect(fakeModel.set).toHaveBeenCalledWith('id', 33);
expect(fakeModel.set).toHaveBeenCalledWith('uploadStatus', 3);
// make sure the xhr request has been removed from the model
expect(fakeModel.unset).toHaveBeenCalledWith('uploadRequest');
});
/* when a problem occured on upload */
it('handles upload finished with error', function() {
// progress=1, but no id returned from the server
fakeModel.progress = 1;
AttachmentService.uploadLocalAttachmentFinished(fakeModel);
// the upload status are updated on succes
expect(fakeModel.set).toHaveBeenCalledWith('uploadStatus', 2);
// make sure the xhr request has been removed from the model
expect(fakeModel.unset).toHaveBeenCalledWith('uploadRequest');
// == id returned from server but file not uploaded ==
// not sure that is possible though
fakeModel.progress = 0.5;
AttachmentService.uploadLocalAttachmentFinished(fakeModel, 33);
// the upload status are updated on succes
expect(fakeModel.set).toHaveBeenCalledWith('uploadStatus', 2);
// make sure the xhr request has been removed from the model
expect(fakeModel.unset).toHaveBeenCalledWith('uploadRequest');
});
});
});

Просмотреть файл

@ -1,75 +0,0 @@
var allTestFiles = [];
var TEST_REGEXP = /(spec|test)\.js$/i;
// Get a list of all the test files to include
Object.keys(window.__karma__.files).forEach(function(file) {
if (TEST_REGEXP.test(file)) {
// Normalize paths to RequireJS module names.
// If you require sub-dependencies of test files to be loaded as-is (requiring file extension)
// then do not normalize the paths
var normalizedTestModule = file.replace(/^\/base\/js\/|\.js$/g, '');
if (normalizedTestModule.substring(0, 'tests'.length) === 'tests') {
allTestFiles.push(normalizedTestModule);
}
}
});
window.t = function(app, text) {
if (app !== 'mail') {
throw 'wrong app used to for translation';
}
return text;
};
OC = {
Notification: {
showTemporary: function() {
}
},
filePath: function(app, type, path) {
return type + '/' + path;
},
generateUrl: function(url, params) {
var props = [];
for (var prop in params) {
props.push(prop);
}
return '/base/' + props.reduce(function(url, paramName) {
var param = params[paramName];
return url.replace('{' + paramName + '}', param);
}, url);
},
linkToRemote: function() {
}
};
OCA = {};
OCA.Search = function() {
};
// jQuery module stubs
$.fn.tooltip = function() {
};
$.fn.droppable = function() {
};
$.fn.imageplaceholder = function() {
};
formatDate = function(arg) {
return arg;
};
relative_modified_date = function(arg) {
return arg;
};
// es6-object-assign
window.ObjectAssign.polyfill();

Просмотреть файл

@ -1,27 +0,0 @@
/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define([
'app'
], function() {
console.info('Mail initialized for testing');
});

Просмотреть файл

@ -0,0 +1,8 @@
module.exports = {
env: {
mocha: true
},
rules: {
'import/no-extraneous-dependencies': 'off'
}
}

Просмотреть файл

@ -0,0 +1,13 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).to.include(msg)
})
})

Просмотреть файл

@ -1,111 +0,0 @@
/* global expect */
/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define([
'util/htmlhelper'
], function(HtmlHelper) {
'use strict';
describe('HtmlHelper', function() {
it('preserves breaks', function() {
var html = 'line1<br>line2';
var expected = 'line1\nline2';
var actual = HtmlHelper.htmlToText(html);
expect(actual).toBe(expected);
});
it('concats divs', function() {
var html = '<div>one</div><div>two</div>';
var expected = 'onetwo';
var actual = HtmlHelper.htmlToText(html);
expect(actual).toBe(expected);
});
it('does not produce large number of line breaks for nested elements', function() {
var html =
'<div>' +
' <div>' +
' line1' +
' </div>' +
'</div>' +
'<div>line2</div>';
var expected = ' line1 line2';
var actual = HtmlHelper.htmlToText(html);
expect(actual).toBe(expected);
});
it('converts blocks to text', function() {
var html = '<div>hello</div>';
var expected = 'hello';
var actual = HtmlHelper.htmlToText(html);
expect(actual).toBe(expected);
});
it('converts paragraphs to text', function() {
var html = '<p>hello</p>';
var expected = 'hello';
var actual = HtmlHelper.htmlToText(html);
expect(actual).toBe(expected);
});
it('converts lists to text', function() {
var html = '<ul><li>one</li><li>two</li><li>three</li></ul>';
var expected = ' * one\n * two\n * three';
var actual = HtmlHelper.htmlToText(html);
expect(actual).toBe(expected);
});
it('converts deeply nested elements to text', function() {
var html = '<html>'
+ '<body><p>Hello!</p><p>this <i>is</i> <b>some</b> random <strong>text</strong></p></body>'
+ '</html>';
var expected = 'Hello!\n\nthis is some random text';
var actual = HtmlHelper.htmlToText(html);
expect(actual).toBe(expected);
});
it('does not leak internal redirection URLs', function() {
var html = '<a href="https://localhost/apps/mail/redirect?src=domain.tld">domain.tld</a>';
var expected = 'domain.tld';
var actual = HtmlHelper.htmlToText(html);
expect(actual).toBe(expected);
});
});
});

Просмотреть файл

@ -1,95 +0,0 @@
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'util/notificationhandler',
'radio'
], function(NotificationHandler, Radio) {
describe('NotificationHandler', function() {
var notification;
var originalNotification;
function getNotificationMock() {
var mock = function(title, options) {
this.title = title;
this.options = options;
// Hack to get ref to notification object
notification = this;
};
mock.requestPermission = function() {
};
mock.prototype = {
getTitle: function() {
return this.title;
},
getOptions: function() {
return this.options;
},
close: function() {
}
};
return mock;
}
beforeEach(function() {
originalNotification = window.Notification;
jasmine.clock().install();
});
afterEach(function() {
window.Notification = originalNotification;
jasmine.clock().uninstall();
notification = undefined;
});
it('requests notification permissions', function() {
window.Notification = getNotificationMock();
spyOn(window.Notification, 'requestPermission');
Radio.ui.trigger('notification:request');
expect(window.Notification.requestPermission).toHaveBeenCalled();
});
it('should do nothing if notifications are not supported by the browser', function() {
window.Notification = undefined;
NotificationHandler.showNotification('a', 'b');
expect(notification).toBe(undefined);
});
it('should construct and show a new notification', function() {
window.Notification = getNotificationMock();
spyOn(Radio.navigation, 'trigger');
NotificationHandler.showNotification('a', 'b');
// A new notification should have been created
expect(notification).not.toBe(undefined);
// Check click handler
expect(Radio.navigation.trigger).not.toHaveBeenCalled();
expect(notification.onclick).not.toBe(undefined);
});
});
});

Просмотреть файл

@ -1,43 +0,0 @@
/* global expect */
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2017
*/
define([
'views/accountview',
'models/account',
], function(AccountView, Account) {
describe('Account view', function() {
var account;
var accountView;
beforeEach(function() {
account = new Account();
accountView = new AccountView({
model: account
});
});
it('has a delete button if the account is deletable', function() {
accountView.render();
expect(accountView.$el.html()).toContain('Delete');
});
it('has no delete button if the account is not deletable', function() {
account.set('accountId', -2);
accountView.render();
expect(accountView.$el.html()).not.toContain('Delete');
});
});
});

Просмотреть файл

@ -1,128 +0,0 @@
/* global expect */
/**
* @author Luc Calaresu <dev@calaresu.com>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'views/attachmentview',
'models/localattachment',
'models/attachment',
'radio',
'jquery-ui'
], function(AttachmentView, LocalAttachment, Attachment, Radio) {
describe('AttachmentView', function() {
var view;
var model;
describe('on local attachment', function() {
beforeEach(function() {
// on local attachment, we use the LocalAttachment model
model = new LocalAttachment({
fileName: 'test.zip',
uploadRequest: true
});
view = new AttachmentView({
model: model
});
view.render();
view.bindUIElements();
});
it('should exist', function() {
expect(view).toBeDefined();
});
it('should have a progress bar set at 0', function() {
// jquery-ui progressbar is initialised
expect(view.ui.attachmentName.attr('role')).toBe('progressbar');
// value of progressbar is set to 0
expect(view.ui.attachmentName.attr('aria-valuenow')).toBe('0');
});
it('should allow attachment removal', function() {
/* We just test that the 'upload:abort' event has been sent */
/* The deletion action itself is tested on the attachmentService tests */
spyOn(Radio.attachment, 'request');
view.removeAttachment();
expect(Radio.attachment.request).toHaveBeenCalledWith('upload:abort', model);
});
it('should update the attachment colour on upload status changes', function() {
// Update the model progress value and make sure the css class has been added
model.set('uploadStatus', 1); // uploading
expect(view.ui.attachmentName.attr('class')).toContain('upload-ongoing');
model.set('uploadStatus', 2); // error
expect(view.ui.attachmentName.attr('class')).not.toContain('upload-ongoing');
expect(view.ui.attachmentName.attr('class')).toContain('upload-warning');
model.set('uploadStatus', 3); // success
expect(view.ui.attachmentName.attr('class')).not.toContain('upload-ongoing');
expect(view.ui.attachmentName.attr('class')).not.toContain('upload-warning');
});
it('should update the progress bar on model progress changes', function() {
spyOn(view.ui.attachmentName, 'progressbar');
// Update the model progress value and make sure the view has been updated
model.set('progress', 0.5);
expect(view.ui.attachmentName.progressbar).toHaveBeenCalledWith(
'option', 'value', 0.5
);
});
});
describe('on attachment from Files', function() {
beforeEach(function() {
// on attachment from Files, we use the Attachment model
model = new Attachment({
fileName: 'test.zip'
});
view = new AttachmentView({
model: model
});
view.render();
});
it('should exist', function() {
expect(view).toBeDefined();
});
it('should not have a progress bar', function() {
// jquery-ui progressbar is NOT initialised
expect(view.ui.attachmentName.attr('role')).toBe(undefined);
// and css style upload-ongoing has NOT been added
expect(view.ui.attachmentName.attr('class')).not.toContain('upload-ongoing');
});
it('should allow attachment removal', function() {
/* We just test that the 'upload:abort' event has been sent */
/* The deletion action itself is tested on the attachmentService tests */
spyOn(Radio.attachment, 'request');
view.removeAttachment();
expect(Radio.attachment.request).toHaveBeenCalledWith('upload:abort', model);
});
});
});
});

Просмотреть файл

@ -1,46 +0,0 @@
/* global expect */
/**
* Mail
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Christoph Wurst 2017
*/
define([
'views/attachmentsview',
'jquery'
], function(AttachmentView, $) {
describe('AttachmentsView', function() {
var view;
beforeEach(function() {
$('body').append('<div id="#mail-attachments-template"></div>');
view = new AttachmentView({});
});
afterEach(function() {
view.remove();
$('#mail-attachments-template').remove();
});
it('produces the correct HTML', function() {
view.render();
expect(view.el.innerHTML)
.toContain('<ul></ul>\n\
<button type="button" id="add-local-attachment" style="display: inline-block;">\n\
<span class="icon-upload"></span> Upload attachment\n\
</button>\n\
<button type="button" id="add-cloud-attachment" style="display: inline-block;">\n\
<span class="icon-folder"></span> Add attachment from Files\n\
</button>\n\
<input type="file" multiple="" id="local-attachments" style="display: none;">\n');
});
});
});

Просмотреть файл

@ -1,40 +0,0 @@
/**
* @author Steffen Lindner <mail@steffen-lindner.de>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(['views/calendarview', 'views/helper'], function(CalendarView) {
describe('CalendarView', function () {
var calendarview;
beforeEach(function () {
calendarview = new CalendarView({});
});
it('produces the correct HTML', function () {
calendarview.render();
html = calendarview.el.innerHTML.trim();
expected_html = '<a class="select-calendar" data-calendar-url=""></a>';
expect(html).toContain(expected_html);
});
});
});

Просмотреть файл

@ -1,275 +0,0 @@
/* global expect, spyOn */
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define([
'views/composerview',
'models/accountcollection',
'models/attachment',
'models/localattachment',
'state'
], function(ComposerView, AccountCollection, Attachment, LocalAttachment, State) {
describe('ComposerView', function() {
var accounts;
beforeEach(function() {
accounts = new AccountCollection([
{
accountId: 13,
name: 'Jane Nextcloud',
emailAddress: 'jane@nextcloud.com',
folders: [
{
id: 'inbox'
}
]
},
{
accountId: 14,
name: 'John Nextcloud',
emailAddress: 'john@nextcloud.com',
folders: [
{
id: 'inbox'
}
]
}
]);
State.currentAccount = accounts.first();
});
it('creates a view to composer a new message', function() {
var view = new ComposerView({
accounts: accounts
});
spyOn(view, 'saveDraft');
expect(view.type).toBe('new');
expect(view.isReply()).toBe(false);
expect(view.account).toBe(accounts.at(0));
expect(view.repliedMessage).toBeNull();
});
it('creates a reply composer', function() {
var account = accounts.at(1);
var folder = account.folders.first();
var view = new ComposerView({
accounts: accounts,
account: account,
folder: folder,
type: 'reply'
});
spyOn(view, 'saveDraft');
expect(view.type).toBe('reply');
expect(view.isReply()).toBe(true);
expect(view.account).toBe(account);
expect(view.folder).toBe(folder);
});
it('doesn\'t have draft UID at creation', function() {
var view = new ComposerView({
accounts: accounts
});
spyOn(view, 'saveDraft');
expect(view.draftUID).toBeUndefined();
});
it('creates the correct list of selectable accounts withoug aliases', function() {
var view = new ComposerView({
accounts: accounts
});
spyOn(view, 'saveDraft');
var expected = [
{
id: 1,
accountId: 13,
aliasId: null,
emailAddress: 'jane@nextcloud.com',
name: 'Jane Nextcloud'
},
{
id: 2,
accountId: 14,
aliasId: null,
emailAddress: 'john@nextcloud.com',
name: 'John Nextcloud'
}
];
expect(view.buildAliases()).toEqual(expected);
});
it('renders correctly', function() {
var view = new ComposerView({
accounts: accounts
});
spyOn(view, 'saveDraft');
view.render();
var $el = view.$el;
// Two accounts should be selectable
expect($el.find('select.mail-account').
children().length).toBe(2);
});
describe('with attachments', function () {
var view;
var localAttachment;
beforeEach(function() {
var accounts = new AccountCollection([
{
accountId: 13,
name: 'Jane Nextcloud',
emailAddress: 'jane@nextcloud.com',
folders: [
{
id: 'inbox'
}
]
}
]);
State.currentAccount = accounts.first();
view = new ComposerView({
accounts: accounts
});
spyOn(view, 'saveDraft');
view.render();
view.bindUIElements();
localAttachment = new LocalAttachment({
fileName: 'test.zip'
});
});
it('calls onInputChanged and checkAllAttachmentsValid when the attachment list changed', function() {
spyOn(view, 'onInputChanged');
spyOn(view, 'checkAllAttachmentsValid');
view.bindAttachments();
view.attachments.add(localAttachment);
expect(view.onInputChanged).toHaveBeenCalled();
expect(view.checkAllAttachmentsValid).toHaveBeenCalled();
});
it('disables the send button when a local attachment is not valid', function() {
// we put something into the 'To:' input, so the submit button can be activated
view.$('.to').val('test@mailserver');
view.onInputChanged();
// and we chack how the button reacts when checkAllAttachmentsValid return false
spyOn(view, 'checkAllAttachmentsValid').and.returnValue(false);
expect(view.$('.submit-message').attr('disabled')).toBe(undefined);
view.attachments.add(localAttachment);
expect(view.$('.submit-message').attr('disabled')).toBe('disabled');
});
it('enables the send button when all local attachment are valid', function() {
// we put something into the 'To:' input, so the submit button can be activated
view.$('.to').val('test@mailserver');
view.onInputChanged();
// and we chack how the button reacts when checkAllAttachmentsValid return false
spyOn(view, 'checkAllAttachmentsValid').and.returnValue(true);
expect(view.$('.submit-message').attr('disabled')).toBe(undefined);
view.attachments.add(localAttachment);
expect(view.$('.submit-message').attr('disabled')).toBe(undefined);
});
describe('from local source (upload)', function () {
it('detects when all local attachments are valid', function() {
var localAttachmentA = new LocalAttachment({
fileName: 'test.zip',
uploadStatus: 3 // success
});
var localAttachmentB = new LocalAttachment({
fileName: 'test2.zip',
uploadStatus: 3 // success
});
view.attachments.add(localAttachmentA);
view.attachments.add(localAttachmentB);
expect(view.checkAllAttachmentsValid()).toBe(true);
});
it('detects when at least one local attachment is pending', function() {
var localAttachmentA = new LocalAttachment({
fileName: 'test.zip',
uploadStatus: 0 // pending
});
var localAttachmentB = new LocalAttachment({
fileName: 'test2.zip',
uploadStatus: 3 // success
});
view.attachments.add(localAttachmentA);
view.attachments.add(localAttachmentB);
expect(view.checkAllAttachmentsValid()).toBe(false);
});
it('detects when at least one local attachment is ongoing', function() {
var localAttachmentA = new LocalAttachment({
fileName: 'test.zip',
uploadStatus: 1 // ongoing
});
var localAttachmentB = new LocalAttachment({
fileName: 'test2.zip',
uploadStatus: 3 // success
});
view.attachments.add(localAttachmentA);
view.attachments.add(localAttachmentB);
expect(view.checkAllAttachmentsValid()).toBe(false);
});
it('detects when at least one local attachment is broken', function() {
var localAttachmentA = new LocalAttachment({
fileName: 'test.zip',
uploadStatus: 2 // error
});
var localAttachmentB = new LocalAttachment({
fileName: 'test2.zip',
uploadStatus: 3 // success
});
view.attachments.add(localAttachmentA);
view.attachments.add(localAttachmentB);
expect(view.checkAllAttachmentsValid()).toBe(false);
});
});
describe('from Files', function () {
it('always consider attachments from Files as valid', function() {
var localAttachmentA = new Attachment({
fileName: 'test.zip'
});
var localAttachmentB = new Attachment({
fileName: 'test2.zip'
});
view.attachments.add(localAttachmentA);
view.attachments.add(localAttachmentB);
expect(view.checkAllAttachmentsValid()).toBe(true);
});
});
});
});
});

Просмотреть файл

@ -1,43 +0,0 @@
/**
* @author Steffen Lindner <mail@steffen-lidnner.de>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
define(['views/emptyfolderview', 'views/helper'], function(EmptyfolderView) {
describe('EmptyfolderView', function () {
var emptyfolderview;
beforeEach(function () {
emptyfolderview = new EmptyfolderView({});
});
describe('Rendering', function () {
it('produces the correct HTML', function () {
emptyfolderview.render();
html = emptyfolderview.el.innerHTML.trim();
expected_html = '<div class="icon-mail"></div>\n<h2>No messages in this folder</h2>';
expect(html).toContain(expected_html);
});
});
});
});

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше