Aleksander Machniak
2012-06-28 669747a81c4b2ff823d1f20dc50899163c0a8a4a
program/js/app.js
@@ -17,8 +17,6 @@
 +-----------------------------------------------------------------------+
 | Requires: jquery.js, common.js, list.js                               |
 +-----------------------------------------------------------------------+
  $Id$
*/
function rcube_webmail()
@@ -54,9 +52,10 @@
  // set jQuery ajax options
  $.ajaxSetup({
    cache:false,
    error:function(request, status, err){ ref.http_error(request, status, err); },
    beforeSend:function(xmlhttp){ xmlhttp.setRequestHeader('X-Roundcube-Request', ref.env.request_token); }
    cache: false,
    timeout: this.env.request_timeout * 1000,
    error: function(request, status, err){ ref.http_error(request, status, err); },
    beforeSend: function(xmlhttp){ xmlhttp.setRequestHeader('X-Roundcube-Request', ref.env.request_token); }
  });
  // set environment variable(s)
@@ -224,7 +223,7 @@
        this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list', 'forward',
          'moveto', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource', 'download',
          'print', 'load-attachment', 'load-headers', 'forward-attachment'];
          'print', 'load-attachment', 'show-headers', 'hide-headers', 'forward-attachment'];
        if (this.env.action == 'show' || this.env.action == 'preview') {
          this.enable_command(this.env.message_commands, this.env.uid);
@@ -307,6 +306,10 @@
          }
          this.http_post(postact, postdata);
        }
        // detect browser capabilities
        if (!this.is_framed())
          this.browser_capabilities_check();
        break;
