New Message View (with attachments thumbnails)

This commit is contained in:
RainLoop Team 2014-12-24 21:55:18 +04:00
parent 1c7f1928c7
commit 06e6f1a2da
22 changed files with 3046 additions and 162 deletions

View file

@ -57,6 +57,15 @@
return this.sServer + '/Raw/' + this.sSubQuery + this.sSpecSuffix + '/View/' + this.sSubSubQuery + sDownload;
};
/**
* @param {string} sDownload
* @return {string}
*/
Links.prototype.attachmentThumbnailPreview = function (sDownload)
{
return this.sServer + '/Raw/' + this.sSubQuery + this.sSpecSuffix + '/ViewThumbnail/' + this.sSubSubQuery + sDownload;
};
/**
* @param {string} sDownload
* @return {string}

View file

@ -27,6 +27,7 @@
this.friendlySize = '';
this.isInline = false;
this.isLinked = false;
this.isThumbnail = false;
this.cid = '';
this.cidWithOutTags = '';
this.contentLocation = '';
@ -56,6 +57,7 @@
AttachmentModel.prototype.friendlySize = '';
AttachmentModel.prototype.isInline = false;
AttachmentModel.prototype.isLinked = false;
AttachmentModel.prototype.isThumbnail = false;
AttachmentModel.prototype.cid = '';
AttachmentModel.prototype.cidWithOutTags = '';
AttachmentModel.prototype.contentLocation = '';
@ -78,6 +80,7 @@
this.estimatedSize = Utils.pInt(oJsonAttachment.EstimatedSize);
this.isInline = !!oJsonAttachment.IsInline;
this.isLinked = !!oJsonAttachment.IsLinked;
this.isThumbnail = !!oJsonAttachment.IsThumbnail;
this.cid = oJsonAttachment.CID;
this.contentLocation = oJsonAttachment.ContentLocation;
this.download = oJsonAttachment.Download;
@ -106,6 +109,14 @@
);
};
/**
* @return {boolean}
*/
AttachmentModel.prototype.hasThumbnail = function ()
{
return this.isThumbnail;
};
/**
* @return {boolean}
*/
@ -132,6 +143,14 @@
!this.isPdf() && !this.isText() && !this.isImage();
};
/**
* @return {boolean}
*/
AttachmentModel.prototype.hasPreview = function ()
{
return this.isImage() || this.isPdf() || this.isText() || this.isFramed();
};
/**
* @return {string}
*/
@ -148,6 +167,15 @@
return Links.attachmentPreview(this.download);
};
/**
* @return {string}
*/
AttachmentModel.prototype.linkThumbnailPreviewStyle = function ()
{
return !this.hasThumbnail() ? '' :
'background:url(' + Links.attachmentThumbnailPreview(this.download) + ')';
};
/**
* @return {string}
*/

View file

