Aleksander Machniak
2013-06-13 99e17f6131bf0eb1762ddc13766b41fcd1942a29
program/js/app.js
@@ -3,8 +3,8 @@
 | Roundcube Webmail Client Script                                       |
 |                                                                       |
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
 | Copyright (C) 2011, Kolab Systems AG                                  |
 | Copyright (C) 2005-2013, The Roundcube Dev Team                       |
 | Copyright (C) 2011-2013, Kolab Systems AG                             |
 |                                                                       |
 | Licensed under the GNU General Public License version 3 or            |
 | any later version with exceptions for skins & plugins.                |
@@ -44,7 +44,9 @@
    comm_path: './',
    blankpage: 'program/resources/blank.gif',
    recipients_separator: ',',
    recipients_delimiter: ', '
    recipients_delimiter: ', ',
    popup_width: 1150,
    popup_width_small: 900
  };
  // create protected reference to myself
@@ -179,7 +181,8 @@
    }
    // enable general commands
    this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref', 'compose', 'undo', 'about', 'switch-task', true);
    this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref',
      'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-save', true);
    if (this.env.permaurl)
      this.enable_command('permaurl', 'extwin', true);
@@ -205,12 +208,13 @@
          this.message_list.addEventListener('dragend', function(e){ p.drag_end(e); });
          this.message_list.addEventListener('expandcollapse', function(e){ p.msglist_expand(e); });
          this.message_list.addEventListener('column_replace', function(e){ p.msglist_set_coltypes(e); });
          this.message_list.addEventListener('listupdate', function(e){ p.triggerEvent('listupdate', e); });
          document.onmouseup = function(e){ return p.doc_mouse_up(e); };
          this.gui_objects.messagelist.parentNode.onmousedown = function(e){ return p.click_on_list(e); };
          this.message_list.init();
          this.enable_command('toggle_status', 'toggle_flag', 'menu-open', 'menu-save', 'sort', true);
          this.enable_command('toggle_status', 'toggle_flag', 'sort', true);
          // load messages
          this.command('list');
@@ -219,15 +223,15 @@
        if (this.gui_objects.qsearchbox) {
          if (this.env.search_text != null)
            this.gui_objects.qsearchbox.value = this.env.search_text;
          $(this.gui_objects.qsearchbox).focusin(function() { rcmail.message_list.blur(); });
          $(this.gui_objects.qsearchbox).focusin(function() { rcmail.message_list && rcmail.message_list.blur(); });
        }
        this.set_button_titles();
        this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list',
          'moveto', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource',
          'print', 'load-attachment', 'show-headers', 'hide-headers', 'download',
          'forward', 'forward-inline', 'forward-attachment'];
          'print', 'load-attachment', 'download-attachment', 'show-headers', 'hide-headers', 'download',
          'forward', 'forward-inline', 'forward-attachment', 'change-format'];
        if (this.env.action == 'show' || this.env.action == 'preview') {
          this.enable_command(this.env.message_commands, this.env.uid);
@@ -251,7 +255,7 @@
          }
        }
        else if (this.env.action == 'compose') {
          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'extwin'];
          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'search', 'reset-search', 'extwin'];
          if (this.env.drafts_mailbox)
            this.env.compose_commands.push('savedraft')
@@ -312,7 +316,7 @@
        }
        // detect browser capabilities
        if (!this.is_framed())
        if (!this.is_framed() && !this.env.extwin)
          this.browser_capabilities_check();
        break;
@@ -459,8 +463,22 @@
      this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]);
    // map implicit containers
    if (this.gui_objects.folderlist)
    if (this.gui_objects.folderlist) {
      this.gui_containers.foldertray = $(this.gui_objects.folderlist);
      // init treelist widget
      if (window.rcube_treelist_widget) {
        this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, {
          id_prefix: 'rcmli',
          id_encode: this.html_identifier_encode,
          id_decode: this.html_identifier_decode,
          check_droptarget: function(node){ return !node.virtual && ref.check_droptarget(node.id) }
        });
        this.treelist.addEventListener('collapse', function(node){ ref.folder_collapsed(node) });
        this.treelist.addEventListener('expand', function(node){ ref.folder_collapsed(node) });
        this.treelist.addEventListener('select', function(node){ ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }) });
      }
    }
    // activate html5 file drop feature (if browser supports it and if configured)
    if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) {
@@ -580,18 +598,36 @@
      case 'extwin':
        if (this.env.action == 'compose') {
          var prevstate = this.env.compose_extwin;
          $("input[name='_action']", this.gui_objects.messageform).val('compose');
          this.gui_objects.messageform.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 });
          this.gui_objects.messageform.target = this.open_window('', 1150, 900);
          this.gui_objects.messageform.submit();
          var form = this.gui_objects.messageform,
            win = this.open_window('');
          $("input[name='_action']", form).val('compose');
          form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 });
          form.target = win.name;
          form.submit();
        }
        else {
          this.open_window(this.env.permaurl, 1000, 1200);
          this.open_window(this.env.permaurl, true);
        }
        break;
      case 'change-format':
        url = this.env.permaurl + '&_format=' + props;
        if (this.env.action == 'preview')
          url = url.replace(/_action=show/, '_action=preview') + '&_framed=1';
        if (this.env.extwin)
          url += '&_extwin=1';
        location.href = url;
        break;
      case 'menu-open':
        if (props && props.menu == 'attachmentmenu') {
          var mimetype = this.env.attachments[props.id];
          this.enable_command('open-attachment', mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0);
        }
      case 'menu-save':
        this.triggerEvent(command, {props:props});
        return false;
@@ -756,7 +792,7 @@
      case 'moveto':
        if (this.task == 'mail')
          this.move_messages(props);
        else if (this.task == 'addressbook' && this.drag_active)
        else if (this.task == 'addressbook')
          this.copy_contact(null, props);
        break;