@@ -455,6 +458,14 @@
    if (this.gui_objects.folderlist)
      this.gui_containers.foldertray = $(this.gui_objects.folderlist);
    // activate html5 file drop feature (if browser supports it and if configured)
    if (this.gui_objects.filedrop && this.env.filedrop && ((XMLHttpRequest && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) {
      $(document.body).bind('dragover dragleave drop', function(e){ return ref.document_drag_hover(e, e.type == 'dragover'); });
      $(this.gui_objects.filedrop).addClass('droptarget')
        .bind('dragover dragleave', function(e){ return ref.file_drag_hover(e, e.type == 'dragover'); })
        .get(0).addEventListener('drop', function(e){ return ref.file_dropped(e); }, false);
    }
    // trigger init event hook
    this.triggerEvent('init', { task:this.task, action:this.env.action });
@@ -482,7 +493,7 @@
  /*********************************************************/
  // execute a specific command on the web client
  this.command = function(command, props, obj)
  this.command = function(command, props, obj, event)
  {
    var ret, uid, cid, url, flag;
@@ -580,10 +591,6 @@
          this.list_contacts(props);
        break;
      case 'load-headers':
        this.load_headers(obj);
        break;
      case 'sort':
        var sort_order, sort_col = props;
@@ -632,7 +639,7 @@
          uid = this.get_single_uid();
          if (uid && (!this.env.uid || uid != this.env.uid)) {
            if (this.env.mailbox == this.env.drafts_mailbox)
              this.goto_url('compose', '_draft_uid='+uid+'&_mbox='+urlencode(this.env.mailbox), true);
              this.goto_url('compose', { _draft_uid: uid, _mbox: this.env.mailbox }, true);
            else
              this.show_message(uid);
          }
@@ -654,13 +661,14 @@
        break;
      case 'edit':
        if (this.task=='addressbook' && (cid = this.get_single_cid()))
        if (this.task == 'addressbook' && (cid = this.get_single_cid()))
          this.load_contact(cid, 'edit');
        else if (this.task=='settings' && props)
        else if (this.task == 'settings' && props)
          this.load_identity(props, 'edit-identity');
        else if (this.task=='mail' && (cid = this.get_single_uid())) {
          url = (this.env.mailbox == this.env.drafts_mailbox) ? '_draft_uid=' : '_uid=';
          this.goto_url('compose', url+cid+'&_mbox='+urlencode(this.env.mailbox), true);
        else if (this.task == 'mail' && (cid = this.get_single_uid())) {
          url = { _mbox: this.env.mailbox };
          url[this.env.mailbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid'] = cid;
          this.goto_url('compose', url, true);
        }
        break;
@@ -705,7 +713,7 @@
      case 'delete':
        // mail task
        if (this.task == 'mail')
          this.delete_messages();
          this.delete_messages(event);
        // addressbook task
        else if (this.task == 'addressbook')
          this.delete_contacts();
@@ -770,7 +778,7 @@
      case 'always-load':
        if (this.env.uid && this.env.sender) {
          this.add_contact(urlencode(this.env.sender));
          this.add_contact(this.env.sender);
          setTimeout(function(){ ref.command('load-images'); }, 300);
          break;
        }
@@ -959,8 +967,6 @@
        form.action = this.add_url(form.action, '_lang', lang);
        form.submit();
        // clear timeout (sending could take longer)
        clearTimeout(this.request_timer);
        break;
      case 'send-attachment':
@@ -987,12 +993,12 @@
      case 'reply-list':
      case 'reply':
        if (uid = this.get_single_uid()) {
          url = '_reply_uid='+uid+'&_mbox='+urlencode(this.env.mailbox);
          url = {_reply_uid: uid, _mbox: this.env.mailbox};
          if (command == 'reply-all')
            // do reply-list, when list is detected and popup menu wasn't used 
            url += '&_all=' + (!props && this.commands['reply-list'] ? 'list' : 'all');
            url._all = (!props && this.commands['reply-list'] ? 'list' : 'all');
          else if (command == 'reply-list')
            url += '&_all=list';
            url._all = list;
          this.goto_url('compose', url, true);
        }
@@ -1001,9 +1007,9 @@
      case 'forward-attachment':
      case 'forward':
        if (uid = this.get_single_uid()) {
          url = '_forward_uid='+uid+'&_mbox='+urlencode(this.env.mailbox);
          url = { _forward_uid: uid, _mbox: this.env.mailbox };
          if (command == 'forward-attachment' || (!props && this.env.forward_attachment))
            url += '&_attachment=1';
            url._attachment = 1;
          this.goto_url('compose', url, true);
        }
        break;
@@ -1029,7 +1035,7 @@
      case 'download':
        if (uid = this.get_single_uid())
          this.goto_url('viewsource', '&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox)+'&_save=1');
          this.goto_url('viewsource', { _uid: uid, _mbox: this.env.mailbox, _save: 1 });
        break;
      // quicksearch
@@ -1082,7 +1088,7 @@
      case 'export':
        if (this.contact_list.rowcount > 0) {
          this.goto_url('export', { _source:this.env.source, _gid:this.env.group, _search:this.env.search_request });
          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request });
        }
        break;
@@ -1109,7 +1115,7 @@
      default:
        var func = command.replace(/-/g, '_');
        if (this[func] && typeof this[func] === 'function') {
          ret = this[func](props);
          ret = this[func](props, obj);
        }
        break;
    }
@@ -1162,14 +1168,6 @@
    if (this.gui_objects.editform)
      this.lock_form(this.gui_objects.editform, a);
    // clear pending timer
    if (this.request_timer)
      clearTimeout(this.request_timer);
    // set timer for requests
    if (a && this.env.request_timeout)
      this.request_timer = setTimeout(function(){ ref.request_timed_out(); }, this.env.request_timeout * 1000);
    return id;
  };
@@ -1206,13 +1204,6 @@
      url = this.env.comm_path;
    return url.replace(/_task=[a-z]+/, '_task='+task);
  };
  // called when a request timed out
  this.request_timed_out = function()
  {
    this.set_busy(false);
    this.display_message('Request timed out!', 'error');
  };
  this.reload = function(delay)
@@ -1598,7 +1589,7 @@
    var uid = list.get_single_selection();
    if (uid && this.env.mailbox == this.env.drafts_mailbox)
      this.goto_url('compose', '_draft_uid='+uid+'&_mbox='+urlencode(this.env.mailbox), true);
      this.goto_url('compose', { _draft_uid: uid, _mbox: this.env.mailbox }, true);
    else if (uid)
      this.show_message(uid, false, false);
  };