@ -52,7 +52,7 @@ html.rl-no-preview-pane {
padding-left: 8px;
padding-right: 8px;
}
.b-message-list-wrapper {
position: absolute;
top: 50px;
@ -275,7 +275,7 @@ html.rl-no-preview-pane {
.flagParent {
display: inline-block;
float: right;
padding: 0 8px 0 5px;
padding: 0 10px 0 5px;
}
&.e-single-line .flagParent {
@ -299,7 +299,7 @@ html.rl-no-preview-pane {
display: inline-block;
float: right;
position: relative;
margin: 2px 8px 0 5px;
margin: 2px 10px 0 5px;
}
&.e-single-line .attachmentParent {
@ -374,7 +374,7 @@ html.rl-no-preview-pane {
overflow: hidden;
text-overflow: ellipsis;
}
.subject-prefix {
color: #888;
/*font-style: italic;*/
@ -449,11 +449,11 @@ html.rl-no-preview-pane {
.sidebarParent {
background-color: #bdc3c7;
}
&.focused .sidebarParent {
background-color: darken(#bdc3c7, 10%) !important;
}
&.unseen {
background-color: darken(#ecf0f1, 5%);
@ -489,13 +489,13 @@ html.rl-no-preview-pane {
background-color: #398CF2;
.opacity(20);
}
+ .messageListItem .delimiter {
background-color: #398CF2;
opacity: 0.3;
}
}
&.hasFlaggedSubMessage {
.flagOff, .flagOn {
display: none;

View file

@ -70,6 +70,13 @@ html.rl-no-preview-pane {
-webkit-overflow-scrolling: touch;
}
.messageItemDropdown {
z-index: 100;
position: fixed;
top: 70px;
right: 25px;
}
.messageItem {
position: absolute;
@ -94,8 +101,8 @@ html.rl-no-preview-pane {
.buttonUp, .buttonUnFull, .buttonFull {
display: inline-block;
position: fixed;
right: 30px;
top: 90px;
right: 25px;
bottom: 25px;
height: 30px;
width: 30px;
text-align: center;
@ -119,11 +126,11 @@ html.rl-no-preview-pane {
}
.buttonUp {
right: 70px;
right: 65px;
z-index: 0;
}
.buttonUnFull {
.buttonUp, .buttonUnFull {
display: none;
}
@ -230,8 +237,7 @@ html.rl-no-preview-pane {
display: inline-block;
margin: 5px;
padding: 5px;
max-width: 170px;
max-width: 200px;
min-width: 60px;
overflow: hidden;
cursor: pointer;
@ -246,19 +252,83 @@ html.rl-no-preview-pane {
border-radius: 2px;
.attachmentIcon {
font-size: 23px;
width: 23px;
height: 23px;
color: #666;
.attachmentIconParent {
height: 56px;
width: 60px;
background: none;
}
.attachmentPreview {
color: #999;
margin: 0px 5px;
}
.attachmentPreview:hover {
.attachmentNameParent {
margin-left: 60px;
padding: 4px;
padding-left: 6px;
min-width: 90px;
color: #fff;
background: #eee;
color: #333;
background: #fafafa;
border-left: 1px solid #ddd;
}
&.hasThumbnail .attachmentNameParent {
background: rgba(0, 0, 0, .6);
color: #fff;
text-shadow: 0 1px 0 #000;
border-left: 0px;
}
.attachmentIcon {
margin: 6px 0 0 13px;
font-size: 36px;
width: 36px;
height: 36px;
color: #aaa;
}
.attachmentIconParent.hasPreview {
.attachmentNonePreview {
display: none;
}
}
a.attachmentPreview {
display: inline-block;
height: 100%;
width: 100%;
}
.attachmentIconParent.hasPreview:hover {
background: #555 !important;
background-image: none !important;
background: rgba(0, 0, 0, .8) !important;
color: #fff;
.attachmentIcon {
color: #fff;
text-shadow: 0 1px 0 #000;
}
}
.attachmentIconParent.hasPreview .show-hover {
display: none;
}
.attachmentIconParent.hasPreview:hover .show-hover {
display: inline-block;
}
.attachmentIconParent.hasPreview:hover .hide-hover {
display: none;
}
&.hasThumbnail .attachmentMainIcon {
display: none;
}
}
}
@ -382,6 +452,7 @@ html.rl-no-preview-pane .messageView {
.toolbar {
padding-left: @rlMainBorderSize;
}
.b-content {
top: 50px;
bottom: @rlBottomMargin;
@ -389,10 +460,6 @@ html.rl-no-preview-pane .messageView {
border: @rlMainBorderSize solid @rlMainDarkColor;
box-shadow: @rlMainShadow;
border-radius: @rlMainBorderRadius;
.buttonUp, .buttonUnFull, .buttonFull {
top: 70px;
}
}
}
@ -423,9 +490,12 @@ html.rl-message-fullscreen {
border: @rlLowBorderSize solid @rlMainDarkColor;
border-radius: @rlLowBorderRadius;
.buttonUp, .buttonUnFull {
.messageItemDropdown {
top: 20px;
}
.buttonUnFull {
display: inline-block;
top: 36px;
}
.buttonFull {
@ -433,3 +503,7 @@ html.rl-message-fullscreen {
}
}
}
.nano-scrolllimit-top .buttonUp {
display: inline-block !important;
}

View file

@ -81,7 +81,10 @@
}
}, this);
this.canBeRepliedOrForwarded = this.messageVisibility;
this.canBeRepliedOrForwarded = ko.computed(function () {
var bV = this.messageVisibility();
return !this.isDraftFolder() && bV;
}, this);
// commands
this.closeMessage = Utils.createCommand(this, function () {

View file

@ -1,8 +1,8 @@
{
"name": "RainLoop",
"title": "RainLoop Webmail",
"version": "1.7.0",
"release": "211",
"version": "1.7.1",
"release": "215",
"description": "Simple, modern & fast web-based email client",
"homepage": "http://rainloop.net",
"main": "gulpfile.js",

View file

@ -35,6 +35,14 @@ if (!\defined('RAINLOOP_APP_LIBRARIES_PATH'))
{
return include RAINLOOP_APP_LIBRARIES_PATH.'GuzzleHttp/'.\str_replace('\\', '/', \substr($sClassName, 11)).'.php';
}
else if (0 === \strpos($sClassName, 'Symfony') && false !== \strpos($sClassName, '\\'))
{
return include RAINLOOP_APP_LIBRARIES_PATH.'Symfony/'.\str_replace('\\', '/', \substr($sClassName, 8)).'.php';
}
else if (0 === \strpos($sClassName, 'PHPThumb') && false !== \strpos($sClassName, '\\'))
{
return include RAINLOOP_APP_LIBRARIES_PATH.'PHPThumb/'.\str_replace('\\', '/', \substr($sClassName, 9)).'.php';
}
else if (0 === \strpos($sClassName, 'Sabre') && false !== \strpos($sClassName, '\\'))
{
if (!RAINLOOP_MB_SUPPORTED && !defined('RL_MB_FIXED'))
@ -67,7 +75,7 @@ if (\class_exists('RainLoop\Service'))
if (!\defined('APP_API_STARTED'))
{
\define('APP_API_STARTED', true);
\RainLoop\Api::Handle();
}
}

View file

@ -0,0 +1,161 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2014 Usenko Timur
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Base\StreamWrappers;
/**
* @category MailSo
* @package Base
* @subpackage StreamWrappers
*/
class TempFile
{
/**
* @var string
*/
const STREAM_NAME = 'mailsotempfile';
/**
* @var array
*/
private static $aStreams = array();
/**
* @var resource
*/
private $rSream;
public static function Reg()
{
if (!in_array(self::STREAM_NAME, stream_get_wrappers()))
{
stream_wrapper_register(self::STREAM_NAME, '\MailSo\Base\StreamWrappers\TempFile');
}
}
/**
* @param string $sHash
*
* @return resource|bool
*/
public static function CreateStream($sHash)
{
self::Reg();
return fopen(self::STREAM_NAME.'://'.$sHash, 'r+b');
}
/**
* @param string $sPath
*
* @return bool
*/
public function stream_open($sPath)
{
$bResult = false;
$aPath = parse_url($sPath);
if (isset($aPath['host']) && isset($aPath['scheme']) &&
0 < strlen($aPath['host']) && 0 < strlen($aPath['scheme']) &&
self::STREAM_NAME === $aPath['scheme'])
{
$sHashName = $aPath['host'];
if (isset(self::$aStreams[$sHashName]) &&
is_resource(self::$aStreams[$sHashName]))
{
$this->rSream = self::$aStreams[$sHashName];
$bResult = true;
}
else
{
$this->rSream = fopen('php://memory', 'r+b');
self::$aStreams[$sHashName] = $this->rSream;
$bResult = true;
\MailSo\Base\Loader::IncStatistic('CreateStream/TempFile');
}
}
return $bResult;
}
/**
* @return bool
*/
public function stream_close()
{
return -1 !== fseek($this->rSream, 0);
}
/**
* @return bool
*/
public function stream_flush()
{
return fflush($this->rSream);
}
/**
* @param int $iLen
*
* @return string
*/
public function stream_read($iLen)
{
return fread($this->rSream, $iLen);
}
/**
* @param string $sInputString
*
* @return int
*/
public function stream_write($sInputString)
{
return fwrite($this->rSream, $sInputString);
}
/**
* @return int
*/
public function stream_tell()
{
return ftell($this->rSream);
}
/**
* @return bool
*/
public function stream_eof()
{
return feof($this->rSream);
}
/**
* @return array
*/
public function stream_stat()
{
return fstat($this->rSream);
}
/**
* @param int $iOffset
* @param int $iWhence = SEEK_SET
*
* @return int
*/
public function stream_seek($iOffset, $iWhence = SEEK_SET)
{
return fseek($this->rSream, $iOffset, $iWhence);
}
}

View file

@ -1215,7 +1215,7 @@ class Utils
*/
public static function ClearFileName($sFileName)
{
return \MailSo\Base\Utils::ClearNullBite(\preg_replace('/[\s]+/', ' ',
return \MailSo\Base\Utils::ClearNullBite(\preg_replace('/[\s]+/u', ' ',
\str_replace(array('"', '/', '\\', '*', '?', '<', '>', '|', ':'), ' ', $sFileName)));
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,149 @@
<?php
namespace PHPThumb;
/**
* PhpThumb : PHP Thumb Library <http://phpthumb.gxdlabs.com>
* Copyright (c) 2009, Ian Selby/Gen X Design
*
* Author(s): Ian Selby <ian@gen-x-design.com>
*
* Licensed under the MIT License
* Redistributions of files must retain the above copyright notice.
*
* @author Ian Selby <ian@gen-x-design.com>
* @copyright Copyright (c) 2009 Gen X Design
* @link http://phpthumb.gxdlabs.com
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
abstract class PHPThumb
{
/**
* The name of the file we're manipulating
* This must include the path to the file (absolute paths recommended)
*
* @var string
*/
protected $fileName;
/**
* @var \Symfony\Component\Filesystem\Filesystem
*/
protected $filesystem;
/**
* What the file format is (mime-type)
*
* @var string
*/
protected $format;
/**
* Whether or not the image is hosted remotely
*
* @var bool
*/
protected $remoteImage;
/**
* An array of attached plugins to execute in order.
* @var array
*/
protected $plugins;
/**
* @param $fileName
* @param array $options
* @param array $plugins
*/
public function __construct($fileName, array $options = array(), array $plugins = array())
{
$this->filesystem = new \Symfony\Component\Filesystem\Filesystem();
$this->fileName = $fileName;
$this->remoteImage = false;
if(!$this->validateRequestedResource($fileName)) {
throw new \InvalidArgumentException("Image file not found: {$fileName}");
}
$this->setOptions($options);
$this->plugins = $plugins;
}
abstract public function setOptions(array $options = array());
/**
* Check the provided filename/url. If it is a url, validate that it is properly
* formatted. If it is a file, check to make sure that it actually exists on
* the filesystem.
*
* @param $filename
* @return bool
*/
protected function validateRequestedResource($filename)
{
if(false !== filter_var($filename, FILTER_VALIDATE_URL)) {
$this->remoteImage = true;
return true;
}
if($this->filesystem->exists($filename)) {
return true;
}
return false;
}
/**
* Returns the filename.
* @return string
*/
public function getFileName()
{
return $this->fileName;
}
/**
* Sets the filename.
* @param $fileName
* @return PHPThumb
*/
public function setFileName($fileName)
{
$this->fileName = $fileName;
return $this;
}
/**
* Returns the format.
* @return string
*/
public function getFormat()
{
return $this->format;
}
/**
* Sets the format.
* @param $format
* @return PHPThumb
*/
public function setFormat($format)
{
$this->format = $format;
return $this;
}
/**
* Returns whether the image exists remotely, i.e. it was loaded via a URL.
* @return bool
*/
public function getIsRemoteImage()
{
return $this->remoteImage;
}
}

View file

@ -0,0 +1,12 @@
<?php
namespace PHPThumb;
interface PluginInterface
{
/**
* @param PHPThumb $phpthumb
* @return PHPThumb
*/
public function execute($phpthumb);
}

View file

@ -0,0 +1,261 @@
<?php
namespace PHPThumb\Plugins;
/**
* GD Reflection Lib Plugin Definition File
*
* This file contains the plugin definition for the GD Reflection Lib for PHP Thumb
*
* PHP Version 5.3 with GD 2.0+
* PhpThumb : PHP Thumb Library <http://phpthumb.gxdlabs.com>
* Copyright (c) 2009, Ian Selby/Gen X Design
*
* Author(s): Ian Selby <ian@gen-x-design.com>
*
* Licensed under the MIT License
* Redistributions of files must retain the above copyright notice.
*
* @author Ian Selby <ian@gen-x-design.com>
* @copyright Copyright (c) 2009 Gen X Design
* @link http://phpthumb.gxdlabs.com
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @version 3.0
* @package PhpThumb
* @filesource
*/
/**
* GD Reflection Lib Plugin
*
* This plugin allows you to create those fun Apple(tm)-style reflections in your images
*
* @package PhpThumb
* @subpackage Plugins
*/
class Reflection implements \PHPThumb\PluginInterface
{
protected $currentDimensions;
protected $workingImage;
protected $newImage;
protected $options;
protected $percent;
protected $reflection;
protected $white;
protected $border;
protected $borderColor;
public function __construct($percent, $reflection, $white, $border, $borderColor)
{
$this->percent = $percent;
$this->reflection = $reflection;
$this->white = $white;
$this->border = $border;
$this->borderColor = $borderColor;
}
/**
* @param \PHPThumb\PHPThumb $phpthumb
* @return \PHPThumb\PHPThumb
*/
public function execute($phpthumb)
{
$this->currentDimensions = $phpthumb->getCurrentDimensions();
$this->workingImage = $phpthumb->getWorkingImage();
$this->newImage = $phpthumb->getOldImage();
$this->options = $phpthumb->getOptions();
$width = $this->currentDimensions['width'];
$height = $this->currentDimensions['height'];
$this->reflectionHeight = intval($height * ($this->reflection / 100));
$newHeight = $height + $this->reflectionHeight;
$reflectedPart = $height * ($this->percent / 100);
$this->workingImage = imagecreatetruecolor($width, $newHeight);
imagealphablending($this->workingImage, true);
$colorToPaint = imagecolorallocatealpha(
$this->workingImage,
255,
255,
255,
0
);
imagefilledrectangle(
$this->workingImage,
0,
0,
$width,
$newHeight,
$colorToPaint
);
imagecopyresampled(
$this->workingImage,
$this->newImage,
0,
0,
0,
$reflectedPart,
$width,
$this->reflectionHeight,
$width,
($height - $reflectedPart)
);
$this->imageFlipVertical();
imagecopy(
$this->workingImage,
$this->newImage,
0,
0,
0,
0,
$width,
$height
);
imagealphablending($this->workingImage, true);
for ($i = 0; $i < $this->reflectionHeight; $i++) {
$colorToPaint = imagecolorallocatealpha(
$this->workingImage,
255,
255,
255,
($i / $this->reflectionHeight * -1 + 1) * $this->white
);
imagefilledrectangle(
$this->workingImage,
0,
$height + $i,
$width,
$height + $i,
$colorToPaint
);
}
if ($this->border == true) {
$rgb = $this->hex2rgb($this->borderColor, false);
$colorToPaint = imagecolorallocate($this->workingImage, $rgb[0], $rgb[1], $rgb[2]);
//top line
imageline(
$this->workingImage,
0,
0,
$width,
0,
$colorToPaint
);
//bottom line
imageline(
$this->workingImage,
0,
$height,
$width,
$height,
$colorToPaint
);
//left line
imageline(
$this->workingImage,
0,
0,
0,
$height,
$colorToPaint
);
//right line
imageline(
$this->workingImage,
$width - 1,
0,
$width - 1,
$height,
$colorToPaint
);
}
if ($phpthumb->getFormat() == 'PNG') {
$colorTransparent = imagecolorallocatealpha(
$this->workingImage,
$this->options['alphaMaskColor'][0],
$this->options['alphaMaskColor'][1],
$this->options['alphaMaskColor'][2],
0
);
imagefill($this->workingImage, 0, 0, $colorTransparent);
imagesavealpha($this->workingImage, true);
}
$phpthumb->setOldImage($this->workingImage);
$this->currentDimensions['width'] = $width;
$this->currentDimensions['height'] = $newHeight;
$phpthumb->setCurrentDimensions($this->currentDimensions);
return $phpthumb;
}
/**
* Flips the image vertically
*
*/
protected function imageFlipVertical ()
{
$x_i = imagesx($this->workingImage);
$y_i = imagesy($this->workingImage);
for ($x = 0; $x < $x_i; $x++) {
for ($y = 0; $y < $y_i; $y++) {
imagecopy(
$this->workingImage,
$this->workingImage,
$x,
$y_i - $y - 1,
$x,
$y,
1,
1
);
}
}
}
/**
* Converts a hex color to rgb tuples
*
* @return mixed
* @param string $hex
* @param bool $asString
*/
protected function hex2rgb ($hex, $asString = false)
{
// strip off any leading #
if (0 === strpos($hex, '#')) {
$hex = substr($hex, 1);
} elseif (0 === strpos($hex, '&H')) {
$hex = substr($hex, 2);
}
// break into hex 3-tuple
$cutpoint = ceil(strlen($hex) / 2)-1;
$rgb = explode(':', wordwrap($hex, $cutpoint, ':', $cutpoint), 3);
// convert each tuple to decimal
$rgb[0] = (isset($rgb[0]) ? hexdec($rgb[0]) : 0);
$rgb[1] = (isset($rgb[1]) ? hexdec($rgb[1]) : 0);
$rgb[2] = (isset($rgb[2]) ? hexdec($rgb[2]) : 0);
return ($asString ? "{$rgb[0]} {$rgb[1]} {$rgb[2]}" : $rgb);
}
}

View file

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Filesystem\Exception;
/**
* Exception interface for all exceptions thrown by the component.
*
* @author Romain Neutron <imprec@gmail.com>
*
* @api
*/
interface ExceptionInterface
{
}

View file

@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Filesystem\Exception;
/**
* Exception class thrown when a file couldn't be found
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Christian Gärtner <christiangaertner.film@googlemail.com>
*/
class FileNotFoundException extends IOException
{
public function __construct($message = null, $code = 0, \Exception $previous = null, $path = null)
{
if (null === $message) {
if (null === $path) {
$message = 'File could not be found.';
} else {
$message = sprintf('File "%s" could not be found.', $path);
}
}
parent::__construct($message, $code, $previous, $path);
}
}

View file

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Filesystem\Exception;
/**
* Exception class thrown when a filesystem operation failure happens
*
* @author Romain Neutron <imprec@gmail.com>
* @author Christian Gärtner <christiangaertner.film@googlemail.com>
* @author Fabien Potencier <fabien@symfony.com>
*
* @api
*/
class IOException extends \RuntimeException implements IOExceptionInterface
{
private $path;
public function __construct($message, $code = 0, \Exception $previous = null, $path = null)
{
$this->path = $path;
parent::__construct($message, $code, $previous);
}
/**
* {@inheritdoc}
*/
public function getPath()
{
return $this->path;
}
}

View file

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Filesystem\Exception;
/**
* IOException interface for file and input/output stream releated exceptions thrown by the component.
*
* @author Christian Gärtner <christiangaertner.film@googlemail.com>
*/
interface IOExceptionInterface extends ExceptionInterface
{
/**
* Returns the associated path for the exception
*
* @return string The path.
*/
public function getPath();
}

View file

@ -0,0 +1,474 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Filesystem;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
/**
* Provides basic utility to manipulate the file system.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Filesystem
{
/**
* Copies a file.
*
* This method only copies the file if the origin file is newer than the target file.
*
* By default, if the target already exists, it is not overridden.
*
* @param string $originFile The original filename
* @param string $targetFile The target filename
* @param boolean $override Whether to override an existing file or not
*
* @throws FileNotFoundException When orginFile doesn't exist
* @throws IOException When copy fails
*/
public function copy($originFile, $targetFile, $override = false)
{
if (stream_is_local($originFile) && !is_file($originFile)) {
throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile);
}
$this->mkdir(dirname($targetFile));
if (!$override && is_file($targetFile)) {
$doCopy = filemtime($originFile) > filemtime($targetFile);
} else {
$doCopy = true;
}
if ($doCopy) {
// https://bugs.php.net/bug.php?id=64634
$source = fopen($originFile, 'r');
$target = fopen($targetFile, 'w+');
stream_copy_to_stream($source, $target);
fclose($source);
fclose($target);
unset($source, $target);
if (!is_file($targetFile)) {
throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile);
}
}
}
/**
* Creates a directory recursively.
*
* @param string|array|\Traversable $dirs The directory path
* @param integer $mode The directory mode
*
* @throws IOException On any directory creation failure
*/
public function mkdir($dirs, $mode = 0777)
{
foreach ($this->toIterator($dirs) as $dir) {
if (is_dir($dir)) {
continue;
}
if (true !== @mkdir($dir, $mode, true)) {
throw new IOException(sprintf('Failed to create "%s".', $dir), 0, null, $dir);
}
}
}
/**
* Checks the existence of files or directories.
*
* @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to check
*
* @return Boolean true if the file exists, false otherwise
*/
public function exists($files)
{
foreach ($this->toIterator($files) as $file) {
if (!file_exists($file)) {
return false;
}
}
return true;
}
/**
* Sets access and modification time of file.
*
* @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to create
* @param integer $time The touch time as a unix timestamp
* @param integer $atime The access time as a unix timestamp
*
* @throws IOException When touch fails
*/
public function touch($files, $time = null, $atime = null)
{
foreach ($this->toIterator($files) as $file) {
$touch = $time ? @touch($file, $time, $atime) : @touch($file);
if (true !== $touch) {
throw new IOException(sprintf('Failed to touch "%s".', $file), 0, null, $file);
}
}
}
/**
* Removes files or directories.
*
* @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to remove
*
* @throws IOException When removal fails
*/
public function remove($files)
{
$files = iterator_to_array($this->toIterator($files));
$files = array_reverse($files);
foreach ($files as $file) {
if (!file_exists($file) && !is_link($file)) {
continue;
}
if (is_dir($file) && !is_link($file)) {
$this->remove(new \FilesystemIterator($file));
if (true !== @rmdir($file)) {
throw new IOException(sprintf('Failed to remove directory "%s".', $file), 0, null, $file);
}
} else {
// https://bugs.php.net/bug.php?id=52176
if (defined('PHP_WINDOWS_VERSION_MAJOR') && is_dir($file)) {
if (true !== @rmdir($file)) {
throw new IOException(sprintf('Failed to remove file "%s".', $file), 0, null, $file);
}
} else {
if (true !== @unlink($file)) {
throw new IOException(sprintf('Failed to remove file "%s".', $file), 0, null, $file);
}
}
}
}
}
/**
* Change mode for an array of files or directories.
*
* @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change mode
* @param integer $mode The new mode (octal)
* @param integer $umask The mode mask (octal)
* @param Boolean $recursive Whether change the mod recursively or not
*
* @throws IOException When the change fail
*/
public function chmod($files, $mode, $umask = 0000, $recursive = false)
{
foreach ($this->toIterator($files) as $file) {
if ($recursive && is_dir($file) && !is_link($file)) {
$this->chmod(new \FilesystemIterator($file), $mode, $umask, true);
}
if (true !== @chmod($file, $mode & ~$umask)) {
throw new IOException(sprintf('Failed to chmod file "%s".', $file), 0, null, $file);
}
}
}
/**
* Change the owner of an array of files or directories
*
* @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change owner
* @param string $user The new owner user name
* @param Boolean $recursive Whether change the owner recursively or not
*
* @throws IOException When the change fail
*/
public function chown($files, $user, $recursive = false)
{
foreach ($this->toIterator($files) as $file) {
if ($recursive && is_dir($file) && !is_link($file)) {
$this->chown(new \FilesystemIterator($file), $user, true);
}
if (is_link($file) && function_exists('lchown')) {
if (true !== @lchown($file, $user)) {
throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file);
}
} else {
if (true !== @chown($file, $user)) {
throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file);
}
}
}
}
/**
* Change the group of an array of files or directories
*
* @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change group
* @param string $group The group name
* @param Boolean $recursive Whether change the group recursively or not
*
* @throws IOException When the change fail
*/
public function chgrp($files, $group, $recursive = false)
{
foreach ($this->toIterator($files) as $file) {
if ($recursive && is_dir($file) && !is_link($file)) {
$this->chgrp(new \FilesystemIterator($file), $group, true);
}
if (is_link($file) && function_exists('lchgrp')) {
if (true !== @lchgrp($file, $group)) {
throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file);
}
} else {
if (true !== @chgrp($file, $group)) {
throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file);
}
}
}
}
/**
* Renames a file or a directory.
*
* @param string $origin The origin filename or directory
* @param string $target The new filename or directory
* @param Boolean $overwrite Whether to overwrite the target if it already exists
*
* @throws IOException When target file or directory already exists
* @throws IOException When origin cannot be renamed
*/
public function rename($origin, $target, $overwrite = false)
{
// we check that target does not exist
if (!$overwrite && is_readable($target)) {
throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target);
}
if (true !== @rename($origin, $target)) {
throw new IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target), 0, null, $target);
}
}
/**
* Creates a symbolic link or copy a directory.
*
* @param string $originDir The origin directory path
* @param string $targetDir The symbolic link name
* @param Boolean $copyOnWindows Whether to copy files if on Windows
*
* @throws IOException When symlink fails
*/
public function symlink($originDir, $targetDir, $copyOnWindows = false)
{
if (!function_exists('symlink') && $copyOnWindows) {
$this->mirror($originDir, $targetDir);
return;
}
$this->mkdir(dirname($targetDir));
$ok = false;
if (is_link($targetDir)) {
if (readlink($targetDir) != $originDir) {
$this->remove($targetDir);
} else {
$ok = true;
}
}
if (!$ok) {
if (true !== @symlink($originDir, $targetDir)) {
$report = error_get_last();
if (is_array($report)) {
if (defined('PHP_WINDOWS_VERSION_MAJOR') && false !== strpos($report['message'], 'error code(1314)')) {
throw new IOException('Unable to create symlink due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?');
}
}
throw new IOException(sprintf('Failed to create symbolic link from "%s" to "%s".', $originDir, $targetDir), 0, null, $targetDir);
}
}
}
/**
* Given an existing path, convert it to a path relative to a given starting path
*
* @param string $endPath Absolute path of target
* @param string $startPath Absolute path where traversal begins
*
* @return string Path of target relative to starting path
*/
public function makePathRelative($endPath, $startPath)
{
// Normalize separators on windows
if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
$endPath = strtr($endPath, '\\', '/');
$startPath = strtr($startPath, '\\', '/');
}
// Split the paths into arrays
$startPathArr = explode('/', trim($startPath, '/'));
$endPathArr = explode('/', trim($endPath, '/'));
// Find for which directory the common path stops
$index = 0;
while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
$index++;
}
// Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
$depth = count($startPathArr) - $index;
// Repeated "../" for each level need to reach the common path
$traverser = str_repeat('../', $depth);
$endPathRemainder = implode('/', array_slice($endPathArr, $index));
// Construct $endPath from traversing to the common path, then to the remaining $endPath
$relativePath = $traverser.(strlen($endPathRemainder) > 0 ? $endPathRemainder.'/' : '');
return (strlen($relativePath) === 0) ? './' : $relativePath;
}
/**
* Mirrors a directory to another.
*
* @param string $originDir The origin directory
* @param string $targetDir The target directory
* @param \Traversable $iterator A Traversable instance
* @param array $options An array of boolean options
* Valid options are:
* - $options['override'] Whether to override an existing file on copy or not (see copy())
* - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink())
* - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
*
* @throws IOException When file type is unknown
*/
public function mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array())
{
$targetDir = rtrim($targetDir, '/\\');
$originDir = rtrim($originDir, '/\\');
// Iterate in destination folder to remove obsolete entries
if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
$deleteIterator = $iterator;
if (null === $deleteIterator) {
$flags = \FilesystemIterator::SKIP_DOTS;
$deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST);
}
foreach ($deleteIterator as $file) {
$origin = str_replace($targetDir, $originDir, $file->getPathname());
if (!$this->exists($origin)) {
$this->remove($file);
}
}
}
$copyOnWindows = false;
if (isset($options['copy_on_windows']) && !function_exists('symlink')) {
$copyOnWindows = $options['copy_on_windows'];
}
if (null === $iterator) {
$flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS;
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST);
}
foreach ($iterator as $file) {
$target = str_replace($originDir, $targetDir, $file->getPathname());
if ($copyOnWindows) {
if (is_link($file) || is_file($file)) {
$this->copy($file, $target, isset($options['override']) ? $options['override'] : false);
} elseif (is_dir($file)) {
$this->mkdir($target);
} else {
throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file);
}
} else {
if (is_link($file)) {
$this->symlink($file, $target);
} elseif (is_dir($file)) {
$this->mkdir($target);
} elseif (is_file($file)) {
$this->copy($file, $target, isset($options['override']) ? $options['override'] : false);
} else {
throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file);
}
}
}
}
/**
* Returns whether the file path is an absolute path.
*
* @param string $file A file path
*
* @return Boolean
*/
public function isAbsolutePath($file)
{
if (strspn($file, '/\\', 0, 1)
|| (strlen($file) > 3 && ctype_alpha($file[0])
&& substr($file, 1, 1) === ':'
&& (strspn($file, '/\\', 2, 1))
)
|| null !== parse_url($file, PHP_URL_SCHEME)
) {
return true;
}
return false;
}
/**
* Atomically dumps content into a file.
*
* @param string $filename The file to be written to.
* @param string $content The data to write into the file.
* @param integer $mode The file mode (octal).
* @throws IOException If the file cannot be written to.
*/
public function dumpFile($filename, $content, $mode = 0666)
{
$dir = dirname($filename);
if (!is_dir($dir)) {
$this->mkdir($dir);
} elseif (!is_writable($dir)) {
throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir);
}
$tmpFile = tempnam($dir, basename($filename));
if (false === @file_put_contents($tmpFile, $content)) {
throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename);
}
$this->rename($tmpFile, $filename, true);
$this->chmod($filename, $mode);
}
/**
* @param mixed $files
*
* @return \Traversable
*/
private function toIterator($files)
{
if (!$files instanceof \Traversable) {
$files = new \ArrayObject(is_array($files) ? $files : array($files));
}
return $files;
}
}