@@ -817,15 +853,15 @@
        break;
      case 'load-attachment':
        var qstring = '_mbox='+urlencode(this.env.mailbox)+'&_uid='+this.env.uid+'&_part='+props.part;
      case 'open-attachment':
      case 'download-attachment':
        var qstring = '_mbox='+urlencode(this.env.mailbox)+'&_uid='+this.env.uid+'&_part='+props,
          mimetype = this.env.attachments[props];
        // open attachment in frame if it's of a supported mimetype
        if (this.env.uid && props.mimetype && this.env.mimetypes && $.inArray(props.mimetype, this.env.mimetypes) >= 0) {
          var attachment_win = window.open(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1', 'rcubemailattachment'+this.env.uid+props.part);
          if (attachment_win) {
            setTimeout(function(){ attachment_win.focus(); }, 10);
        if (command != 'download-attachment' && mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0) {
          if (this.open_window(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1', true, true))
            break;
          }
        }
        this.goto_url('get', qstring+'&_download=1', false);
@@ -864,7 +900,7 @@
      case 'nextmessage':
        if (this.env.next_uid)
          this.show_message(this.env.next_uid, false, this.env.action=='preview');
          this.show_message(this.env.next_uid, false, this.env.action == 'preview');
        break;
      case 'lastmessage':
@@ -900,16 +936,13 @@
            url._to = props;
          }
          else {
            // use contact_id passed as command parameter
            var n, len, a_cids = [];
            var a_cids = [];
            // use contact id passed as command parameter
            if (props)
              a_cids.push(props);
            // get selected contacts
            else if (this.contact_list) {
              var selection = this.contact_list.get_selection();
              for (n=0, len=selection.length; n<len; n++)
                a_cids.push(selection[n]);
            }
            else if (this.contact_list)
              a_cids = this.contact_list.get_selection();
            if (a_cids.length)
              this.http_post('mailto', { _cid: a_cids.join(','), _source: this.env.source }, true);
@@ -944,8 +977,8 @@
        // Reset the auto-save timer
        clearTimeout(this.save_timer);
        // compose form did not change
        if (this.cmp_hash == this.compose_field_hash()) {
        // compose form did not change (and draft wasn't saved already)
        if (this.env.draft_id && this.cmp_hash == this.compose_field_hash()) {
          this.auto_save_start();
          break;
        }
@@ -1012,9 +1045,8 @@
      case 'print':
        if (uid = this.get_single_uid()) {
          ref.printwin = window.open(this.env.comm_path+'&_action=print&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox)+(this.env.safemode ? '&_safe=1' : ''));
          ref.printwin = this.open_window(this.env.comm_path+'&_action=print&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox)+(this.env.safemode ? '&_safe=1' : ''), true, true);
          if (this.printwin) {
            setTimeout(function(){ ref.printwin.focus(); }, 20);
            if (this.env.action != 'show')
              this.mark_message('read', uid);
          }
@@ -1022,11 +1054,8 @@
        break;
      case 'viewsource':
        if (uid = this.get_single_uid()) {
          ref.sourcewin = window.open(this.env.comm_path+'&_action=viewsource&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox));
          if (this.sourcewin)
            setTimeout(function(){ ref.sourcewin.focus(); }, 20);
          }
        if (uid = this.get_single_uid())
          this.open_window(this.env.comm_path+'&_action=viewsource&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox), true, true);
        break;
      case 'download':
@@ -1050,8 +1079,13 @@
        this.reset_qsearch();
        this.select_all_mode = false;
        if (s && this.env.mailbox)
        if (s && this.env.action == 'compose') {
          if (this.contact_list)
            this.list_contacts_clear();
        }
        else if (s && this.env.mailbox) {
          this.list_mailbox(this.env.mailbox, 1);
        }
        else if (s && this.task == 'addressbook') {
          if (this.env.source == '') {
            for (n in this.env.address_sources) break;
@@ -1141,6 +1175,7 @@
      if (typeof cmd === 'string') {
        this.commands[cmd] = enable;
        this.set_button(cmd, (enable ? 'act' : 'pas'));
        this.triggerEvent('enable-command', {command: cmd, status: enable});
      }
      // push array elements into commands array
      else {
@@ -1205,7 +1240,7 @@
    if (!url)
      url = this.env.comm_path;
    return url.replace(/_task=[a-z]+/, '_task='+task);
    return url.replace(/_task=[a-z0-9_-]+/i, '_task='+task);
  };
  this.reload = function(delay)
@@ -1258,11 +1293,12 @@
  this.html_identifier = function(str, encode)
  {
    str = String(str);
    if (encode)
      return Base64.encode(str).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
    else
      return str.replace(this.identifier_expr, '_');
    return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_');
  };
  this.html_identifier_encode = function(str)
  {
    return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
  };
  this.html_identifier_decode = function(str)
@@ -1315,29 +1351,9 @@
    if (this.preview_read_timer)
      clearTimeout(this.preview_read_timer);
    // save folderlist and folders location/sizes for droptarget calculation in drag_move()
    if (this.gui_objects.folderlist && model) {
      this.initialBodyScrollTop = bw.ie ? 0 : window.pageYOffset;
      this.initialListScrollTop = this.gui_objects.folderlist.parentNode.scrollTop;
      var k, li, height,
        list = $(this.gui_objects.folderlist);
        pos = list.offset();
      this.env.folderlist_coords = { x1:pos.left, y1:pos.top, x2:pos.left + list.width(), y2:pos.top + list.height() };
      this.env.folder_coords = [];
      for (k in model) {
        if (li = this.get_folder_li(k)) {
          // only visible folders
          if (height = li.firstChild.offsetHeight) {
            pos = $(li.firstChild).offset();
            this.env.folder_coords[k] = { x1:pos.left, y1:pos.top,
              x2:pos.left + li.firstChild.offsetWidth, y2:pos.top + height, on:0 };
          }
        }
      }
    }
    // prepare treelist widget for dragging interactions
    if (this.treelist)
      this.treelist.drag_start();
  };
  this.drag_end = function(e)
@@ -1345,87 +1361,28 @@
    this.drag_active = false;
    this.env.last_folder_target = null;
    if (this.folder_auto_timer) {
      clearTimeout(this.folder_auto_timer);
      this.folder_auto_timer = null;
      this.folder_auto_expand = null;
    }
    // over the folders
    if (this.gui_objects.folderlist && this.env.folder_coords) {
      for (var k in this.env.folder_coords) {
        if (this.env.folder_coords[k].on)
          $(this.get_folder_li(k)).removeClass('droptarget');
      }
    }
    if (this.treelist)
      this.treelist.drag_end();
  };
  this.drag_move = function(e)
  {
    if (this.gui_objects.folderlist && this.env.folder_coords) {
      var k, li, div, check, oldclass,
    if (this.gui_objects.folderlist) {
      var drag_target, oldclass,
        layerclass = 'draglayernormal',
        mouse = rcube_event.get_mouse_pos(e),
        pos = this.env.folderlist_coords,
        // offsets to compensate for scrolling while dragging a message
        boffset = bw.ie ? -document.documentElement.scrollTop : this.initialBodyScrollTop,
        moffset = this.initialListScrollTop-this.gui_objects.folderlist.parentNode.scrollTop;
        mouse = rcube_event.get_mouse_pos(e);
      if (this.contact_list && this.contact_list.draglayer)
        oldclass = this.contact_list.draglayer.attr('class');
      mouse.y += -moffset-boffset;
      // if mouse pointer is outside of folderlist
      if (mouse.x < pos.x1 || mouse.x >= pos.x2 || mouse.y < pos.y1 || mouse.y >= pos.y2) {
        if (this.env.last_folder_target) {
          $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget');
          this.env.folder_coords[this.env.last_folder_target].on = 0;
          this.env.last_folder_target = null;
        }
        if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
          this.contact_list.draglayer.attr('class', layerclass);
        return;
      // mouse intersects a valid drop target on the treelist
      if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) {
        this.env.last_folder_target = drag_target;
        layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal');
      }
      // over the folders
      for (k in this.env.folder_coords) {
        pos = this.env.folder_coords[k];
        if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.y >= pos.y1 && mouse.y < pos.y2) {
          if (check = this.check_droptarget(k)) {
            li = this.get_folder_li(k);
            div = $(li.getElementsByTagName('div')[0]);
            // if the folder is collapsed, expand it after 1sec and restart the drag & drop process.
            if (div.hasClass('collapsed')) {
              if (this.folder_auto_timer)
                clearTimeout(this.folder_auto_timer);
              this.folder_auto_expand = this.env.mailboxes[k].id;
              this.folder_auto_timer = setTimeout(function() {
                rcmail.command('collapse-folder', rcmail.folder_auto_expand);
                rcmail.drag_start(null);
              }, 1000);
            }
            else if (this.folder_auto_timer) {
              clearTimeout(this.folder_auto_timer);
              this.folder_auto_timer = null;
              this.folder_auto_expand = null;
            }
            $(li).addClass('droptarget');
            this.env.folder_coords[k].on = 1;
            this.env.last_folder_target = k;
            layerclass = 'draglayer' + (check > 1 ? 'copy' : 'normal');
          }
          // Clear target, otherwise drag end will trigger move into last valid droptarget
          else
            this.env.last_folder_target = null;
        }
        else if (pos.on) {
          $(this.get_folder_li(k)).removeClass('droptarget');
          this.env.folder_coords[k].on = 0;
        }
      else {
        // Clear target, otherwise drag end will trigger move into last valid droptarget
        this.env.last_folder_target = null;
      }
      if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
@@ -1435,40 +1392,33 @@
  this.collapse_folder = function(name)
  {
    var li = this.get_folder_li(name, '', true),
      div = $('div:first', li),
      ul = $('ul:first', li);
    if (this.treelist)
      this.treelist.toggle(name);
  };
    if (div.hasClass('collapsed')) {
      ul.show();
      div.removeClass('collapsed').addClass('expanded');
      var reg = new RegExp('&'+urlencode(name)+'&');
      this.env.collapsed_folders = this.env.collapsed_folders.replace(reg, '');
    }
    else if (div.hasClass('expanded')) {
      ul.hide();
      div.removeClass('expanded').addClass('collapsed');
      this.env.collapsed_folders = this.env.collapsed_folders+'&'+urlencode(name)+'&';
  this.folder_collapsed = function(node)
  {
    var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders';
    if (node.collapsed) {
      this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&';
      // select the folder if one of its childs is currently selected
      // don't select if it's virtual (#1488346)
      if (this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !$(li).hasClass('virtual'))
      if (this.env.mailbox && this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !node.virtual)
        this.command('list', name);
    }
    else
      return;
    // Work around a bug in IE6 and IE7, see #1485309
    if (bw.ie6 || bw.ie7) {
      var siblings = li.nextSibling ? li.nextSibling.getElementsByTagName('ul') : null;
      if (siblings && siblings.length && (li = siblings[0]) && li.style && li.style.display != 'none') {
        li.style.display = 'none';
        li.style.display = '';
      }
    else {
      var reg = new RegExp('&'+urlencode(node.id)+'&');
      this.env[prefname] = this.env[prefname].replace(reg, '');
    }
    this.command('save-pref', { name: 'collapsed_folders', value: this.env.collapsed_folders });
    this.set_unread_count_display(name, false);
    if (!this.drag_active) {
      this.command('save-pref', { name: prefname, value: this.env[prefname] });
      if (this.env.unread_counts)
        this.set_unread_count_display(node.id, false);
    }
  };
  this.doc_mouse_up = function(e)
@@ -1493,9 +1443,9 @@
    if (this.drag_active && model && this.env.last_folder_target) {
      var target = model[this.env.last_folder_target];
      $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget');
      this.env.last_folder_target = null;
      list.draglayer.hide();
      this.drag_end(e);
      if (!this.drag_menu(e, target))
        this.command('moveto', target);
@@ -1553,7 +1503,7 @@
    // start timer for message preview (wait for double click)
    if (selected && this.env.contentframe && !list.multi_selecting && !this.dummy_select)
      this.preview_timer = setTimeout(function(){ ref.msglist_get_preview(); }, 200);
      this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time);
    else if (this.env.contentframe)
      this.show_contentframe(false);
  };
@@ -1569,12 +1519,13 @@
    var win = this.get_frame_window(this.env.contentframe);
    if (win && win.location.href.indexOf(this.env.blankpage)>=0) {
    if (win && win.location.href.indexOf(this.env.blankpage) >= 0) {
      if (this.preview_timer)
        clearTimeout(this.preview_timer);
      if (this.preview_read_timer)
        clearTimeout(this.preview_read_timer);
      this.preview_timer = setTimeout(function(){ ref.msglist_get_preview(); }, 200);
      this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time);
    }
  };
@@ -1582,11 +1533,11 @@
  {
    if (this.preview_timer)
      clearTimeout(this.preview_timer);
    if (this.preview_read_timer)
      clearTimeout(this.preview_read_timer);
    var uid = list.get_single_selection();
    if (uid && this.env.mailbox == this.env.drafts_mailbox)
      this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox });
    else if (uid)
@@ -1626,7 +1577,7 @@
  this.msglist_set_coltypes = function(list)
  {
    var i, found, name, cols = list.list.tHead.rows[0].cells;
    var i, found, name, cols = list.thead.rows[0].cells;
    this.env.coltypes = [];
@@ -1674,16 +1625,28 @@
    return 0;
  };
  this.open_window = function(url, width, height)
  // open popup window
  this.open_window = function(url, small, toolbar)
  {
    var w = Math.min(width, screen.width - 10),
      h = Math.min(height, screen.height - 100),
      l = (screen.width - w) / 2 + (screen.left || 0),
      t = Math.max(0, (screen.height - h) / 2 + (screen.top || 0) - 20);
    var wname = 'rcmextwin' + new Date().getTime();
    var wname = 'rcmextwin' + new Date().getTime(),
      extwin = window.open(url + '&_extwin=1', wname, 'width='+w+',height='+h+',top='+t+',left='+l+',resizable=yes,toolbar=no,status=no');
    extwin.moveTo(l,t);
    url += (url.match(/\?/) ? '&' : '?') + '_extwin=1';
    if (this.env.standard_windows)
      extwin = window.open(url, wname);
    else {
      var win = this.is_framed() ? parent.window : window,
        page = $(win),
        page_width = page.width(),
        page_height = bw.mz ? $('body', win).height() : page.height(),
        w = Math.min(small ? this.env.popup_width_small : this.env.popup_width, page_width),
        h = page_height, // always use same height
        l = (win.screenLeft || win.screenX) + 20,
        t = (win.screenTop || win.screenY) + 20,
        extwin = window.open(url, wname,
          'width='+w+',height='+h+',top='+t+',left='+l+',resizable=yes,location=no,scrollbars=yes'
          +(toolbar ? ',toolbar=yes,menubar=yes,status=yes' : ',toolbar=no,menubar=no,status=no'));
    }
    // write loading... message to empty windows
    if (!url && extwin.document) {
@@ -1691,9 +1654,9 @@
    }
    // focus window, delayed to bring to front
    window.setTimeout(function(){ extwin.focus(); }, 10);
    window.setTimeout(function() { extwin.focus(); }, 10);
    return wname;
    return extwin;
  };
@@ -1777,10 +1740,7 @@
        + (flags.flagged ? ' flagged' : '')
        + (flags.unread_children && flags.seen && !this.env.autoexpand_threads ? ' unroot' : '')
        + (message.selected ? ' selected' : ''),
      // for performance use DOM instead of jQuery here
      row = document.createElement('tr');
    row.id = 'rcmrow'+uid;
      row = { cols:[], style:{}, id:'rcmrow'+uid };
    // message status icons
    css_class = 'msgicon';
@@ -1844,8 +1804,7 @@
    // add each submitted col
    for (n in this.env.coltypes) {
      c = this.env.coltypes[n];
      col = document.createElement('td');
      col.className = String(c).toLowerCase();
      col = { className: String(c).toLowerCase() };
      if (c == 'flag') {
        css_class = (flags.flagged ? 'flagged' : 'unflagged');
@@ -1890,8 +1849,7 @@
        html = cols[c];
      col.innerHTML = html;
      row.appendChild(col);
      row.cols.push(col);
    }
    list.insert_row(row, attop);
@@ -1992,7 +1950,7 @@
    }
    else {
      if (!preview && this.env.message_extwin && !this.env.extwin)
        this.open_window(this.env.comm_path+url, 1000, 1200);
        this.open_window(this.env.comm_path+url, true);
      else
        this.location_href(this.env.comm_path+url, target, true);
@@ -2254,7 +2212,7 @@
    if (root)
      row = rows[root] ? rows[root].obj : null;
    else
      row = this.message_list.list.tBodies[0].firstChild;
      row = this.message_list.tbody.firstChild;
    while (row) {
      if (row.nodeType == 1 && (r = rows[row.uid])) {
@@ -2430,7 +2388,7 @@
  this.delete_excessive_thread_rows = function()
  {
    var rows = this.message_list.rows,
      tbody = this.message_list.list.tBodies[0],
      tbody = this.message_list.tbody,
      row = tbody.firstChild,
      cnt = this.env.pagesize + 1;
@@ -3018,11 +2976,12 @@
    // open new compose window
    if (this.env.compose_extwin && !this.env.extwin) {
      this.open_window(url, 1150, 900);
      this.open_window(url);
    }
    else {
      this.redirect(url);
      window.resizeTo(Math.max(1150, $(window).width()), Math.max(900, $(window).height()));
      if (this.env.extwin)
        window.resizeTo(Math.max(this.env.popup_width, $(window).width()), $(window).height() + 24);
    }
  };
@@ -3038,10 +2997,10 @@
      input_message = $("[name='_message']").get(0),
      html_mode = $("input[name='_is_html']").val() == '1',
      ac_fields = ['cc', 'bcc', 'replyto', 'followupto'],
      ac_props;
      ac_props, opener_rc = this.opener();
    // close compose step in opener
    if (window.opener && !window.opener.closed && opener.rcmail && opener.rcmail.env.action == 'compose') {
    if (opener_rc && opener_rc.env.action == 'compose') {
      setTimeout(function(){ opener.history.back(); }, 100);
      this.env.opened_extwin = true;
    }
@@ -3113,6 +3072,13 @@
    form._draft.value = draft ? '1' : '';
    form.action = this.add_url(form.action, '_unlock', msgid);
    form.action = this.add_url(form.action, '_lang', lang);
    // register timer to notify about connection timeout
    this.submit_timer = setTimeout(function(){
      ref.set_busy(false, null, msgid);
      ref.display_message(ref.get_label('requesttimedout'), 'error');
    }, this.env.request_timeout * 1000);
    form.submit();
  };
@@ -3123,7 +3089,7 @@
  this.compose_add_recipient = function(field)
  {
    var recipients = [], input = $('#_'+field);
    var recipients = [], input = $('#_'+field), delim = this.env.recipients_delimiter;
    if (this.contact_list && this.contact_list.selection.length) {
      for (var id, n=0; n < this.contact_list.selection.length; n++) {
@@ -3142,8 +3108,10 @@
    }
    if (recipients.length && input.length) {
      var oldval = input.val();
      input.val((oldval ? oldval + this.env.recipients_delimiter : '') + recipients.join(this.env.recipients_delimiter));
      var oldval = input.val(), rx = new RegExp(RegExp.escape(delim) + '\\s*$');
      if (oldval && !rx.test(oldval))
        oldval += delim + ' ';
      input.val(oldval + recipients.join(delim + ' ') + delim + ' ');
      this.triggerEvent('add-recipient', { field:field, recipients:recipients });
    }
  };
@@ -3333,6 +3301,15 @@
  this.set_draft_id = function(id)
  {
    var rc;
    if (!this.env.draft_id && id && (rc = this.opener())) {
      // refresh the drafts folder in opener window
      if (rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == this.env.drafts_mailbox)
        rc.command('checkmail');
    }
    this.env.draft_id = id;
    $("input[name='_draft_saveid']").val(id);
  };
@@ -3377,12 +3354,45 @@
    if (!show_sig)
      show_sig = this.env.show_sig;
    var cursor_pos, p = -1,
    var i, rx, cursor_pos, p = -1,
      id = obj.options[obj.selectedIndex].value,
      input_message = $("[name='_message']"),
      message = input_message.val(),
      is_html = ($("input[name='_is_html']").val() == '1'),
      sig = this.env.identity;
      sig = this.env.identity,
      delim = this.env.recipients_delimiter,
      headers = ['replyto', 'bcc'];
    // update reply-to/bcc fields with addresses defined in identities
    for (i in headers) {
      var key = headers[i],
        old_val = sig && this.env.identities[sig] ? this.env.identities[sig][key] : '',
        new_val = id && this.env.identities[id] ? this.env.identities[id][key] : '',
        input = $('[name="_'+key+'"]'), input_val = input.val();
      // remove old address(es)
      if (old_val && input_val) {
        rx = new RegExp('\\s*' + RegExp.escape(old_val) + '\\s*');
        input_val = input_val.replace(rx, '');
      }
      // cleanup
      rx = new RegExp(RegExp.escape(delim) + '\\s*' + RegExp(delim), 'g');
      input_val = input_val.replace(rx, delim)
      rx = new RegExp('^\\s*' + RegExp.escape(delim) + '\\s*$');
      input_val = input_val.replace(rx, '')
      // add new address(es)
      if (new_val) {
        rx = new RegExp(RegExp.escape(delim) + '\\s*$');
        if (input_val && !rx.test(input_val))
          input_val += delim + ' ';
        input_val += new_val + delim + ' ';
      }
      if (old_val || new_val)
        input.val(input_val).change();
    }
    // enable manual signature insert
    if (this.env.signatures && this.env.signatures[id]) {
@@ -3398,7 +3408,7 @@
        sig = this.env.signatures[sig].text;
        sig = sig.replace(/\r\n/g, '\n');
        p = this.env.sig_above ? message.indexOf(sig) : message.lastIndexOf(sig);
        p = this.env.top_posting ? message.indexOf(sig) : message.lastIndexOf(sig);
        if (p >= 0)
          message = message.substring(0, p) + message.substring(p+sig.length, message.length);
      }
@@ -3407,7 +3417,7 @@
        sig = this.env.signatures[id].text;
        sig = sig.replace(/\r\n/g, '\n');
        if (this.env.sig_above) {
        if (this.env.top_posting) {
          if (p >= 0) { // in place of removed signature
            message = message.substring(0, p) + sig + message.substring(p, message.length);
            cursor_pos = p - 1;
@@ -3451,7 +3461,7 @@
        sigElem = doc.createElement('div');
        sigElem.setAttribute('id', '_rc_sig');
        if (this.env.sig_above) {
        if (this.env.top_posting) {
          // if no existing sig and top posting then insert at caret pos
          editor.getWin().focus(); // correct focus in IE & Chrome
@@ -3652,7 +3662,8 @@
      // reset vars
      this.env.current_page = 1;
      r = this.http_request('search', url, lock);
      var action = this.env.action == 'compose' && this.contact_list ? 'search-contacts' : 'search';
      r = this.http_request(action, url, lock);
      this.env.qsearch = {lock: lock, request: r};
    }
@@ -3707,14 +3718,19 @@
    this.env.search_id = null;
  };
  this.sent_successfully = function(type, msg)
  this.sent_successfully = function(type, msg, target)
  {
    this.display_message(msg, type);
    if (this.env.extwin) {
      var rc = this.opener();
      this.lock_form(this.gui_objects.messageform);
      if (window.opener && !window.opener.closed && opener.rcmail)
        opener.rcmail.display_message(msg, type);
      if (rc) {
        rc.display_message(msg, type);
        // refresh the folder where sent message was saved
        if (target && rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == target)
          rc.command('checkmail');
      }
      setTimeout(function(){ window.close() }, 1000);
    }
    else {
@@ -4089,6 +4105,7 @@
    var n, id, sid, ref = this, writable = false,
      source = this.env.source ? this.env.address_sources[this.env.source] : null;
    // we don't have dblclick handler here, so use 200 instead of this.dblclick_time
    if (id = list.get_single_selection())
      this.preview_timer = setTimeout(function(){ ref.load_contact(id, 'show'); }, 200);
    else if (this.env.contentframe)
@@ -4147,10 +4164,10 @@
    if (this.env.search_id)
      folder = 'S'+this.env.search_id;
    else
    else if (!this.env.search_request)
      folder = group ? 'G'+src+group : src;
    this.select_folder(folder);
    this.select_folder(folder, '', true);
    this.env.source = src;
    this.env.group = group;
@@ -4200,7 +4217,7 @@
    this.env.source = src;
    this.env.group = group;
    // also send search request to get the right messages
    // also send search request to get the right records
    if (this.env.search_request)
      url._search = this.env.search_request;
@@ -4281,7 +4298,7 @@
        this.group_member_change('add', cid, dest, to.id);
      else {
        var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
          post_data = {_cid: cid, _source: source, _to: dest, _togid: to.id, _gid: group};
          post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group};
        this.http_post('copy', post_data, lock);
      }
@@ -4289,7 +4306,7 @@
    // target is an addressbook
    else if (to.id != source) {
      var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
        post_data = {_cid: cid, _source: source, _to: to.id, _gid: group};
        post_data = {_cid: cid, _source: this.env.source, _to: to.id, _gid: group};
      this.http_post('copy', post_data, lock);
    }
@@ -4351,21 +4368,7 @@
        newcid = newcid+'-'+source;
    }
    if (list.rows[cid] && (row = list.rows[cid].obj)) {
      for (c=0; c<cols_arr.length; c++)
        if (row.cells[c])
          $(row.cells[c]).html(cols_arr[c]);
      // cid change
      if (newcid) {
        newcid = this.html_identifier(newcid);
        row.id = 'rcmrow' + newcid;
        list.remove_row(cid);
        list.init_row(row);
        list.selection[0] = newcid;
        row.style.display = '';
      }
    }
    list.update_row(cid, cols_arr, newcid, true);
  };
  // add row to contacts list
@@ -4375,7 +4378,7 @@
      return false;
    var c, col, list = this.contact_list,
      row = document.createElement('tr');
      row = { cols:[] };
    row.id = 'rcmrow'+this.html_identifier(cid);
    row.className = 'contact ' + (classes || '');
@@ -4385,10 +4388,10 @@
    // add each submitted col
    for (c in cols) {
      col = document.createElement('td');
      col = {};
      col.className = String(c).toLowerCase();
      col.innerHTML = cols[c];
      row.appendChild(col);
      row.cols.push(col);
    }
    list.insert_row(row);
@@ -4454,7 +4457,7 @@
      this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
      this.env.group_renaming = true;
      var link, li = this.get_folder_li(this.env.source+this.env.group, 'rcmliG');
      var link, li = this.get_folder_li('G'+this.env.source+this.env.group,'',true);
      if (li && (link = li.firstChild)) {
        $(link).hide().before(this.name_input);
      }
@@ -4474,11 +4477,8 @@
  // callback from server upon group-delete command
  this.remove_group_item = function(prop)
  {
    var li, key = 'G'+prop.source+prop.id;
    if ((li = this.get_folder_li(key))) {
      this.triggerEvent('group_delete', { source:prop.source, id:prop.id, li:li });
      li.parentNode.removeChild(li);
    var key = 'G'+prop.source+prop.id;
    if (this.treelist.remove(key)) {
      delete this.env.contactfolders[key];
      delete this.env.contactgroups[key];
    }
@@ -4497,8 +4497,22 @@
      this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
      this.name_input_li = $('<li>').addClass(type).append(this.name_input);
      var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : this.get_folder_li(this.env.source);
      this.name_input_li.insertAfter(li);
      var ul, li;
      // find list (UL) element
      if (type == 'contactsearch')
        ul = this.gui_objects.folderlist;
      else
        ul = $('ul.groups', this.get_folder_li(this.env.source,'',true));
      // append to the list
      li = $('li:last', ul);
      if (li.length)
        this.name_input_li.insertAfter(li);
      else {
        this.name_input_li.appendTo(ul);
        ul.show(); // make sure the list is visible
      }
    }
    this.name_input.select().focus();
@@ -4555,11 +4569,13 @@
  this.reset_add_input = function()
  {
    if (this.name_input) {
      var li = this.name_input.parent();
      if (this.env.group_renaming) {
        var li = this.name_input.parent();
        li.children().last().show();
        this.env.group_renaming = false;
      }
      else if ($('li', li.parent()).length == 1)
        li.parent().hide();
      this.name_input.remove();
@@ -4582,14 +4598,12 @@
      link = $('<a>').attr('href', '#')
        .attr('rel', prop.source+':'+prop.id)
        .click(function() { return rcmail.command('listgroup', prop, this); })
        .html(prop.name),
      li = $('<li>').attr({id: 'rcmli'+this.html_identifier(key), 'class': 'contactgroup'})
        .append(link);
        .html(prop.name);
    this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
    this.add_contact_group_row(prop, li);
    this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, true);
    this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:li[0] });
    this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key) });
  };
  // callback for renaming a contact group
@@ -4598,15 +4612,13 @@
    this.reset_add_input();
    var key = 'G'+prop.source+prop.id,
      li = this.get_folder_li(key),
      link;
      newnode = {};
    // group ID has changed, replace link node and identifiers
    if (li && prop.newid) {
    if (prop.newid) {
      var newkey = 'G'+prop.source+prop.newid,
        newprop = $.extend({}, prop);;
        newprop = $.extend({}, prop);
      li.id = 'rcmli' + this.html_identifier(newkey);
      this.env.contactfolders[newkey] = this.env.contactfolders[key];
      this.env.contactfolders[newkey].id = prop.newid;
      this.env.group = prop.newid;
@@ -4617,45 +4629,22 @@
      newprop.id = prop.newid;
      newprop.type = 'group';
      link = $('<a>').attr('href', '#')
      newnode.id = newkey;
      newnode.html = $('<a>').attr('href', '#')
        .attr('rel', prop.source+':'+prop.newid)
        .click(function() { return rcmail.command('listgroup', newprop, this); })
        .html(prop.name);
      $(li).children().replaceWith(link);
    }
    // update displayed group name
    else if (li && (link = li.firstChild) && link.tagName.toLowerCase() == 'a')
      link.innerHTML = prop.name;
    this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
    this.add_contact_group_row(prop, $(li), true);
    this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:li[0], newid:prop.newid });
  };
  // add contact group row to the list, with sorting
  this.add_contact_group_row = function(prop, li, reloc)
  {
    var row, name = prop.name.toUpperCase(),
      sibling = this.get_folder_li(prop.source),
      prefix = 'rcmliG' + this.html_identifier(prop.source);
    // When renaming groups, we need to remove it from DOM and insert it in the proper place
    if (reloc) {
      row = li.clone(true);
      li.remove();
    else {
      $(this.treelist.get_item(key)).children().first().html(prop.name);
      this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
    }
    else
      row = li;
    $('li[id^="'+prefix+'"]', this.gui_objects.folderlist).each(function(i, elem) {
      if (name >= $(this).text().toUpperCase())
        sibling = elem;
      else
        return false;
    });
    // update list node and re-sort it
    this.treelist.update(key, newnode, true);
    row.insertAfter(sibling);
    this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key), newid:prop.newid });
  };
  this.update_group_commands = function()
@@ -4887,45 +4876,14 @@
        .attr('rel', id)
        .click(function() { return rcmail.command('listsearch', id, this); })
        .html(name),
      li = $('<li>').attr({id: 'rcmli' + this.html_identifier(key), 'class': 'contactsearch'})
        .append(link),
      prop = {name:name, id:id, li:li[0]};
      prop = { name:name, id:id };
    this.add_saved_search_row(prop, li);
    this.select_folder('S'+id);
    this.treelist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch');
    this.select_folder(key,'',true);
    this.enable_command('search-delete', true);
    this.env.search_id = id;
    this.triggerEvent('abook_search_insert', prop);
  };
  // add saved search row to the list, with sorting
  this.add_saved_search_row = function(prop, li, reloc)
  {
    var row, sibling, name = prop.name.toUpperCase();
    // When renaming groups, we need to remove it from DOM and insert it in the proper place
    if (reloc) {
      row = li.clone(true);
      li.remove();
    }
    else
      row = li;
    $('li[class~="contactsearch"]', this.gui_objects.folderlist).each(function(i, elem) {
      if (!sibling)
        sibling = this.previousSibling;
      if (name >= $(this).text().toUpperCase())
        sibling = elem;
      else
        return false;
    });
    if (sibling)
      row.insertAfter(sibling);
    else
      row.appendTo(this.gui_objects.folderlist);
  };
  // creates an input for saved search name
@@ -4946,10 +4904,8 @@
  this.remove_search_item = function(id)
  {
    var li, key = 'S'+id;
    if ((li = this.get_folder_li(key))) {
    if (this.treelist.remove(key)) {
      this.triggerEvent('search_delete', { id:id, li:li });
      li.parentNode.removeChild(li);
    }
    this.env.search_id = null;
@@ -4968,7 +4924,7 @@
    }
    this.reset_qsearch();
    this.select_folder('S'+id);
    this.select_folder('S'+id, '', true);
    // reset vars
    this.env.current_page = 1;
@@ -5047,17 +5003,15 @@
  this.update_identity_row = function(id, name, add)
  {
    var row, col, list = this.identity_list,
    var list = this.identity_list,
      rid = this.html_identifier(id);
    if (list.rows[rid] && (row = list.rows[rid].obj)) {
      $(row.cells[0]).html(name);
    }
    else if (add) {
      row = $('<tr>').attr('id', 'rcmrow'+rid).get(0);
      col = $('<td>').addClass('mail').html(name).appendTo(row);
      list.insert_row(row);
    if (add) {
      list.insert_row({ id:'rcmrow'+rid, cols:[ { className:'mail', innerHTML:name } ] });
      list.select(rid);
    }
    else {
      list.update_row(rid, [ name ]);
    }
  };
@@ -5683,14 +5637,15 @@
    if (!this.gui_objects.message)
      return;
    var k, n, i, msg, m = this.messages;
    var k, n, i, o, m = this.messages;
    // Hide message by object, don't use for 'loading'!
    if (typeof obj === 'object') {
      $(obj)[fade?'fadeOut':'hide']();
      msg = $(obj).data('key');
      if (this.messages[msg])
        delete this.messages[msg];
      o = $(obj);
      k = o.data('key');
      this.hide_message_object(o, fade);
      if (m[k])
        delete m[k];
    }
    // Hide message by id
    else {
@@ -5700,7 +5655,7 @@
            m[k].elements.splice(n, 1);
            // hide DOM element if last instance is removed
            if (!m[k].elements.length) {
              m[k].obj[fade?'fadeOut':'hide']();
              this.hide_message_object(m[k].obj, fade);
              delete m[k];
            }
            // set pending action label for 'loading' message
@@ -5710,15 +5665,24 @@
                  delete m[k].labels[i];
                }
                else {
                  msg = m[k].labels[i].msg;
                  o = m[k].labels[i].msg;
                  m[k].obj.html(o);
                }
                m[k].obj.html(msg);
              }
            }
          }
        }
      }
    }
  };
  // hide message object and remove from the DOM
  this.hide_message_object = function(o, fade)
  {
    if (fade)
      o.fadeOut(600, function() {$(this).remove(); });
    else
      o.hide().remove();
  };
  // remove all messages immediately
@@ -5733,17 +5697,17 @@
    for (k in m)
      for (n in m[k].elements)
        if (m[k].obj)
          m[k].obj.hide();
          this.hide_message_object(m[k].obj);
    this.messages = {};
  };
  // open a jquery UI dialog with the given content
  this.show_popup_dialog = function(html, title)
  this.show_popup_dialog = function(html, title, buttons)
  {
    // forward call to parent window
    if (this.is_framed()) {
      parent.rcmail.show_popup_dialog(html, title);
      parent.rcmail.show_popup_dialog(html, title, buttons);
      return;
    }
@@ -5751,17 +5715,21 @@
      .html(html)
      .dialog({
        title: title,
        buttons: buttons,
        modal: true,
        resizable: true,
        width: 580,
        width: 500,
        close: function(event, ui) { $(this).remove() }
      });
      // resize and center popup
      var win = $(window), w = win.width(), h = win.height(),
        width = popup.width(), height = popup.height();
      popup.dialog('option', { height: Math.min(h-40, height+50), width: Math.min(w-20, width+50) })
        .dialog('option', 'position', ['center', 'center']);  // only works in a separate call (!?)
    // resize and center popup
    var win = $(window), w = win.width(), h = win.height(),
      width = popup.width(), height = popup.height();
    popup.dialog('option', {
      height: Math.min(h - 40, height + 75 + (buttons ? 50 : 0)),
      width: Math.min(w - 20, width + 20)
    });
  };
  // enable/disable buttons for page shifting
@@ -5774,7 +5742,10 @@
  // mark a mailbox as selected and set environment variable
  this.select_folder = function(name, prefix, encode)
  {
    if (this.gui_objects.folderlist) {
    if (this.treelist) {
      this.treelist.select(name);
    }
    else if (this.gui_objects.folderlist) {
      var current_li, target_li;
      if ((current_li = $('li.selected', this.gui_objects.folderlist))) {
@@ -5820,7 +5791,7 @@
  this.set_message_coltypes = function(coltypes, repl, smart_col)
  {
    var list = this.message_list,
      thead = list ? list.list.tHead : null,
      thead = list ? list.thead : null,
      cell, col, n, len, th, tr;
    this.env.coltypes = coltypes;
@@ -5833,7 +5804,7 @@
        for (c=0, len=repl.length; c < len; c++) {
          cell = document.createElement('td');
          cell.innerHTML = repl[c].html;
          cell.innerHTML = repl[c].html || '';
          if (repl[c].id) cell.id = repl[c].id;
          if (repl[c].className) cell.className = repl[c].className;
          tr.appendChild(cell);
@@ -6062,9 +6033,9 @@
    var base = this.env.comm_path, k, param = {};
    // overwrite task name
    if (query._action.match(/([a-z]+)\/([a-z0-9-_.]+)/)) {
    if (query._action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) {
      query._action = RegExp.$2;
      base = base.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
      base = base.replace(/\_task=[a-z0-9_-]+/, '_task='+RegExp.$1);
    }
    // remove undefined values
@@ -6338,12 +6309,29 @@
    // redirect to url specified in location header if not empty
    var location_url = request.getResponseHeader("Location");
    if (location_url)
    if (location_url && this.env.action != 'compose')  // don't redirect on compose screen, contents might get lost (#1488926)
      this.redirect(location_url);
    // 403 Forbidden response (CSRF prevention) - reload the page.
    // In case there's a new valid session it will be used, otherwise
    // login form will be presented (#1488960).
    if (request.status == 403) {
      (this.is_framed() ? parent : window).location.reload();
      return;
    }
    // re-send keep-alive requests after 30 seconds
    if (action == 'keep-alive')
      setTimeout(function(){ ref.keep_alive(); ref.start_keepalive(); }, 30000);
  };
  // callback when an iframe finished loading
  this.iframe_loaded = function(unlock)
  {
    this.set_busy(false, null, unlock);
    if (this.submit_timer)
      clearTimeout(this.submit_timer);
  };
  // post the given form to a hidden iframe
@@ -6445,6 +6433,7 @@
        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,
        timeout: 0, // disable default timeout set in ajaxSetup()
        data: formdata || multipart,
        headers: {'X-Roundcube-Request': ref.env.request_token},
        beforeSend: function(xhr, s) { if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; },
@@ -6584,6 +6573,17 @@
  /*********            helper methods            *********/
  /********************************************************/
  // get window.opener.rcmail if available
  this.opener = function()
  {
    // catch Error: Permission denied to access property rcmail
    try {
      if (window.opener && !opener.closed && opener.rcmail)
        return opener.rcmail;
    }
    catch (e) {}
  };
  // check if we're in show mode or if we have a unique selection
  // and return the message uid
  this.get_single_uid = function()