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