View file

@ -2050,8 +2050,24 @@ class Actions
*/
public function DoFiltersSave()
{
sleep(2);
$aIncFilters = $this->GetActionParam('Filters', array());
$aFilters = array();
foreach ($aIncFilters as $aFilter)
{
if ($aFilter)
{
$oFilter = new \RainLoop\Providers\Filters\Classes\Filter();
if ($oFilter->FromJSON($aFilter))
{
$this->Logger()->WriteDump($oFilter);
$aFilters[] = $oFilter;
}
}
}
return $this->TrueResponse(__FUNCTION__);
return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->Save($aFilters));
}
/**
@ -7001,10 +7017,11 @@ class Actions
/**
* @param bool $bDownload
* @param bool $bThumbnail = false
*
* @return bool
*/
private function rawSmart($bDownload)
private function rawSmart($bDownload, $bThumbnail = false)
{
$sRawKey = (string) $this->GetActionParam('RawKey', '');
$aValues = $this->getDecodedRawKeyValue($sRawKey);
@ -7021,11 +7038,11 @@ class Actions
$this->verifyCacheByKey($sRawKey);
}
$this->initMailClientConnection();
$oAccount = $this->initMailClientConnection();
$self = $this;
return $this->MailClient()->MessageMimeStream(
function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use ($self, $sRawKey, $sContentTypeIn, $sFileNameIn, $bDownload) {
function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use ($self, $oAccount, $sRawKey, $sContentTypeIn, $sFileNameIn, $bDownload, $bThumbnail) {
if (\is_resource($rResource))
{
$sContentTypeOut = $sContentTypeIn;
@ -7046,16 +7063,41 @@ class Actions
$sFileNameOut = $self->MainClearFileName($sFileNameOut, $sContentTypeOut, $sMimeIndex);
\header('Content-Type: '.$sContentTypeOut);
\header('Content-Disposition: '.($bDownload ? 'attachment' : 'inline').'; '.
\trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileNameOut)), true);
\header('Accept-Ranges: none', true);
\header('Content-Transfer-Encoding: binary');
$self->cacheByKey($sRawKey);
\MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource);
$bDone = false;
if ($bThumbnail && !$bDownload)
{
\MailSo\Base\StreamWrappers\TempFile::Reg();
$sFileName = 'mailsotempfile://'.\md5($sFileNameOut.\rand(1000, 9999));
$rTempResource = \fopen($sFileName, 'r+b');
if (@\is_resource($rTempResource))
{
\MailSo\Base\Utils::MultipleStreamWriter($rResource, array($rTempResource));
@\fclose($rTempResource);
$oThumb =@ new \PHPThumb\GD($sFileName);
if ($oThumb)
{
$oThumb->adaptiveResize(220, 80)->show();
$bDone = true;
}
}
}
if (!$bDone)
{
\header('Content-Type: '.$sContentTypeOut);
\header('Content-Disposition: '.($bDownload ? 'attachment' : 'inline').'; '.
\trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileNameOut)), true);
\header('Accept-Ranges: none', true);
\header('Content-Transfer-Encoding: binary');
\MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource);
}
}
}, $sFolder, $iUid, true, $sMimeIndex);
}
@ -7076,6 +7118,14 @@ class Actions
return $this->rawSmart(false);
}
/**
* @return bool
*/
public function RawViewThumbnail()
{
return $this->rawSmart(false, true);
}
/**
* @return bool
*/
@ -7126,6 +7176,42 @@ class Actions
return \in_array($sExt, array('doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'));
}
/**
* @param string $sFileName
*
* @return bool
*/
public function isFileHasThumbnail($sFileName)
{
static $aCache = array();
$sExt = \MailSo\Base\Utils::GetFileExtension($sFileName);
if (isset($aCache[$sExt]))
{
return $aCache[$sExt];
}
$bResult = \function_exists('gd_info');
if ($bResult)
{
$bResult = false;
switch ($sExt)
{
case 'png':
$bResult = \function_exists('imagecreatefrompng');
break;
case 'jpg':
case 'jpeg':
$bResult = \function_exists('imagecreatefromjpeg');
break;
}
}
$aCache[$sExt] = $bResult;
return $bResult;
}
/**
* @return bool
*/
@ -8185,12 +8271,18 @@ class Actions
'CID' => $mResponse->Cid(),
'ContentLocation' => $mResponse->ContentLocation(),
'IsInline' => $mResponse->IsInline(),
'IsThumbnail' => !!$this->Config()->Get('interface', 'show_attachment_thumbnail', true),
'IsLinked' => ($mFoundedCIDs && \in_array(\trim(\trim($mResponse->Cid()), '<>'), $mFoundedCIDs)) ||
($mFoundedContentLocationUrls && \in_array(\trim($mResponse->ContentLocation()), $mFoundedContentLocationUrls))
));
$mResult['Framed'] = $this->isFileHasFramedPreview($mResult['FileName']);
if ($mResult['IsThumbnail'])
{
$mResult['IsThumbnail'] = $this->isFileHasThumbnail($mResult['FileName']);
}
$mResult['Download'] = \RainLoop\Utils::EncodeKeyValues(array(
'V' => APP_VERSION,
'Account' => $oAccount ? \md5($oAccount->Hash()) : '',
@ -8201,6 +8293,8 @@ class Actions
'FileName' => $mResult['FileName'],
'Framed' => $mResult['Framed']
));
$this->Logger()->WriteDump($mResult);
}
else if ('MailSo\Mail\Folder' === $sClassName)
{

View file

@ -80,6 +80,10 @@ class Application extends \RainLoop\Config\AbstractConfig
0 for unlimited.')
),
'interface' => array(
'show_attachment_thumbnail' => array(true, '')
),
'branding' => array(
'login_logo' => array(''),
'login_desc' => array(''),

View file

@ -4,6 +4,11 @@ namespace RainLoop\Providers\Filters\Classes;
class Filter
{
/**
* @var string
*/
private $sID;
/**
* @var string
*/
@ -41,6 +46,7 @@ class Filter
public function Clear()
{
$this->sID = '';
$this->sName = '';
$this->aConditions = array();
@ -54,6 +60,14 @@ class Filter
$this->bSkipOthers = false;
}
/**
* @return string
*/
public function ID()
{
return $this->sID;
}
/**
* @return string
*/
@ -129,6 +143,16 @@ class Filter
}
}
/**
* @param array $aFilter
*
* @return array
*/
public function FromJSON($aFilter)
{
//
}
/**
* @param bool $bAjax = false
*
@ -138,6 +162,7 @@ class Filter
{
$aConditions = $this->Conditions();
return array(
'ID' => $this->ID(),
'Name' => $this->Name(),
'Conditions' => $aConditions,
'FilterRulesType' => $this->FilterRulesType(),

View file

@ -15,17 +15,6 @@
<i class="icon-pencil icon-white"></i>
</a>
</div>
<div class="btn-group" data-bind="visible: !isDraftFolder()">
<a class="btn btn-dark-disabled-border buttonReply" data-tooltip-placement="bottom" data-bind="command: replyCommand, tooltip: 'MESSAGE/BUTTON_REPLY'">
<i class="icon-reply"></i>
</a>
<a class="btn btn-dark-disabled-border buttonReplyAll" data-tooltip-placement="bottom" data-bind="command: replyAllCommand, tooltip: 'MESSAGE/BUTTON_REPLY_ALL'">
<i class="icon-reply-all"></i>
</a>
<a class="btn btn-dark-disabled-border buttonForward" data-tooltip-placement="bottom" data-bind=" command: forwardCommand, tooltip: 'MESSAGE/BUTTON_FORWARD'">
<i class="icon-forward"></i>
</a>
</div>
<div class="btn-group" data-bind="visible: !usePreviewPane()">&nbsp;</div>
<div class="btn-group" data-bind="visible: !usePreviewPane()">
<a class="btn btn-dark-disabled-border button-archive" data-tooltip-placement="bottom" data-bind="visible: !isDraftFolder() && !isArchiveFolder() && !isArchiveDisabled(), command: archiveCommand, tooltip: 'MESSAGE/BUTTON_ARCHIVE'">
@ -42,87 +31,6 @@
</a>
</div>
<div class="btn-group">&nbsp;</div>
<div class="btn-group dropdown colored-toggle" data-bind="registrateBootstrapDropdown: true, openDropdownTrigger: moreDropdownTrigger">
<a id="more-view-dropdown-id" class="btn btn-dark-disabled-border dropdown-toggle buttonMore" href="#" tabindex="-1" data-toggle="dropdown" data-tooltip-placement="bottom" data-bind="command: messageVisibilityCommand, tooltip: 'MESSAGE/BUTTON_MORE'">
<i class="icon-list"></i>
</a>
<ul class="dropdown-menu g-ui-menu" role="menu" aria-labelledby="more-view-dropdown-id">
<li class="e-item" role="presentation" data-bind="visible: !isDraftFolder()">
<a class="e-link menuitem" href="#" tabindex="-1" data-bind="command: forwardAsAttachmentCommand">
<i class="icon-reply"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_FORWARD_AS_ATTACHMENT"></span>
</a>
</li>
<li class="e-item" role="presentation" data-bind="visible: !isDraftFolder()">
<a class="e-link menuitem" href="#" tabindex="-1" data-bind="command: editAsNewCommand">
<i class="icon-pencil"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_EDIT_AS_NEW"></span>
</a>
</li>
<li class="divider" role="presentation" data-bind="visible: !isDraftFolder()"></li>
<li class="e-item" role="presentation" data-bind="visible: usePreviewPane() && !isDraftFolder() && !isArchiveFolder() && !isArchiveDisabled()">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="command: archiveCommand">
<i class="icon-archive"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_ARCHIVE"></span>
</a>
</li>
<li class="e-item" role="presentation" data-bind="visible: usePreviewPane() && !isDraftFolder() && !isSentFolder() && !isSpamFolder() && !isSpamDisabled()">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="command: spamCommand">
<i class="icon-angry-smiley"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_SPAM"></span>
</a>
</li>
<li class="e-item" role="presentation" data-bind="visible: usePreviewPane() && !isDraftFolder() && !isSentFolder() && isSpamFolder() && !isSpamDisabled()">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="command: notSpamCommand">
<i class="icon-happy-smiley"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_NOT_SPAM"></span>
</a>
</li>
<li class="e-item" role="presentation" data-bind="visible: usePreviewPane()">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="command: deleteCommand">
<i class="icon-trash"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_DELETE"></span>
</a>
</li>
<li class="divider" role="presentation" data-bind="visible: usePreviewPane()"></li>
<li class="e-item" role="presentation">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="click: function () { if (message()) { message().printMessage(); }} ">
<i class="icon-print"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/MENU_PRINT"></span>
</a>
</li>
<li class="e-item" role="presentation">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="click: function () { if (message()) { message().viewPopupMessage(); }}">
<i class="icon-popup"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_IN_NEW_WINDOW"></span>
</a>
</li>
<li class="divider" role="presentation"></li>
<li class="e-item" role="presentation">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="link: viewViewLink()">
<i class="icon-file-code"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/MENU_VIEW_ORIGINAL"></span>
</a>
</li>
<li class="e-item" role="presentation">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="link: viewDownloadLink()">
<i class="icon-download"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/MENU_DOWNLOAD_ORIGINAL"></span>
</a>
</li>
</ul>
</div>
<div class="btn-group">&nbsp;</div>
<div class="btn-group" data-bind="visible: !usePreviewPane()">
<a class="btn btn-dark-disabled-border buttonUp" data-bind="command: goUpCommand">
<i class="icon-left-middle"></i>
@ -146,7 +54,129 @@
<div class="b-message-view-desc error" data-bind="visible: !message() && '' !== messageError()">
<span class="text" data-bind="text: messageError()"></span>
</div>
<div data-bind="visible: message">
<div class="btn-group colored-toggle pull-right messageItemDropdown">
<a class="btn btn-dark-disabled-border buttonReply" data-tooltip-placement="bottom"
data-bind="command: replyCommand, tooltip: 'MESSAGE/BUTTON_REPLY'">
<i class="icon-reply"></i>
</a>
<!--
<a class="btn btn-dark-disabled-border buttonReplyAll" data-tooltip-placement="bottom"
data-bind="command: replyAllCommand, tooltip: 'MESSAGE/BUTTON_REPLY_ALL'">
<i class="icon-reply-all"></i>
</a>
<a class="btn btn-dark-disabled-border buttonForward" data-tooltip-placement="bottom"
data-bind="command: forwardCommand, tooltip: 'MESSAGE/BUTTON_FORWARD'">
<i class="icon-reply-all"></i>
</a>
-->
<a id="more-view-dropdown-id" class="btn btn-dark-disabled-border dropdown-toggle buttonMore"
href="#" tabindex="-1" data-toggle="dropdown" data-tooltip-placement="bottom"
data-bind="command: messageVisibilityCommand, tooltip: 'MESSAGE/BUTTON_MORE'">
<span class="caret"></span>
</a>
<ul class="dropdown-menu g-ui-menu" role="menu" aria-labelledby="more-view-dropdown-id">
<!--
<li class="e-item" role="presentation" data-bind="visible: !isDraftFolder()">
<a class="e-link menuitem" href="#" tabindex="-1" data-bind="command: replyCommand">
<i class="icon-reply"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_REPLY"></span>
</a>
</li>
-->
<li class="e-item" role="presentation" data-bind="visible: !isDraftFolder()">
<a class="e-link menuitem" href="#" tabindex="-1" data-bind="command: replyAllCommand">
<i class="icon-reply-all"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_REPLY_ALL"></span>
</a>
</li>
<li class="e-item" role="presentation" data-bind="visible: !isDraftFolder()">
<a class="e-link menuitem" href="#" tabindex="-1" data-bind="command: forwardCommand">
<i class="icon-forward"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_FORWARD"></span>
</a>
</li>
<li class="e-item" role="presentation" data-bind="visible: !isDraftFolder()">
<a class="e-link menuitem" href="#" tabindex="-1" data-bind="command: editAsNewCommand">
<i class="icon-pencil"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_EDIT_AS_NEW"></span>
</a>
</li>
<li class="e-item" role="presentation" data-bind="visible: !isDraftFolder()">
<a class="e-link menuitem" href="#" tabindex="-1" data-bind="command: forwardAsAttachmentCommand">
<i class="icon-forward"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_FORWARD_AS_ATTACHMENT"></span>
</a>
</li>
<li class="divider" role="presentation" data-bind="visible: !isDraftFolder()"></li>
<li class="e-item" role="presentation" data-bind="visible: usePreviewPane() && !isDraftFolder() && !isArchiveFolder() && !isArchiveDisabled()">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="command: archiveCommand">
<i class="icon-archive"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_ARCHIVE"></span>
</a>
</li>
<li class="e-item" role="presentation" data-bind="visible: usePreviewPane() && !isDraftFolder() && !isSentFolder() && !isSpamFolder() && !isSpamDisabled()">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="command: spamCommand">
<i class="icon-angry-smiley"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_SPAM"></span>
</a>
</li>
<li class="e-item" role="presentation" data-bind="visible: usePreviewPane() && !isDraftFolder() && !isSentFolder() && isSpamFolder() && !isSpamDisabled()">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="command: notSpamCommand">
<i class="icon-happy-smiley"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_NOT_SPAM"></span>
</a>
</li>
<li class="e-item" role="presentation" data-bind="visible: usePreviewPane()">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="command: deleteCommand">
<i class="icon-trash"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_DELETE"></span>
</a>
</li>
<li class="divider" role="presentation" data-bind="visible: usePreviewPane()"></li>
<li class="e-item" role="presentation">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="click: function () { if (message()) { message().printMessage(); }} ">
<i class="icon-print"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/MENU_PRINT"></span>
</a>
</li>
<li class="e-item" role="presentation">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="click: function () { if (message()) { message().viewPopupMessage(); }}">
<i class="icon-popup"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/BUTTON_IN_NEW_WINDOW"></span>
</a>
</li>
<li class="divider" role="presentation"></li>
<li class="e-item" role="presentation">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="link: viewViewLink()">
<i class="icon-file-code"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/MENU_VIEW_ORIGINAL"></span>
</a>
</li>
<li class="e-item" role="presentation">
<a target="_blank" class="e-link menuitem" href="#" tabindex="-1" data-bind="link: viewDownloadLink()">
<i class="icon-download"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n-text="MESSAGE/MENU_DOWNLOAD_ORIGINAL"></span>
</a>
</li>
</ul>
</div>
<div class="messageItem" data-bind="css: viewLineAsCcc(), nano: true">
<div class="content g-scrollbox" tabindex="0" data-bind="hasfocus: messageDomFocused">
<div class="content-wrapper">
@ -264,30 +294,40 @@
</div>
<div class="attachmentsPlace" data-bind="visible: message() && message().hasVisibleAttachments()">
<ul class="attachmentList" data-bind="foreach: message() ? message().attachments() : []">
<li class="attachmentItem" draggable="true" data-bind="visible: !isLinked, title: fileName, event: { 'dragstart': eventDragStart }">
<div style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">
<i class="attachmentIcon" data-bind="css: iconClass()"></i>
&nbsp;&nbsp;
<span class="attachmentName" data-bind="text: fileName"></span>
<li class="attachmentItem clearfix" draggable="true" data-bind="visible: !isLinked, title: fileName, event: { 'dragstart': eventDragStart },
css: { 'hasThumbnail': hasThumbnail() }, attr: { 'style': linkThumbnailPreviewStyle() }">
<div class="attachmentIconParent pull-left" data-bind="css: { 'hasPreview': hasPreview() }">
<div class="attachmentNonePreview">
<i class="attachmentIcon attachmentMainIcon" data-bind="css: iconClass()"></i>
</div>
<a class="attachmentPreview attachmentImagePreview magnificPopupImage"
data-bind="visible: isImage(), attr: {href: linkPreview(), title: fileName, 'data-index': $index}" target="_blank">
<i class="attachmentIcon icon-eye show-hover"></i>
<i class="attachmentIcon attachmentMainIcon hide-hover" data-bind="css: iconClass()"></i>
</a>
<a class="attachmentPreview"
data-bind="visible: isText(), attr: {href: linkPreviewAsPlain(), title: fileName}" target="_blank">
<i class="attachmentIcon icon-eye show-hover"></i>
<i class="attachmentIcon attachmentMainIcon hide-hover" data-bind="css: iconClass()"></i>
</a>
<a class="attachmentPreview"
data-bind="visible: isPdf(), attr: {href: linkPreview(), title: fileName}" target="_blank">
<i class="attachmentIcon icon-eye show-hover"></i>
<i class="attachmentIcon attachmentMainIcon hide-hover" data-bind="css: iconClass()"></i>
</a>
<a class="attachmentPreview"
data-bind="visible: isFramed(), attr: {href: linkFramed(), title: fileName}" target="_blank">
<i class="attachmentIcon icon-eye show-hover"></i>
<i class="attachmentIcon attachmentMainIcon hide-hover" data-bind="css: iconClass()"></i>
</a>
</div>
<div>
<a class="attachmentPreview attachmentImagePreview magnificPopupImage pull-left"
data-bind="visible: isImage(), attr: {href: linkPreview(), title: fileName, 'data-index': $index}" target="_blank">
<i class="icon-eye"></i>
</a>
<a class="attachmentPreview pull-left"
data-bind="visible: isText(), attr: {href: linkPreviewAsPlain(), title: fileName}" target="_blank">
<i class="icon-eye"></i>
</a>
<a class="attachmentPreview pull-left"
data-bind="visible: isPdf(), attr: {href: linkPreview(), title: fileName}" target="_blank">
<i class="icon-eye"></i>
</a>
<a class="attachmentPreview pull-left"
data-bind="visible: isFramed(), attr: {href: linkFramed(), title: fileName}" target="_blank">
<i class="icon-eye"></i>
</a>
<span class="attachmentSize pull-right" data-bind="text: friendlySize"></span>
<div class="attachmentNameParent">
<div style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">
<span class="attachmentName" data-bind="text: fileName"></span>&nbsp;
</div>
<div>
<span class="attachmentSize" data-bind="text: friendlySize"></span>&nbsp;
</div>
</div>
</li>
</ul>