@@ -1836,7 +1827,7 @@
        html = '<span id="flagicn'+uid+'" class="'+css_class+'">&nbsp;</span>';
      }
      else if (c == 'attachment') {
        if (/application\/|multipart\/m/.test(flags.ctype))
        if (/application\/|multipart\/(m|signed)/.test(flags.ctype))
          html = '<span class="attachment">&nbsp;</span>';
        else if (/multipart\/report/.test(flags.ctype))
          html = '<span class="report">&nbsp;</span>';
@@ -1857,8 +1848,11 @@
      else if (c == 'threads')
        html = expando;
      else if (c == 'subject') {
        if (bw.ie)
        if (bw.ie) {
          col.onmouseover = function() { rcube_webmail.long_subject_title_ie(this, message.depth+1); };
          if (bw.ie8)
            tree = '<span></span>' + tree; // #1487821
        }
        html = tree + cols[c];
      }
      else if (c == 'priority') {
@@ -1962,13 +1956,16 @@
    if (this.env.search_request)
      url += '&_search='+this.env.search_request;
    if (action == 'preview' && String(target.location.href).indexOf(url) >= 0)
    // add browser capabilities, so we can properly handle attachments
    url += '&_caps='+urlencode(this.browser_capabilities());
    if (preview && String(target.location.href).indexOf(url) >= 0)
      this.show_contentframe(true);
    else {
      this.location_href(this.env.comm_path+url, target, true);
      // mark as read and change mbox unread counter
      if (action == 'preview' && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read >= 0) {
      if (preview && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read >= 0) {
        this.preview_read_timer = setTimeout(function() {
          ref.set_message(id, 'unread', false);
          ref.update_thread_root(id, 'read');
@@ -2558,7 +2555,7 @@
  };
  // delete selected messages from the current mailbox
  this.delete_messages = function()
  this.delete_messages = function(event)
  {
    var uid, i, len, trash = this.env.trash_mailbox,
      list = this.message_list,
@@ -2590,7 +2587,7 @@
    // if there is a trash mailbox defined and we're not currently in it
    else {
      // if shift was pressed delete it immediately
      if (list && list.modkey == SHIFT_KEY) {
      if ((list && list.modkey == SHIFT_KEY) || (event && rcube_event.get_modifier(event) == SHIFT_KEY)) {
        if (confirm(this.get_label('deletemessagesconfirm')))
          this.permanently_remove_messages();
      }
@@ -3005,7 +3002,7 @@
      this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length);
      // add signature according to selected identity
      // if we have HTML editor, signature is added in callback
      if (input_from.prop('type') == 'select-one' && $("input[name='_draft_saveid']").val() == '') {
      if (input_from.prop('type') == 'select-one') {
        this.change_identity(input_from[0]);
      }
    }
@@ -3464,12 +3461,7 @@
      var content = '<span>' + this.get_label('uploading' + (files > 1 ? 'many' : '')) + '</span>',
        ts = frame_name.replace(/^rcmupload/, '');
      if (this.env.loadingicon)
        content = '<img src="'+this.env.loadingicon+'" alt="" class="uploading" />'+content;
      content = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+ts+'\', \''+frame_name+'\');" href="#cancelupload" class="cancelupload">'
        + (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="" />' : this.get_label('cancel')) + '</a>' + content;
      this.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false });
      this.add2attachment_list(ts, { name:'', html:content, classname:'uploading', frame:frame_name, complete:false });
      // upload progress support
      if (this.env.upload_progress_time) {
@@ -3488,6 +3480,13 @@
  {
    if (!this.gui_objects.attachmentlist)
      return false;
    if (!att.complete && ref.env.loadingicon)
      att.html = '<img src="'+ref.env.loadingicon+'" alt="" class="uploading" />' + att.html;
    if (!att.complete && att.frame)
      att.html = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\', \''+att.frame+'\');" href="#cancelupload" class="cancelupload">'
        + (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="" />' : this.get_label('cancel')) + '</a>' + att.html;
    var indicator, li = $('<li>').attr('id', name).addClass(att.classname).html(att.html);
@@ -3815,7 +3814,7 @@
      return;
    // ...new search value contains old one and previous search was not finished or its result was empty
    if (old_value && old_value.length && q.indexOf(old_value) == 0 && (!ac || !ac.num) && this.env.contacts && !this.env.contacts.length)
    if (old_value && old_value.length && q.indexOf(old_value) == 0 && (!ac || ac.num <= 0) && this.env.contacts && !this.env.contacts.length)
      return;
    var i, lock, source, xhr, reqid = new Date().getTime(),
@@ -3829,7 +3828,7 @@
    for (i=0; i<threads; i++) {
      source = this.ksearch_data.sources.shift();
      if (threads > 1 && source === null)
      if (threads > 1 && source === undefined)
        break;
      post_data._source = source ? source : '';
@@ -4716,11 +4715,11 @@
  {
    if (form && form.elements._photo.value) {
      this.async_upload_form(form, 'upload-photo', function(e) {
        rcmail.set_busy(false, null, rcmail.photo_upload_id);
        rcmail.set_busy(false, null, rcmail.file_upload_id);
      });
      // display upload indicator
      this.photo_upload_id = this.set_busy(true, 'uploading');
      this.file_upload_id = this.set_busy(true, 'uploading');
    }
  };
@@ -4735,8 +4734,8 @@
  this.photo_upload_end = function()
  {
    this.set_busy(false, null, this.photo_upload_id);
    delete this.photo_upload_id;
    this.set_busy(false, null, this.file_upload_id);
    delete this.file_upload_id;
  };
  this.set_photo_actions = function(id)
@@ -4937,7 +4936,7 @@
    // submit request with appended token
    if (confirm(this.get_label('deleteidentityconfirm')))
      this.goto_url('delete-identity', '_iid='+id+'&_token='+this.env.request_token, true);
      this.goto_url('delete-identity', { _iid: id, _token: this.env.request_token }, true);
    return true;
  };
@@ -5850,14 +5849,14 @@
  };
  // display all-headers row and fetch raw message headers
  this.load_headers = function(elem)
  this.show_headers = function(props, elem)
  {
    if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box || !this.env.uid)
      return;
    $(elem).removeClass('show-headers').addClass('hide-headers');
    $(this.gui_objects.all_headers_row).show();
    elem.onclick = function() { rcmail.hide_headers(elem); };
    elem.onclick = function() { rcmail.command('hide-headers', '', elem); };
    // fetch headers only once
    if (!this.gui_objects.all_headers_box.innerHTML) {
@@ -5867,14 +5866,14 @@
  };
  // hide all-headers row
  this.hide_headers = function(elem)
  this.hide_headers = function(props, elem)
  {
    if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box)
      return;
    $(elem).removeClass('hide-headers').addClass('show-headers');
    $(this.gui_objects.all_headers_row).hide();
    elem.onclick = function() { rcmail.load_headers(elem); };
    elem.onclick = function() { rcmail.command('show-headers', '', elem); };
  };
@@ -5998,7 +5997,7 @@
    return $.ajax({
      type: 'GET', url: url, data: { _unlock:(lock?lock:0) }, dataType: 'json',
      success: function(data){ ref.http_response(data); },
      error: function(o, status, err) { rcmail.http_error(o, status, err, lock); }
      error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
    });
  };
@@ -6030,7 +6029,7 @@
    return $.ajax({
      type: 'POST', url: url, data: postdata, dataType: 'json',
      success: function(data){ ref.http_response(data); },
      error: function(o, status, err) { rcmail.http_error(o, status, err, lock); }
      error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
    });
  };
@@ -6162,7 +6161,7 @@
  };
  // handle HTTP request errors
  this.http_error = function(request, status, err, lock)
  this.http_error = function(request, status, err, lock, action)
  {
    var errmsg = request.statusText;
@@ -6171,6 +6170,16 @@
    if (request.status && errmsg)
      this.display_message(this.get_label('servererror') + ' (' + errmsg + ')', 'error');
    else if (status == 'timeout')
      this.display_message(this.get_label('requesttimedout'), 'error');
    else if (request.status == 0 && status != 'abort')
      this.display_message(this.get_label('servererror') + ' (No connection)', 'error');
    // re-send keep-alive requests after 30 seconds
    if (action == 'keep-alive')
      setTimeout(function(){ ref.keep_alive(); }, 30000);
    else if (action == 'check-recent')
      setTimeout(function(){ ref.check_for_recent(false); }, 30000);
  };
  // post the given form to a hidden iframe
@@ -6220,6 +6229,125 @@
    return frame_name;
  };
  // html5 file-drop API
  this.document_drag_hover = function(e, over)
  {
    e.preventDefault();
    $(ref.gui_objects.filedrop)[(over?'addClass':'removeClass')]('active');
  };
  this.file_drag_hover = function(e, over)
  {
    e.preventDefault();
    e.stopPropagation();
    $(ref.gui_objects.filedrop)[(over?'addClass':'removeClass')]('hover');
  };
  // handler when files are dropped to a designated area.
  // compose a multipart form data and submit it to the server
  this.file_dropped = function(e)
  {
    // abort event and reset UI
    this.file_drag_hover(e, false);
    // prepare multipart form data composition
    var files = e.target.files || e.dataTransfer.files,
      formdata = window.FormData ? new FormData() : null,
      fieldname = (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'),
      boundary = '------multipartformboundary' + (new Date).getTime(),
      dashdash = '--', crlf = '\r\n',
      multipart = dashdash + boundary + crlf;
    if (!files || !files.length)
      return;
    // inline function to submit the files to the server
    var submit_data = function() {
      var multiple = files.length > 1,
        ts = new Date().getTime(),
        content = '<span>' + (multiple ? ref.get_label('uploadingmany') : files[0].name) + '</span>';
      // add to attachments list
      if (!ref.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false }))
        ref.file_upload_id = ref.set_busy(true, 'uploading');
      // complete multipart content and post request
      multipart += dashdash + boundary + dashdash + crlf;
      $.ajax({
        type: 'POST',
        dataType: 'json',
        url: ref.url(ref.env.filedrop.action||'upload', { _id:ref.env.compose_id||ref.env.cid||'', _uploadid:ts, _remote:1 }),
        contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary,
        processData: false,
        data: formdata || multipart,
        beforeSend: function(xhr, s) { if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; },
        success: function(data){ ref.http_response(data); },
        error: function(o, status, err) { ref.http_error(o, status, err, null, 'attachment'); }
      });
    };
    // get contents of all dropped files
    var last = this.env.filedrop.single ? 0 : files.length - 1;
    for (var j=0, i=0, f; j <= last && (f = files[i]); i++) {
      if (!f.name) f.name = f.fileName;
      if (!f.size) f.size = f.fileSize;
      if (!f.type) f.type = 'application/octet-stream';
      // file name contains non-ASCII characters, do UTF8-binary string conversion.
      if (!formdata && /[^\x20-\x7E]/.test(f.name))
        f.name_bin = unescape(encodeURIComponent(f.name));
      // filter by file type if requested
      if (this.env.filedrop.filter && !f.type.match(new RegExp(this.env.filedrop.filter))) {
        // TODO: show message to user
        continue;
      }
      // do it the easy way with FormData (FF 4+, Chrome 5+, Safari 5+)
      if (formdata) {
        formdata.append(fieldname, f);
        if (j == last)
          return submit_data();
      }
      // use FileReader supporetd by Firefox 3.6
      else if (window.FileReader) {
        var reader = new FileReader();
        // closure to pass file properties to async callback function
        reader.onload = (function(file, j) {
          return function(e) {
            multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
            multipart += '; filename="' + (f.name_bin || file.name) + '"' + crlf;
            multipart += 'Content-Length: ' + file.size + crlf;
            multipart += 'Content-Type: ' + file.type + crlf + crlf;
            multipart += e.target.result + crlf;
            multipart += dashdash + boundary + crlf;
            if (j == last)  // we're done, submit the data
              return submit_data();
          }
        })(f,j);
        reader.readAsBinaryString(f);
      }
      // Firefox 3
      else if (f.getAsBinary) {
        multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
        multipart += '; filename="' + (f.name_bin || f.name) + '"' + crlf;
        multipart += 'Content-Length: ' + f.size + crlf;
        multipart += 'Content-Type: ' + f.type + crlf + crlf;
        multipart += f.getAsBinary() + crlf;
        multipart += dashdash + boundary +crlf;
        if (j == last)
          return submit_data();
      }
      j++;
    }
  };
  // starts interval for keep-alive/check-recent signal
  this.start_keepalive = function()
@@ -6377,6 +6505,99 @@
      $(elem).click(function() { rcmail.register_protocol_handler(name); return false; });
  };
  // Checks browser capabilities eg. PDF support, TIF support
  this.browser_capabilities_check = function()
  {
    if (!this.env.browser_capabilities)
      this.env.browser_capabilities = {};
    if (this.env.browser_capabilities.pdf === undefined)
      this.env.browser_capabilities.pdf = this.pdf_support_check();
    if (this.env.browser_capabilities.flash === undefined)
      this.env.browser_capabilities.flash = this.flash_support_check();
    if (this.env.browser_capabilities.tif === undefined)
      this.tif_support_check();
  };
  // Returns browser capabilities string
  this.browser_capabilities = function()
  {
    if (!this.env.browser_capabilities)
      return '';
    var n, ret = [];
    for (n in this.env.browser_capabilities)
      ret.push(n + '=' + this.env.browser_capabilities[n]);
    return ret.join();
  };
  this.tif_support_check = function()
  {
    var img = new Image();
    img.onload = function() { rcmail.env.browser_capabilities.tif = 1; };
    img.onerror = function() { rcmail.env.browser_capabilities.tif = 0; };
    img.src = 'program/blank.tif';
  };
  this.pdf_support_check = function()
  {
    var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/pdf"] : {},
      plugins = navigator.plugins,
      len = plugins.length,
      regex = /Adobe Reader|PDF|Acrobat/i;
    if (plugin && plugin.enabledPlugin)
        return 1;
    if (window.ActiveXObject) {
      try {
        if (axObj = new ActiveXObject("AcroPDF.PDF"))
          return 1;
      }
      catch (e) {}
      try {
        if (axObj = new ActiveXObject("PDF.PdfCtrl"))
          return 1;
      }
      catch (e) {}
    }
    for (i=0; i<len; i++) {
      plugin = plugins[i];
      if (typeof plugin === 'String') {
        if (regex.test(plugin))
          return 1;
      }
      else if (plugin.name && regex.test(plugin.name))
        return 1;
    }
    return 0;
  };
  this.flash_support_check = function()
  {
    var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/x-shockwave-flash"] : {};
    if (plugin && plugin.enabledPlugin)
        return 1;
    if (window.ActiveXObject) {
      try {
        if (axObj = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
          return 1;
      }
      catch (e) {}
    }
    return 0;
  };
}  // end object rcube_webmail