0012950: More attachment methods for mail
[tine20] / tine20 / Felamimail / js / MessageEditDialog.js
1 /*
2  * Tine 2.0
3  * 
4  * @package     Felamimail
5  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
6  * @author      Philipp Schüle <p.schuele@metaways.de>
7  * @copyright   Copyright (c) 2009-2011 Metaways Infosystems GmbH (http://www.metaways.de)
8  */
9  
10 Ext.namespace('Tine.Felamimail');
11
12 /**
13  * @namespace   Tine.Felamimail
14  * @class       Tine.Felamimail.MessageEditDialog
15  * @extends     Tine.widgets.dialog.EditDialog
16  * 
17  * <p>Message Compose Dialog</p>
18  * <p>This dialog is for composing emails with recipients, body and attachments. 
19  * you can choose from which account you want to send the mail.</p>
20  * <p>
21  * TODO         make email note editable
22  * </p>
23  * 
24  * @author      Philipp Schüle <p.schuele@metaways.de>
25  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
26  * 
27  * @param       {Object} config
28  * @constructor
29  * Create a new MessageEditDialog
30  */
31 Tine.Felamimail.MessageEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
32     /**
33      * @cfg {Array/String} bcc
34      * initial config for bcc
35      */
36     bcc: null,
37     
38     /**
39      * @cfg {String} body
40      */
41     msgBody: '',
42     
43     /**
44      * @cfg {Array/String} cc
45      * initial config for cc
46      */
47     cc: null,
48     
49     /**
50      * @cfg {Array} of Tine.Felamimail.Model.Message (optionally encoded)
51      * messages to forward
52      */
53     forwardMsgs: null,
54     
55     /**
56      * @cfg {String} accountId
57      * the accout id this message is sent from
58      */
59     accountId: null,
60     
61     /**
62      * @cfg {Tine.Felamimail.Model.Message} (optionally encoded)
63      * message to reply to
64      */
65     replyTo: null,
66
67     /**
68      * @cfg {Tine.Felamimail.Model.Message} (optionally encoded)
69      * message to use as draft/template
70      */
71     draftOrTemplate: null,
72     
73     /**
74      * @cfg {Boolean} (defaults to false)
75      */
76     replyToAll: false,
77     
78     /**
79      * @cfg {String} subject
80      */
81     subject: '',
82     
83     /**
84      * @cfg {Array/String} to
85      * initial config for to
86      */
87     to: null,
88     
89     /**
90      * validation error message
91      * @type String
92      */
93     validationErrorMessage: '',
94     
95     /**
96      * array with e-mail-addresses used as recipients
97      * @type {Array}
98      */
99     mailAddresses: null,
100     /**
101      * json-encoded selection filter
102      * @type {String} selectionFilter
103      */
104     selectionFilter: null,
105     
106     /**
107      * holds default values for the record
108      * @type {Object}
109      */
110     recordDefaults: null,
111
112     quotedPGPMessage: null,
113
114     /**
115      * @private
116      */
117     windowNamePrefix: 'MessageEditWindow_',
118     appName: 'Felamimail',
119     recordClass: Tine.Felamimail.Model.Message,
120     recordProxy: Tine.Felamimail.messageBackend,
121     loadRecord: false,
122     evalGrants: false,
123     hideAttachmentsPanel: true,
124     
125     bodyStyle:'padding:0px',
126     
127     /**
128      * overwrite update toolbars function (we don't have record grants)
129      * @private
130      */
131     updateToolbars: Ext.emptyFn,
132     
133     //private
134     initComponent: function() {
135         var me = this;
136
137         Tine.Felamimail.MessageEditDialog.superclass.initComponent.call(this);
138
139         Tine.Felamimail.mailvelopeHelper.mailvelopeLoaded.then(function() {
140             me.button_toggleEncrypt.setVisible(true);
141         })['catch'](function() {
142             Tine.log.info('mailvelope not available');
143         });
144     },
145
146
147     /**
148      * init buttons
149      */
150     initButtons: function() {
151         this.fbar = [];
152         
153         this.action_send = new Ext.Action({
154             text: this.app.i18n._('Send'),
155             handler: this.onSaveAndClose,
156             iconCls: 'FelamimailIconCls',
157             disabled: false,
158             scope: this
159         });
160
161         this.action_searchContacts = new Ext.Action({
162             text: this.app.i18n._('Search Recipients'),
163             handler: this.onSearchContacts,
164             iconCls: 'AddressbookIconCls',
165             disabled: false,
166             scope: this
167         });
168         
169         this.action_saveAsDraft = new Ext.Action({
170             text: this.app.i18n._('Save As Draft'),
171             handler: this.onSaveInFolder.createDelegate(this, ['drafts_folder']),
172             iconCls: 'action_saveAsDraft',
173             disabled: false,
174             scope: this
175         });
176
177         this.action_saveAsTemplate = new Ext.Action({
178             text: this.app.i18n._('Save As Template'),
179             handler: this.onSaveInFolder.createDelegate(this, ['templates_folder']),
180             iconCls: 'action_saveAsTemplate',
181             disabled: false,
182             scope: this
183         });
184         
185         // TODO think about changing icon onToggle
186         this.action_saveEmailNote = new Ext.Action({
187             text: this.app.i18n._('Save Email Note'),
188             handler: this.onToggleSaveNote,
189             iconCls: 'notes_noteIcon',
190             disabled: false,
191             scope: this,
192             enableToggle: true
193         });
194         this.button_saveEmailNote = Ext.apply(new Ext.Button(this.action_saveEmailNote), {
195             tooltip: this.app.i18n._('Activate this toggle button to save the email text as a note attached to the recipient(s) contact(s).')
196         });
197
198         this.action_toggleReadingConfirmation = new Ext.Action({
199             text: this.app.i18n._('Reading Confirmation'),
200             handler: this.onToggleReadingConfirmation,
201             iconCls: 'felamimail-action-reading-confirmation',
202             disabled: false,
203             scope: this,
204             enableToggle: true
205         });
206         this.button_toggleReadingConfirmation = Ext.apply(new Ext.Button(this.action_toggleReadingConfirmation), {
207             tooltip: this.app.i18n._('Activate this toggle button to receive a reading confirmation.')
208         });
209
210         this.action_toggleEncrypt = new Ext.Action({
211             text: this.app.i18n._('Encrypt Email'),
212             toggleHandler: this.onToggleEncrypt,
213             iconCls: 'felamimail-action-decrypt',
214             disabled: false,
215             pressed: false,
216             hidden: true,
217             scope: this,
218             enableToggle: true
219         });
220         this.button_toggleEncrypt = Ext.apply(new Ext.Button(this.action_toggleEncrypt), {
221             tooltip: this.app.i18n._('Encrypt email using Mailvelope')
222         });
223
224         this.tbar = new Ext.Toolbar({
225             defaults: {height: 55},
226             items: [{
227                 xtype: 'buttongroup',
228                 columns: 6,
229                 items: [
230                     Ext.apply(new Ext.Button(this.action_send), {
231                         scale: 'medium',
232                         rowspan: 2,
233                         iconAlign: 'top'
234                     }),
235                     Ext.apply(new Ext.Button(this.action_searchContacts), {
236                         scale: 'medium',
237                         rowspan: 2,
238                         iconAlign: 'top',
239                         tooltip: this.app.i18n._('Click to search for and add recipients from the Addressbook.')
240                     }),
241                     this.action_saveAsDraft,
242                     this.button_saveEmailNote,
243                     this.action_saveAsTemplate,
244                     this.button_toggleReadingConfirmation,
245                     this.button_toggleEncrypt
246                 ]
247             }]
248         });
249     },
250
251     
252     /**
253      * @private
254      */
255     initRecord: function() {
256         this.decodeMsgs();
257         
258         this.recordDefaults = Tine.Felamimail.Model.Message.getDefaultData();
259         
260         if (this.mailAddresses) {
261             this.recordDefaults.to = Ext.decode(this.mailAddresses);
262         } else if (this.selectionFilter) {
263             this.on('load', this.fetchRecordsOnLoad, this);
264         }
265         
266         if (! this.record) {
267             this.record = new Tine.Felamimail.Model.Message(this.recordDefaults, 0);
268         }
269         this.initFrom();
270         this.initRecipients();
271         this.initSubject();
272         this.initContent();
273         
274         // legacy handling:...
275         // TODO add this information to attachment(s) + flags and remove this
276         if (this.replyTo) {
277             this.record.set('flags', '\\Answered');
278             this.record.set('original_id', this.replyTo.id);
279         } else if (this.forwardMsgs) {
280             this.record.set('flags', 'Passed');
281             this.record.set('original_id', this.forwardMsgs[0].id);
282         } else if (this.draftOrTemplate) {
283             this.record.set('original_id', this.draftOrTemplate.id);
284         }
285         
286         Tine.log.debug('Tine.Felamimail.MessageEditDialog::initRecord() -> record:');
287         Tine.log.debug(this.record);
288     },
289     
290     /**
291      * show loadMask (loadRecord is false in this dialog)
292      * @param {} ct
293      * @param {} position
294      */
295     onRender : function(ct, position) {
296         Tine.Felamimail.MessageEditDialog.superclass.onRender.call(this, ct, position);
297         this.loadMask.show();
298     },
299
300     isRendered: function() {
301         var me = this;
302         return new Promise(function (fulfill, reject) {
303             if (me.rendered) {
304                 fulfill(true);
305             } else {
306                 me.on('render', fulfill);
307             }
308         });
309     },
310
311     /**
312      * handle attachments: attaches message when forwarding mails or
313      *  keeps attachments as they are (if preference is set or draft/template)
314      *
315      * @param {Tine.Felamimail.Model.Message} message
316      */
317     handleAttachmentsOfExistingMessage: function(message) {
318         if (message.get('attachments').length == 0) {
319             return;
320         }
321
322         var attachments = [];
323         if ((Tine[this.app.appName].registry.get('preferences').get('emlForward')
324                 && Tine[this.app.appName].registry.get('preferences').get('emlForward') == 0)
325             || this.draftOrTemplate
326         ) {
327
328             Ext.each(message.get('attachments'), function(attachment) {
329                 attachment = {
330                     name: attachment['filename'],
331                     type: attachment['content-type'],
332                     size: attachment['size'],
333                     id: message.id + '_' + attachment['partId']
334                 };
335                 attachments.push(attachment);
336             },this);
337
338         } else {
339             attachments = [{
340                 name: message.get('subject'),
341                 type: 'message/rfc822',
342                 size: message.get('size'),
343                 id: message.id
344             }];
345         }
346
347         this.record.set('attachments', attachments);
348     },
349     
350     /**
351      * inits body and attachments from reply/forward/template
352      */
353     initContent: function() {
354         if (! this.record.get('body')) {
355             var account = Tine.Tinebase.appMgr.get('Felamimail').getAccountStore().getById(this.record.get('account_id')),
356                 format = account && account.get('compose_format') != '' ? 'text/' + account.get('compose_format') : 'text/html';
357             
358             if (! this.msgBody) {
359                 var message = this.getMessageFromConfig();
360                 if (message) {
361                     if (message.bodyIsFetched() && account.get('preserve_format')) {
362                         // format of the received message. this is the format to perserve
363                         format = message.get('body_content_type');
364                     }
365                     if (!message.bodyIsFetched() || format != message.getBodyType()) {
366                         // self callback when body needs to be (re) fetched
367                         return this.recordProxy.fetchBody(message, format, this.initContent.createDelegate(this));
368                     }
369
370                     this.setMessageBody(message, account, format);
371
372                     if (this.isForwardedMessage() || this.draftOrTemplate) {
373                         this.handleAttachmentsOfExistingMessage(message);
374                     }
375                 }
376             }
377
378             this.addSignature(account, format);
379
380             this.record.set('content_type', format);
381             this.record.set('body', this.msgBody);
382         }
383
384         if (this.attachments) {
385             this.handleExternalAttachments();
386         }
387
388         delete this.msgBody;
389
390         this.onRecordLoad();
391     },
392
393     /**
394      * handle attachments like external URLs (COSR)
395      *
396      * TODO: check if this overwrites existing attachments in some cases
397      */
398     handleExternalAttachments: function() {
399         this.attachments = Ext.isArray(this.attachments) ? this.attachments : [this.attachments];
400         var attachments = [];
401         Ext.each(this.attachments, function(attachment) {
402
403             // external URL with COSR header enabled
404             if (Ext.isString(attachment)) {
405                 attachment = {
406                     url: attachment
407                 };
408             }
409
410             attachments.push(attachment);
411         }, this);
412
413         this.record.set('attachments', attachments);
414         delete this.attachments;
415     },
416     
417     /**
418      * set message body: converts newlines, adds quotes
419      * 
420      * @param {Tine.Felamimail.Model.Message} message
421      * @param {Tine.Felamimail.Model.Account} account
422      * @param {String}                        format
423      */
424     setMessageBody: function(message, account, format) {
425         var preparedParts = message.get('preparedParts');
426
427         this.msgBody = message.get('body');
428
429         if (preparedParts && preparedParts.length > 0) {
430             if (preparedParts[0].contentType == 'application/pgp-encrypted') {
431                 this.quotedPGPMessage = preparedParts[0].preparedData;
432
433                 this.msgBody = this.msgBody + this.app.i18n._('Encrypted Content');
434
435                 var me = this;
436                 this.isRendered().then(function () {
437                     me.button_toggleEncrypt.toggle();
438                 });
439             }
440         }
441
442         if (this.replyTo) {
443             if (format == 'text/plain') {
444                 this.msgBody = String('> ' + this.msgBody).replace(/\r?\n/g, '\n> ');
445             } else {
446                 this.msgBody = '<br/>'
447                     + '<blockquote class="felamimail-body-blockquote">' + this.msgBody + '</blockquote><br/>';
448             }
449         }
450         this.msgBody = this.getQuotedMailHeader(format) + this.msgBody;
451     },
452
453     /**
454      * returns true if message is forwarded
455      *
456      * @return {Boolean}
457      */
458     isForwardedMessage: function() {
459         return (this.forwardMsgs && this.forwardMsgs.length === 1);
460     },
461
462     /**
463      * add signature to message
464      * 
465      * @param {Tine.Felamimail.Model.Account} account
466      * @param {String} format
467      */
468     addSignature: function(account, format) {
469         if (this.draftOrTemplate) {
470             return;
471         }
472         
473         var accountId = account ? this.record.get('account_id') : Tine.Felamimail.registry.get('preferences').get('defaultEmailAccount'),
474             account = account ? account :this.app.getAccountStore().getById(accountId),
475             signaturePosition = (account && account.get('signature_position')) ? account.get('signature_position') : 'below',
476             signature = this.getSignature(account, format);
477
478         if (signaturePosition == 'below') {
479             this.msgBody += signature;
480         } else {
481             this.msgBody = signature + '<br/><br/>' + this.msgBody;
482         }
483     },
484
485     /**
486      * get account signature
487      *
488      * @param {Tine.Felamimail.Model.Account} account
489      * @param {String} format
490      */
491     getSignature: function(account, format) {
492         var accountId = account ? this.record.get('account_id') : Tine.Felamimail.registry.get('preferences').get('defaultEmailAccount'),
493             account = account ? account :this.app.getAccountStore().getById(accountId),
494             signaturePosition = (account && account.get('signature_position')) ? account.get('signature_position') : 'below',
495             signature = Tine.Felamimail.getSignature(accountId);
496
497         if (format == 'text/plain') {
498             signature = Tine.Tinebase.common.html2text(signature);
499         }
500
501         return signature;
502     },
503
504     /**
505      * inits / sets sender of message
506      */
507     initFrom: function() {
508         if (! this.record.get('account_id')) {
509             if (! this.accountId) {
510                 var message = this.getMessageFromConfig(),
511                     folderId = message ? message.get('folder_id') : null, 
512                     folder = folderId ? Tine.Tinebase.appMgr.get('Felamimail').getFolderStore().getById(folderId) : null,
513                     accountId = folder ? folder.get('account_id') : null;
514                     
515                 if (! accountId) {
516                     var activeAccount = Tine.Tinebase.appMgr.get('Felamimail').getActiveAccount();
517                     accountId = (activeAccount) ? activeAccount.id : null;
518                 }
519                 
520                 this.accountId = accountId;
521             }
522             
523             this.record.set('account_id', this.accountId);
524         }
525         delete this.accountId;
526     },
527     
528     /**
529      * after render
530      */
531     afterRender: function() {
532         Tine.Felamimail.MessageEditDialog.superclass.afterRender.apply(this, arguments);
533         
534         this.getEl().on(Ext.EventManager.useKeydown ? 'keydown' : 'keypress', this.onKeyPress, this);
535         this.recipientGrid.on('specialkey', function(field, e) {
536             this.onKeyPress(e);
537         }, this);
538         
539         this.htmlEditor.on('keydown', function(e) {
540             if (e.getKey() == e.ENTER && e.ctrlKey) {
541                 this.onSaveAndClose();
542             } else if (e.getKey() == e.TAB && e.shiftKey) {
543                 this.subjectField.focus.defer(50, this.subjectField);
544             }
545         }, this);
546
547         this.htmlEditor.on('toggleFormat', this.onToggleFormat, this);
548
549         this.initHtmlEditorDD();
550     },
551     
552     
553     initHtmlEditorDD: function() {
554         return;
555         if (! this.htmlEditor.rendered) {
556             return this.initHtmlEditorDD.defer(500, this);
557         }
558         
559         this.htmlEditor.getDoc().addEventListener('dragover', function(e) {
560             this.action_addAttachment.plugins[0].onBrowseButtonClick();
561         }.createDelegate(this));
562         
563         this.htmlEditor.getDoc().addEventListener('drop', function(e) {
564             this.action_addAttachment.plugins[0].onDrop(Ext.EventObject.setEvent(e));
565         }.createDelegate(this));
566     },
567     
568     /**
569      * on key press
570      * @param {} e
571      * @param {} t
572      * @param {} o
573      */
574     onKeyPress: function(e, t, o) {
575         if ((e.getKey() == e.TAB || e.getKey() == e.ENTER) && ! e.shiftKey) {
576             if (e.getTarget('input[name=subject]')) {
577                 this.htmlEditor.focus.defer(50, this.htmlEditor);
578             } else if (e.getTarget('input[type=text]')) {
579                 this.subjectField.focus.defer(50, this.subjectField);
580             }
581         }
582     },
583     
584     /**
585      * returns message passed with config
586      * 
587      * @return {Tine.Felamimail.Model.Message}
588      */
589     getMessageFromConfig: function() {
590         return this.replyTo ? this.replyTo : 
591                this.forwardMsgs && this.forwardMsgs.length === 1 ? this.forwardMsgs[0] :
592                this.draftOrTemplate ? this.draftOrTemplate : null;
593     },
594     
595     /**
596      * inits to/cc/bcc
597      */
598     initRecipients: function() {
599         if (this.replyTo) {
600             this.initReplyRecipients();
601         }
602         
603         Ext.each(['to', 'cc', 'bcc'], function(field) {
604             if (this.draftOrTemplate) {
605                 this[field] = this.draftOrTemplate.get(field);
606             }
607             
608             if (! this.record.get(field)) {
609                 this[field] = Ext.isArray(this[field]) ? this[field] : Ext.isString(this[field]) ? [this[field]] : [];
610                 this.record.set(field, Ext.unique(this[field]));
611             }
612             delete this[field];
613             
614             this.resolveRecipientFilter(field);
615             
616         }, this);
617     },
618     
619     /**
620      * init recipients from reply/replyToAll information
621      */
622     initReplyRecipients: function() {
623         var replyTo = this.replyTo.get('headers')['reply-to'];
624         
625         if (replyTo) {
626             this.to = replyTo;
627         } else {
628             var toemail = '<' + this.replyTo.get('from_email') + '>';
629             if (this.replyTo.get('from_name') && this.replyTo.get('from_name') != this.replyTo.get('from_email')) {
630                 this.to = this.replyTo.get('from_name') + ' ' + toemail;
631             } else {
632                 this.to = toemail;
633             }
634         }
635         
636         if (this.replyToAll) {
637             if (! Ext.isArray(this.to)) {
638                 this.to = [this.to];
639             }
640             this.to = this.to.concat(this.replyTo.get('to'));
641             this.cc = this.replyTo.get('cc');
642             
643             // remove own email and all non-email strings/objects from to/cc
644             var account = Tine.Tinebase.appMgr.get('Felamimail').getAccountStore().getById(this.record.get('account_id')),
645                 ownEmailRegexp = new RegExp(account.get('email'));
646             Ext.each(['to', 'cc'], function(field) {
647                 for (var i=0; i < this[field].length; i++) {
648                     if (! Ext.isString(this[field][i]) || ! this[field][i].match(/@/) || ownEmailRegexp.test(this[field][i])) {
649                         this[field].splice(i, 1);
650                     }
651                 }
652             }, this);
653         }
654     },
655     
656     /**
657      * resolve recipient filter / queries addressbook
658      * 
659      * @param {String} field to/cc/bcc
660      */
661     resolveRecipientFilter: function(field) {
662         if (! Ext.isEmpty(this.record.get(field)) && Ext.isObject(this.record.get(field)[0]) &&  this.record.get(field)[0].operator) {
663             // found a filter
664             var filter = this.record.get(field);
665             this.record.set(field, []);
666             
667             this['AddressLoadMask'] = new Ext.LoadMask(Ext.getBody(), {msg: this.app.i18n._('Loading Mail Addresses')});
668             this['AddressLoadMask'].show();
669             
670             Tine.Addressbook.searchContacts(filter, null, function(response) {
671                 var mailAddresses = Tine.Felamimail.GridPanelHook.prototype.getMailAddresses(response.results);
672                 
673                 this.record.set(field, mailAddresses);
674                 this.recipientGrid.syncRecipientsToStore([field], this.record, true, false);
675                 this['AddressLoadMask'].hide();
676                 
677             }.createDelegate(this));
678         }
679     },
680     
681     /**
682      * sets / inits subject
683      */
684     initSubject: function() {
685         if (! this.record.get('subject')) {
686             if (! this.subject) {
687                 if (this.replyTo) {
688                     this.setReplySubject();
689                 } else if (this.forwardMsgs) {
690                     this.setForwardSubject();
691                 } else if (this.draftOrTemplate) {
692                     this.subject = this.draftOrTemplate.get('subject');
693                 }
694             }
695             this.record.set('subject', this.subject);
696         }
697         
698         delete this.subject;
699     },
700     
701     /**
702      * setReplySubject -> this.subject
703      * 
704      * removes existing prefixes + just adds 'Re: '
705      */
706     setReplySubject: function() {
707         var replyPrefix = 'Re: ',
708             replySubject = (this.replyTo.get('subject')) ? this.replyTo.get('subject') : '',
709             replySubject = replySubject.replace(/^((re|aw|antw|fwd|odp|sv|wg|tr|rép):\s*)*/i, replyPrefix);
710             
711         this.subject = replySubject;
712     },
713     
714     /**
715      * setForwardSubject -> this.subject
716      */
717     setForwardSubject: function() {
718         this.subject =  this.app.i18n._('Fwd:') + ' ';
719         this.subject += this.forwardMsgs.length === 1 ?
720             this.forwardMsgs[0].get('subject') :
721             String.format(this.app.i18n._('{0} Message', '{0} Messages', this.forwardMsgs.length));
722     },
723     
724     /**
725      * decode this.replyTo / this.forwardMsgs from interwindow json transport
726      */
727     decodeMsgs: function() {
728         if (Ext.isString(this.draftOrTemplate)) {
729             this.draftOrTemplate = new this.recordClass(Ext.decode(this.draftOrTemplate));
730         }
731         
732         if (Ext.isString(this.replyTo)) {
733             this.replyTo = new this.recordClass(Ext.decode(this.replyTo));
734         }
735         
736         if (Ext.isString(this.forwardMsgs)) {
737             var msgs = [];
738             Ext.each(Ext.decode(this.forwardMsgs), function(msg) {
739                 msgs.push(new this.recordClass(msg));
740             }, this);
741             
742             this.forwardMsgs = msgs;
743         }
744     },
745     
746     /**
747      * fix input fields layout
748      */
749     fixLayout: function() {
750         if (! this.subjectField.rendered || ! this.accountCombo.rendered || ! this.recipientGrid.rendered) {
751             return;
752         }
753         
754         var scrollWidth = this.recipientGrid.getView().getScrollOffset();
755         this.subjectField.setWidth(this.subjectField.getWidth() - scrollWidth + 1);
756         this.accountCombo.setWidth(this.accountCombo.getWidth() - scrollWidth + 1);
757     },
758     
759     /**
760      * save message in folder
761      * 
762      * @param {String} folderField
763      */
764     onSaveInFolder: function (folderField) {
765         this.onRecordUpdate();
766         
767         var account = Tine.Tinebase.appMgr.get('Felamimail').getAccountStore().getById(this.record.get('account_id')),
768             folderName = account.get(folderField);
769         
770         Tine.log.debug('onSaveInFolder() - Save message in folder ' + folderName);
771         Tine.log.debug(this.record);
772             
773         if (! folderName || folderName == '') {
774             Ext.MessageBox.alert(
775                 i18n._('Failed'),
776                 String.format(this.app.i18n._('{0} account setting empty.'), folderField)
777             );
778         } else if (this.attachmentGrid.isUploading()) {
779             Ext.MessageBox.alert(
780                 i18n._('Failed'),
781                 this.app.i18n._('Files are still uploading.')
782             );
783         } else {
784             this.loadMask.show();
785             this.recordProxy.saveInFolder(this.record, folderName, {
786                 scope: this,
787                 success: function(record) {
788                     this.fireEvent('update', Ext.util.JSON.encode(this.record.data));
789                     this.purgeListeners();
790                     this.window.close();
791                 },
792                 failure: Tine.Felamimail.handleRequestException.createInterceptor(function() {
793                         this.loadMask.hide();
794                     }, this
795                 ),
796                 timeout: 150000 // 3 minutes
797             });
798         }
799     },
800     
801     /**
802      * toggle save note
803      * 
804      * @param {} button
805      * @param {} e
806      */
807     onToggleSaveNote: function (button, e) {
808         this.record.set('note', (! this.record.get('note')));
809     },
810     
811     /**
812      * toggle Request Reading Confirmation
813      */
814     onToggleReadingConfirmation: function () {
815         this.record.set('reading_conf', (! this.record.get('reading_conf')));
816     },
817
818     onToggleEncrypt: function(btn, e) {
819         btn.setIconClass(btn.pressed ? 'felamimail-action-encrypt' : 'felamimail-action-decrypt');
820
821         var account = Tine.Tinebase.appMgr.get('Felamimail').getAccountStore().getById(this.record.get('account_id')),
822             text = this.bodyCards.layout.activeItem.getValue() || this.record.get('body'),
823             format = this.record.getBodyType(),
824             textEditor = format == 'text/html' ? this.htmlEditor : this.textEditor;
825
826         this.bodyCards.layout.setActiveItem(btn.pressed ? this.mailvelopeWrap : textEditor);
827
828         if (btn.pressed) {
829             var me = this,
830                 textMsg = Tine.Tinebase.common.html2text(text),
831                 quotedMailHeader = '';
832
833             if (this.quotedPGPMessage) {
834                 textMsg = this.getSignature(account, 'text/plain');
835                 quotedMailHeader = Ext.util.Format.htmlDecode(me.getQuotedMailHeader('text/plain'));
836                 quotedMailHeader = quotedMailHeader.replace(/\n/, "\n>");
837
838             }
839
840             Tine.Felamimail.mailvelopeHelper.getKeyring().then(function(keyring) {
841                 mailvelope.createEditorContainer('#' + me.mailvelopeWrap.id, keyring, {
842                     predefinedText: textMsg,
843                     quotedMailHeader: quotedMailHeader,
844                     quotedMail: me.quotedPGPMessage,
845                     keepAttachments: true,
846                     quota: 32*1024*1024
847                 }).then(function(editor) {
848                     me.mailvelopeEditor = editor;
849                 });
850             });
851
852             this.southPanel.collapse();
853             this.southPanel.setVisible(false);
854             this.btnAddAttachemnt.setDisabled(true);
855         } else {
856             this.mailvelopeEditor = null;
857             delete this.mailvelopeEditor;
858             this.mailvelopeWrap.update('');
859
860             this.southPanel.setVisible(true);
861             this.btnAddAttachemnt.setDisabled(false);
862         }
863     },
864
865     /**
866      * toggle format
867      */
868     onToggleFormat: function() {
869         var source = this.bodyCards.layout.activeItem,
870             format = source.mimeType,
871             target = format == 'text/plain' ? this.htmlEditor : this.textEditor,
872             convert = format == 'text/plain' ?
873                 Ext.util.Format.nl2br :
874                 Tine.Tinebase.common.html2text;
875
876         if (format.match(/^text/)) {
877             this.bodyCards.layout.setActiveItem(target);
878             target.setValue(convert(source.getValue()));
879         } else {
880             // ignore toggle request for encrypted content
881         }
882     },
883
884     /**
885      * get quoted mail header
886      *
887      * @param format
888      * @returns {String}
889      */
890     getQuotedMailHeader: function(format) {
891         if (this.replyTo) {
892             var date = (this.replyTo.get('sent'))
893                 ? this.replyTo.get('sent')
894                 : ((this.replyTo.get('received')) ? this.replyTo.get('received') : new Date());
895
896             return String.format(this.app.i18n._('On {0}, {1} wrote'),
897                     Tine.Tinebase.common.dateTimeRenderer(date),
898                     Ext.util.Format.htmlEncode(this.replyTo.get('from_name'))
899                 ) + ':\n';
900         } else if (this.isForwardedMessage()) {
901             return String.format('{0}-----' + this.app.i18n._('Original message') + '-----{1}',
902                     format == 'text/plain' ? '' : '<br /><b>',
903                     format == 'text/plain' ? '\n' : '</b><br />')
904                 + Tine.Felamimail.GridPanel.prototype.formatHeaders(this.forwardMsgs[0].get('headers'), false, true, format == 'text/plain')
905                 + (format == 'text/plain' ? '' : '<br /><br />');
906         }
907
908         return '';
909     },
910
911     /**
912      * search for contacts as recipients
913      */
914     onSearchContacts: function() {
915         Tine.Felamimail.RecipientPickerDialog.openWindow({
916             record: Ext.encode(Ext.copyTo({}, this.record.data, ['subject', 'to', 'cc', 'bcc'])),
917             listeners: {
918                 scope: this,
919                 'update': function(record) {
920                     var messageWithRecipients = Ext.isString(record) ? new this.recordClass(Ext.decode(record)) : record;
921                     this.recipientGrid.syncRecipientsToStore(['to', 'cc', 'bcc'], messageWithRecipients, true, true);
922                 }
923             }
924         });
925     },
926     
927     /**
928      * executed after record got updated from proxy
929      * 
930      * @private
931      */
932     onRecordLoad: function() {
933         // interrupt process flow till dialog is rendered
934         if (! this.rendered) {
935             this.onRecordLoad.defer(250, this);
936             return;
937         }
938         
939         var title = this.app.i18n._('Compose email:'),
940             editor = this.record.get('content_type') == 'text/html' ? this.htmlEditor : this.textEditor;
941
942         if (this.record.get('subject')) {
943             title = title + ' ' + this.record.get('subject');
944         }
945         this.window.setTitle(title);
946
947         if (! this.button_toggleEncrypt.pressed) {
948             editor.setValue(this.record.get('body'));
949             this.bodyCards.layout.setActiveItem(editor);
950         }
951
952         // to make sure we have all recipients (for example when composing from addressbook with "all pages" filter)
953         var ticketFn = this.onAfterRecordLoad.deferByTickets(this),
954         wrapTicket = ticketFn();
955         this.fireEvent('load', this, this.record, ticketFn);
956         wrapTicket();
957         
958         this.getForm().loadRecord(this.record);
959         this.attachmentGrid.loadRecord(this.record);
960         
961         if (this.record.get('note') && this.record.get('note') == '1') {
962             this.button_saveEmailNote.toggle();
963         }
964         
965         this.onAfterRecordLoad();
966     },
967
968     /**
969      * overwrite, just hide the loadMask
970      */
971     onAfterRecordLoad: function() {
972         if (this.loadMask) {
973             this.loadMask.hide();
974         }
975     },
976     
977     /**
978      * executed when record gets updated from form
979      * - add attachments to record here
980      * - add alias / from
981      * 
982      * @private
983      */
984     onRecordUpdate: function() {
985         this.record.data.attachments = [];
986         var attachmentData = null;
987
988         var format = this.bodyCards.layout.activeItem.mimeType;
989         if (format.match(/^text/)) {
990             var editor = format == 'text/html' ? this.htmlEditor : this.textEditor;
991
992             this.record.set('content_type', format);
993             this.record.set('body', editor.getValue());
994         }
995         
996         this.attachmentGrid.store.each(function(attachment) {
997             var fileData = Ext.copyTo({}, attachment.data, ['tempFile', 'name', 'path', 'size', 'type', 'id', 'attachment_type', 'password']);
998             this.record.data.attachments.push(fileData);
999         }, this);
1000         
1001         var accountId = this.accountCombo.getValue(),
1002             account = this.accountCombo.getStore().getById(accountId),
1003             emailFrom = account.get('email');
1004             
1005         this.record.set('from_email', emailFrom);
1006         
1007         Tine.Felamimail.MessageEditDialog.superclass.onRecordUpdate.call(this);
1008
1009         this.record.set('account_id', account.get('original_id'));
1010         
1011         // need to sync once again to make sure we have the correct recipients
1012         this.recipientGrid.syncRecipientsToRecord();
1013     },
1014
1015     /**
1016      * init attachment grid + add button to toolbar
1017      */
1018     initAttachmentGrid: function() {
1019         if (! this.attachmentGrid) {
1020             this.attachmentGrid = new Tine.Felamimail.AttachmentUploadGrid({
1021                 fieldLabel: this.app.i18n._('Attachments'),
1022                 hideLabel: true,
1023                 filesProperty: 'attachments',
1024                 // TODO     think about that -> when we deactivate the top toolbar, we lose the dropzone for files!
1025                 //showTopToolbar: false,
1026                 anchor: '100% 95%'
1027             });
1028             
1029             // add file upload button to toolbar
1030             this.action_addAttachment = this.attachmentGrid.getAddAction();
1031             this.action_addAttachment.plugins[0].dropElSelector = 'div[id=' + this.id + ']';
1032             this.action_addAttachment.plugins[0].onBrowseButtonClick = function() {
1033                 this.southPanel.expand();
1034             }.createDelegate(this);
1035
1036             this.btnAddAttachemnt = new Ext.Button(this.action_addAttachment);
1037             this.tbar.get(0).insert(1, Ext.apply(this.btnAddAttachemnt, {
1038                 scale: 'medium',
1039                 rowspan: 2,
1040                 iconAlign: 'top'
1041             }));
1042         }
1043     },
1044     
1045     /**
1046      * init account (from) combobox
1047      * 
1048      * - need to create a new store with an account record for each alias
1049      */
1050     initAccountCombo: function() {
1051         var accountStore = Tine.Tinebase.appMgr.get('Felamimail').getAccountStore(),
1052             accountComboStore = new Ext.data.ArrayStore({
1053                 fields   : Tine.Felamimail.Model.Account
1054             });
1055         
1056         var aliasAccount = null,
1057             aliases = null,
1058             id = null;
1059             
1060         accountStore.each(function(account) {
1061             aliases = [ account.get('email') ];
1062
1063             if (account.get('type') == 'system') {
1064                 // add identities / aliases to store (for systemaccounts)
1065                 var user = Tine.Tinebase.registry.get('currentAccount');
1066                 if (user.emailUser && user.emailUser.emailAliases && user.emailUser.emailAliases.length > 0) {
1067                     aliases = aliases.concat(user.emailUser.emailAliases);
1068                 }
1069             }
1070             
1071             for (var i = 0; i < aliases.length; i++) {
1072                 id = (i == 0) ? account.id : Ext.id();
1073                 aliasAccount = account.copy(id);
1074                 if (i > 0) {
1075                     aliasAccount.data.id = id;
1076                     aliasAccount.set('email', aliases[i]);
1077                 }
1078                 aliasAccount.set('name', aliasAccount.get('name') + ' (' + aliases[i] +')');
1079                 aliasAccount.set('original_id', account.id);
1080                 accountComboStore.add(aliasAccount);
1081             }
1082         }, this);
1083         
1084         this.accountCombo = new Ext.form.ComboBox({
1085             name: 'account_id',
1086             ref: '../../accountCombo',
1087             plugins: [ Ext.ux.FieldLabeler ],
1088             fieldLabel: this.app.i18n._('From'),
1089             displayField: 'name',
1090             valueField: 'id',
1091             editable: false,
1092             triggerAction: 'all',
1093             store: accountComboStore,
1094             mode: 'local',
1095             listeners: {
1096                 scope: this,
1097                 select: this.onFromSelect
1098             }
1099         });
1100     },
1101     
1102     /**
1103      * if 'account_id' is changed we need to update the signature
1104      * 
1105      * @param {} combo
1106      * @param {} newValue
1107      * @param {} oldValue
1108      */
1109      onFromSelect: function(combo, record, index) {
1110         
1111         // get new signature
1112         var accountId = record.get('original_id');
1113         var newSignature = Tine.Felamimail.getSignature(accountId);
1114         var signatureRegexp = new RegExp('<br><br><span id="felamimail\-body\-signature">\-\-<br>.*</span>');
1115         
1116         // update signature
1117         var bodyContent = this.htmlEditor.getValue();
1118         bodyContent = bodyContent.replace(signatureRegexp, newSignature);
1119         
1120         this.htmlEditor.setValue(bodyContent);
1121     },
1122     
1123     /**
1124      * returns dialog
1125      * 
1126      * NOTE: when this method gets called, all initialisation is done.
1127      * 
1128      * @return {Object}
1129      * @private
1130      */
1131     getFormItems: function() {
1132         
1133         this.initAttachmentGrid();
1134         this.initAccountCombo();
1135         
1136         this.recipientGrid = new Tine.Felamimail.RecipientGrid({
1137             record: this.record,
1138             i18n: this.app.i18n,
1139             hideLabel: true,
1140             composeDlg: this,
1141             autoStartEditing: !this.AddressLoadMask
1142         });
1143         
1144         this.southPanel = new Ext.Panel({
1145             region: 'south',
1146             layout: 'form',
1147             height: 150,
1148             split: true,
1149             collapseMode: 'mini',
1150             header: false,
1151             collapsible: true,
1152             collapsed: (this.record.bodyIsFetched() && (! this.record.get('attachments') || this.record.get('attachments').length == 0)),
1153             items: [this.attachmentGrid]
1154         });
1155
1156         this.textEditor = new Ext.Panel({
1157             layout: 'fit',
1158             mimeType: 'text/plain',
1159             cls: 'felamimail-edit-text-plain',
1160             flex: 1,  // Take up all *remaining* vertical space
1161             setValue: function(v) {return this.items.get(0).setValue(v);},
1162             getValue: function() {return this.items.get(0).getValue();},
1163             tbar: ['->', {
1164                 iconCls: 'x-edit-toggleFormat',
1165                 tooltip: this.app.i18n._('Convert to formated text'),
1166                 handler: this.onToggleFormat,
1167                 scope: this
1168             }],
1169             items: [
1170                 new Ext.form.TextArea({
1171                     fieldLabel: this.app.i18n._('Body'),
1172                     name: 'body_text'
1173                 })
1174             ]
1175         });
1176
1177         this.htmlEditor = new Tine.Felamimail.ComposeEditor({
1178             fieldLabel: this.app.i18n._('Body'),
1179             name: 'body_html',
1180             mimeType: 'text/html',
1181             flex: 1  // Take up all *remaining* vertical space
1182         });
1183
1184         this.mailvelopeWrap = new Ext.Container({
1185             flex: 1,  // Take up all *remaining* vertical space
1186             mimeType: 'application/pgp-encrypted',
1187             getValue: function() {return '';}
1188         });
1189
1190         return {
1191             border: false,
1192             frame: true,
1193             layout: 'border',
1194             items: [
1195                 {
1196                 region: 'center',
1197                 layout: {
1198                     align: 'stretch',  // Child items are stretched to full width
1199                     type: 'vbox'
1200                 },
1201                 listeners: {
1202                     'afterlayout': this.fixLayout,
1203                     scope: this
1204                 },
1205                 items: [
1206                     this.accountCombo, 
1207                     this.recipientGrid, 
1208                 {
1209                     xtype:'textfield',
1210                     plugins: [ Ext.ux.FieldLabeler ],
1211                     fieldLabel: this.app.i18n._('Subject'),
1212                     name: 'subject',
1213                     ref: '../../subjectField',
1214                     enableKeyEvents: true,
1215                     listeners: {
1216                         scope: this,
1217                         // update title on keyup event
1218                         'keyup': function(field, e) {
1219                             if (! e.isSpecialKey()) {
1220                                 this.window.setTitle(
1221                                     this.app.i18n._('Compose email:') + ' ' 
1222                                     + field.getValue()
1223                                 );
1224                             }
1225                         },
1226                         'focus': function(field) {
1227                             this.subjectField.focus(true, 100);
1228                         }
1229                     }
1230                 }, {
1231                     layout:'card',
1232                     ref: '../../bodyCards',
1233                     activeItem: 0,
1234                     flex: 1,
1235                     items: [
1236                         this.textEditor,
1237                         this.htmlEditor,
1238                         this.mailvelopeWrap
1239                     ]
1240
1241                 }]
1242             }, this.southPanel]
1243         };
1244     },
1245
1246     /**
1247      * is form valid (checks if attachments are still uploading / recipients set)
1248      * 
1249      * @return {Boolean}
1250      */
1251     isValid: function() {
1252         var me = this;
1253         return Tine.Felamimail.MessageEditDialog.superclass.isValid.call(me).then(function(){
1254             if (me.attachmentGrid.isUploading()) {
1255                 reject(me.app.i18n._('Files are still uploading.'));
1256             }
1257
1258             return me.validateRecipients();
1259         });
1260     },
1261     
1262     /**
1263      * generic apply changes handler
1264      * - NOTE: overwritten to check here if the subject is empty and if the user wants to send an empty message
1265      * 
1266      * @param {Ext.Button} button
1267      * @param {Event} event
1268      * @param {Boolean} closeWindow
1269      * 
1270      * TODO add note editing textfield here
1271      */
1272     onApplyChanges: function(closeWindow, emptySubject, passwordSet) {
1273         var me = this;
1274
1275         Tine.log.debug('Tine.Felamimail.MessageEditDialog::onApplyChanges()');
1276         
1277         this.loadMask.show();
1278
1279         // If filemanager attachments are possible check if passwords are required to enter
1280         if (Tine.Tinebase.appMgr.isEnabled('Filemanager') && passwordSet !== true) {
1281             var attachmentStore = this.attachmentGrid.getStore();
1282
1283             if (attachmentStore.find('attachment_type', 'download_protected_fm') !== -1) {
1284                 var dialog = new Tine.Tinebase.widgets.dialog.PasswordDialog();
1285                 dialog.openWindow();
1286
1287                 dialog.on('passwordEntered', function (password) {
1288                     attachmentStore.each(function (attachment) {
1289                         if (attachment.get('attachment_type') === 'download_protected_fm') {
1290                             attachment.data.password = password;
1291                         }
1292                     });
1293
1294                     me.onApplyChanges(closeWindow, emptySubject, true);
1295                 });
1296                 return;
1297             }
1298         }
1299
1300         if (! emptySubject && this.getForm().findField('subject').getValue() == '') {
1301             Tine.log.debug('Tine.Felamimail.MessageEditDialog::onApplyChanges - empty subject');
1302             Ext.MessageBox.confirm(
1303                 this.app.i18n._('Empty subject'),
1304                 this.app.i18n._('Do you really want to send a message with an empty subject?'),
1305                 function (button) {
1306                     Tine.log.debug('Tine.Felamimail.MessageEditDialog::doApplyChanges - button: ' + button);
1307                     if (button == 'yes') {
1308                         this.onApplyChanges(closeWindow, true, true);
1309                     } else {
1310                         this.loadMask.hide();
1311                     }
1312                 },
1313                 this
1314             );
1315             
1316             return;
1317         }
1318
1319         Tine.log.debug('Tine.Felamimail.MessageEditDialog::doApplyChanges - call parent');
1320         this.doApplyChanges(closeWindow);
1321     },
1322     
1323     /**
1324      * checks recipients
1325      * 
1326      * @return {Boolean}
1327      */
1328     validateRecipients: function() {
1329         var me = this;
1330         return new Promise(function (fulfill, reject) {
1331             var to = me.record.get('to'),
1332                 cc = me.record.get('cc'),
1333                 bcc = me.record.get('bcc'),
1334                 all = [].concat(to).concat(cc).concat(bcc);
1335
1336             if (all.length == 0) {
1337                 reject(me.app.i18n._('No recipients set.'));
1338             }
1339
1340             if (me.button_toggleEncrypt.pressed && me.mailvelopeEditor) {
1341                 // always add own address so send message can be decrypted
1342                 all.push(me.record.get('from_email'));
1343
1344                 all = all.map(function (item) {
1345                     return addressparser.parse(item.replace(/,/g, '\\\\,'))[0].address;
1346                 });
1347
1348                 return Tine.Felamimail.mailvelopeHelper.getKeyring().then(function (keyring) {
1349                     keyring.validKeyForAddress(all).then(function (result) {
1350                         var missingKeys = [];
1351                         for (var address in result) {
1352                             if (!result[address]) {
1353                                 missingKeys.push(address);
1354                             }
1355                         }
1356
1357                         if (missingKeys.length) {
1358                             reject(String.format(
1359                                 me.app.i18n._('Cannot encrypt message. Public keys for the following recipients are missing: {0}'),
1360                                 Ext.util.Format.htmlEncode(missingKeys.join(', '))
1361                             ));
1362                         } else {
1363                             // NOTE: we sync message here as we have a promise at hand and onRecordUpdate is done before validation
1364                             return me.mailvelopeEditor.encrypt(all).then(function (armoredMessage) {
1365                                 me.record.set('body', armoredMessage);
1366                                 me.record.set('content_type', 'text/plain');
1367                                 // NOTE: Server would spoil MIME structure with attachments
1368                                 me.record.set('attachments', '');
1369                                 me.record.set('has_attachment', false);
1370                                 fulfill(true);
1371                             });
1372                         }
1373                     });
1374                 });
1375             } else {
1376                 fulfill(true);
1377             }
1378         });
1379     },
1380     
1381     /**
1382      * get validation error message
1383      * 
1384      * @return {String}
1385      */
1386     getValidationErrorMessage: function() {
1387         return this.validationErrorMessage;
1388     },
1389     
1390     /**
1391      * fills the recipient grid with the records gotten from this.fetchRecordsOnLoad
1392      * @param {Array} contacts
1393      */
1394     fillRecipientGrid: function(contacts) {
1395         this.recipientGrid.addRecordsToStore(contacts, 'to');
1396         this.recipientGrid.setFixedHeight(true);
1397     },
1398     
1399     /**
1400      * fetches records to send an email to
1401      */
1402     fetchRecordsOnLoad: function(dialog, record, ticketFn) {
1403         var interceptor = ticketFn(),
1404             sf = Ext.decode(this.selectionFilter);
1405             
1406         Tine.log.debug('Fetching additional records...');
1407         Tine.Addressbook.contactBackend.searchRecords(sf, null, {
1408             scope: this,
1409             success: function(result) {
1410                 this.fillRecipientGrid(result.records);
1411                 interceptor();
1412             }
1413         });
1414         this.addressesLoaded = true;
1415     }
1416 });
1417
1418 /**
1419  * Felamimail Edit Popup
1420  * 
1421  * @param   {Object} config
1422  * @return  {Ext.ux.Window}
1423  */
1424 Tine.Felamimail.MessageEditDialog.openWindow = function (config) {
1425     var window = Tine.WindowFactory.getWindow({
1426         width: 700,
1427         height: 700,
1428         name: Tine.Felamimail.MessageEditDialog.prototype.windowNamePrefix + Ext.id(),
1429         contentPanelConstructor: 'Tine.Felamimail.MessageEditDialog',
1430         contentPanelConstructorConfig: config
1431     });
1432     return window;
1433 };