Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117884613
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
103 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/plugins/enigma/config.inc.php.dist b/plugins/enigma/config.inc.php.dist
index a5a5233c2..d654c764d 100644
--- a/plugins/enigma/config.inc.php.dist
+++ b/plugins/enigma/config.inc.php.dist
@@ -1,80 +1,85 @@
<?php
// Enigma Plugin options
// --------------------
// A driver to use for PGP. Default: "gnupg".
$config['enigma_pgp_driver'] = 'gnupg';
// A driver to use for S/MIME. Default: "phpssl".
$config['enigma_smime_driver'] = 'phpssl';
// Enables logging of enigma operations (including Crypt_GPG debug info)
$config['enigma_debug'] = false;
// REQUIRED! Keys directory for all users.
// Must be writeable by PHP process, and not in the web server document root
$config['enigma_pgp_homedir'] = null;
// Location of gpg binary. By default it will be auto-detected.
// This is also a way to force gpg2 use if there are both 1.x and 2.x on the system.
$config['enigma_pgp_binary'] = '';
// Location of gpg-agent binary. By default it will be auto-detected.
// It's used with GnuPG 2.x.
$config['enigma_pgp_agent'] = '';
// Location of gpgconf binary. By default it will be auto-detected.
// It's used with GnuPG >= 2.1.
$config['enigma_pgp_gpgconf'] = '';
// Name of the PGP symmetric cipher algorithm.
// Run gpg --version to see the list of supported algorithms
$config['enigma_pgp_cipher_algo'] = null;
// Name of the PGP digest (hash) algorithm.
// Run gpg --version to see the list of supported algorithms
$config['enigma_pgp_digest_algo'] = null;
// Enables multi-host environments support.
// Enable it if you have more than one HTTP server.
// Make sure all servers run the same GnuPG version and have time in sync.
// Keys will be stored in SQL database (make sure max_allowed_packet
// is big enough).
$config['enigma_multihost'] = false;
// Enables signatures verification feature.
$config['enigma_signatures'] = true;
// Enables messages decryption feature.
$config['enigma_decryption'] = true;
// Enables messages encryption and signing feature.
$config['enigma_encryption'] = true;
// Enable signing all messages by default
$config['enigma_sign_all'] = false;
// Enable encrypting all messages by default
$config['enigma_encrypt_all'] = false;
// Enable attaching a public key to all messages by default
$config['enigma_attach_pubkey'] = false;
// Default for how long to store private key passwords (in minutes).
// When set to 0 passwords will be stored for the whole session.
$config['enigma_password_time'] = 5;
// Enable support for private keys without passwords.
$config['enigma_passwordless'] = false;
// With this option you can lock composing options
// of the plugin forcing the user to use configured settings.
// The array accepts: 'sign', 'encrypt', 'pubkey'.
//
// For example, to force your users to sign every email,
// you should set:
// - enigma_sign_all = true
// - enigma_options_lock = ['sign']
// - dont_override = ['enigma_sign_all']
$config['enigma_options_lock'] = [];
+
+// Enable Kolab's Web Of Anti-Trust feature
+// Fetches public keys from DNS. Default: false
+// To enable set it to True or an array of domain names.
+$config['enigma_woat'] = false;
diff --git a/plugins/enigma/lib/enigma_engine.php b/plugins/enigma/lib/enigma_engine.php
index 576d9ecdb..adae0cc6f 100644
--- a/plugins/enigma/lib/enigma_engine.php
+++ b/plugins/enigma/lib/enigma_engine.php
@@ -1,1445 +1,1530 @@
<?php
/**
+-------------------------------------------------------------------------+
| Engine of the Enigma Plugin |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
/**
* Enigma plugin engine.
*
* RFC2440: OpenPGP Message Format
* RFC3156: MIME Security with OpenPGP
* RFC3851: S/MIME
*/
class enigma_engine
{
private $rc;
private $enigma;
private $pgp_driver;
private $smime_driver;
private $password_time;
+ private $sender;
private $cache = [];
public $decryptions = [];
public $signatures = [];
public $encrypted_parts = [];
const ENCRYPTED_PARTIALLY = 100;
const SIGN_MODE_BODY = 1;
const SIGN_MODE_SEPARATE = 2;
const SIGN_MODE_MIME = 4;
const ENCRYPT_MODE_BODY = 1;
const ENCRYPT_MODE_MIME = 2;
const ENCRYPT_MODE_SIGN = 4;
/**
* Plugin initialization.
*/
function __construct($enigma)
{
$this->rc = rcmail::get_instance();
$this->enigma = $enigma;
$this->password_time = $this->rc->config->get('enigma_password_time') * 60;
// this will remove passwords from session after some time
if ($this->password_time) {
$this->get_passwords();
}
}
/**
* PGP driver initialization.
*/
function load_pgp_driver()
{
if ($this->pgp_driver) {
return;
}
$driver = 'enigma_driver_' . $this->rc->config->get('enigma_pgp_driver', 'gnupg');
$username = $this->rc->user->get_username();
// Load driver
$this->pgp_driver = new $driver($username);
if (!$this->pgp_driver) {
rcube::raise_error([
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Enigma plugin: Unable to load PGP driver: $driver"
], true, true
);
}
// Initialise driver
$result = $this->pgp_driver->init();
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__, true);
}
}
/**
* S/MIME driver initialization.
*/
function load_smime_driver()
{
if ($this->smime_driver) {
return;
}
$driver = 'enigma_driver_' . $this->rc->config->get('enigma_smime_driver', 'phpssl');
$username = $this->rc->user->get_username();
// Load driver
$this->smime_driver = new $driver($username);
if (!$this->smime_driver) {
rcube::raise_error([
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Enigma plugin: Unable to load S/MIME driver: $driver"
], true, true
);
}
// Initialise driver
$result = $this->smime_driver->init();
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__, true);
}
}
/**
* Handler for message signing
*
* @param Mail_mime &$message Original message
* @param int $mode Encryption mode
*
* @return enigma_error On error returns error object
*/
function sign_message(&$message, $mode = null)
{
$mime = new enigma_mime_message($message, enigma_mime_message::PGP_SIGNED);
$from = $mime->getFromAddress();
// find private key
$key = $this->find_key($from, true);
if (empty($key)) {
return new enigma_error(enigma_error::KEYNOTFOUND);
}
// check if we have password for this key
$passwords = $this->get_passwords();
$pass = $passwords[$key->id] ?? null;
if ($pass === null && !$this->rc->config->get('enigma_passwordless')) {
// ask for password
$error = ['missing' => [$key->id => $key->name]];
return new enigma_error(enigma_error::BADPASS, '', $error);
}
$key->password = $pass;
// select mode
switch ($mode) {
case self::SIGN_MODE_BODY:
$pgp_mode = Crypt_GPG::SIGN_MODE_CLEAR;
break;
case self::SIGN_MODE_MIME:
$pgp_mode = Crypt_GPG::SIGN_MODE_DETACHED;
break;
default:
if ($mime->isMultipart()) {
$pgp_mode = Crypt_GPG::SIGN_MODE_DETACHED;
}
else {
$pgp_mode = Crypt_GPG::SIGN_MODE_CLEAR;
}
}
// get message body
if ($pgp_mode == Crypt_GPG::SIGN_MODE_CLEAR) {
// in this mode we'll replace text part
// with the one containing signature
$body = $message->getTXTBody();
$text_charset = $message->getParam('text_charset');
$line_length = $this->rc->config->get('line_length', 72);
// We can't use format=flowed for signed messages
if (strpos($text_charset, 'format=flowed')) {
list($charset, $params) = explode(';', $text_charset);
$body = rcube_mime::unfold_flowed($body);
$body = rcube_mime::wordwrap($body, $line_length, "\r\n", false, $charset);
$text_charset = str_replace(";\r\n format=flowed", '', $text_charset);
}
}
else {
// here we'll build PGP/MIME message
$body = $mime->getOrigBody();
}
// sign the body
$result = $this->pgp_sign($body, $key, $pgp_mode);
if ($result !== true) {
if ($result->getCode() == enigma_error::BADPASS) {
// ask for password
$error = ['bad' => [$key->id => $key->name]];
return new enigma_error(enigma_error::BADPASS, '', $error);
}
return $result;
}
// replace message body
if ($pgp_mode == Crypt_GPG::SIGN_MODE_CLEAR) {
$message->setTXTBody($body);
if (!empty($text_charset)) {
$message->setParam('text_charset', $text_charset);
}
}
else {
$mime->addPGPSignature($body, $this->pgp_driver->signature_algorithm());
$message = $mime;
}
}
/**
* Handler for message encryption
*
* @param Mail_mime &$message Original message
* @param int $mode Encryption mode
* @param bool $is_draft Is draft-save action - use only sender's key for encryption
*
* @return enigma_error On error returns error object
*/
function encrypt_message(&$message, $mode = null, $is_draft = false)
{
$mime = new enigma_mime_message($message, enigma_mime_message::PGP_ENCRYPTED);
// always use sender's key
$from = $mime->getFromAddress();
$sign_key = null;
$keys = [];
// check senders key for signing
if ($mode & self::ENCRYPT_MODE_SIGN) {
$sign_key = $this->find_key($from, true);
if (empty($sign_key)) {
return new enigma_error(enigma_error::KEYNOTFOUND);
}
// check if we have password for this key
$passwords = $this->get_passwords();
$sign_pass = $passwords[$sign_key->id] ?? null;
if ($sign_pass === null && !$this->rc->config->get('enigma_passwordless')) {
// ask for password
$error = ['missing' => [$sign_key->id => $sign_key->name]];
return new enigma_error(enigma_error::BADPASS, '', $error);
}
$sign_key->password = $sign_pass;
}
$recipients = [$from];
// if it's not a draft we add all recipients' keys
if (!$is_draft) {
$recipients = array_merge($recipients, $mime->getRecipients());
}
$recipients = array_unique($recipients);
+ // Fetch keys from external sources, if configured
+ $this->sync_keys($recipients);
+
// find recipient public keys
foreach ((array) $recipients as $email) {
if ($email == $from && $sign_key) {
$key = $sign_key;
}
else {
$key = $this->find_key($email);
}
if (empty($key)) {
return new enigma_error(enigma_error::KEYNOTFOUND, '', ['missing' => $email]);
}
$keys[] = $key;
}
// select mode
if ($mode & self::ENCRYPT_MODE_BODY) {
$encrypt_mode = $mode;
}
else if ($mode & self::ENCRYPT_MODE_MIME) {
$encrypt_mode = $mode;
}
else {
$encrypt_mode = $mime->isMultipart() ? self::ENCRYPT_MODE_MIME : self::ENCRYPT_MODE_BODY;
}
// get message body
if ($encrypt_mode == self::ENCRYPT_MODE_BODY) {
// in this mode we'll replace text part
// with the one containing encrypted message
$body = $message->getTXTBody();
}
else {
// here we'll build PGP/MIME message
$body = $mime->getOrigBody();
}
// sign the body
$result = $this->pgp_encrypt($body, $keys, $sign_key);
if ($result !== true) {
if ($result->getCode() == enigma_error::BADPASS) {
// ask for password
$error = ['bad' => [$sign_key->id => $sign_key->name]];
return new enigma_error(enigma_error::BADPASS, '', $error);
}
return $result;
}
// replace message body
if ($encrypt_mode == self::ENCRYPT_MODE_BODY) {
$message->setTXTBody($body);
}
else {
$mime->setPGPEncryptedBody($body);
$message = $mime;
}
}
/**
* Handler for attaching public key to a message
*
* @param Mail_mime &$message Original message
*
* @return bool True on success, False on failure
*/
function attach_public_key(&$message)
{
$headers = $message->headers();
$from = rcube_mime::decode_address_list($headers['From'], 1, false, null, true);
$from = $from[1] ?? null;
// find my key
if ($from && ($key = $this->find_key($from, true))) {
$pubkey_armor = $this->export_key($key->id);
if (!$pubkey_armor instanceof enigma_error) {
$pubkey_name = '0x' . enigma_key::format_id($key->id) . '.asc';
$message->addAttachment($pubkey_armor, 'application/pgp-keys', $pubkey_name, false, '7bit');
return true;
}
}
return false;
}
/**
* Handler for message_part_structure hook.
* Called for every part of the message.
*
* @param array $p Original parameters
* @param string $body Part body (will be set if used internally)
*
* @return array Modified parameters
*/
function part_structure($p, $body = null)
{
static $got_content = false;
// Prevent from "decryption oracle" [CVE-2019-10740] (#6638)
// On mail compose (edit/reply/forward) we support encrypted content only
// in the first "content part" of the message.
if ($got_content && $this->rc->task == 'mail' && $this->rc->action == 'compose') {
return;
}
+ // Get the message/part sender
+ if (!empty($p['object']->sender) && !empty($p['object']->sender['mailto'])) {
+ $this->sender = $p['object']->sender['mailto'];
+ }
+ if (!empty($p['structure']->headers) && !empty($p['structure']->headers['from'])) {
+ $from = rcube_mime::decode_address_list($p['structure']->headers['from'], 1, false);
+ if (($from = current($from)) && !empty($from['mailto'])) {
+ $this->sender = $from['mailto'];
+ }
+ }
+
// Don't be tempted to support encryption in text/html parts
// Because of EFAIL vulnerability we should never support this (#6289)
if ($p['mimetype'] == 'text/plain' || $p['mimetype'] == 'application/pgp') {
$this->parse_plain($p, $body);
$got_content = true;
}
else if ($p['mimetype'] == 'multipart/signed') {
$this->parse_signed($p, $body);
$got_content = true;
}
else if ($p['mimetype'] == 'multipart/encrypted') {
$this->parse_encrypted($p);
$got_content = true;
}
else if ($p['mimetype'] == 'application/pkcs7-mime') {
$this->parse_encrypted($p);
$got_content = true;
}
else {
$got_content = !empty($p['structure']->type) && $p['structure']->type === 'content';
}
return $p;
}
/**
* Handler for message_part_body hook.
*
* @param array $p Original parameters
*
* @return array Modified parameters
*/
function part_body($p)
{
// encrypted attachment, see parse_plain_encrypted()
if (!empty($p['part']->need_decryption) && $p['part']->body === null) {
$this->load_pgp_driver();
$storage = $this->rc->get_storage();
$body = $storage->get_message_part($p['object']->uid, $p['part']->mime_id, $p['part'], null, null, true, 0, false);
$result = $this->pgp_decrypt($body);
// @TODO: what to do on error?
if ($result === true) {
$p['part']->body = $body;
$p['part']->size = strlen($body);
$p['part']->body_modified = true;
}
}
return $p;
}
/**
* Handler for plain/text message.
*
* @param array &$p Reference to hook's parameters
* @param string $body Part body (will be set if used internally)
*/
function parse_plain(&$p, $body = null)
{
$part = $p['structure'];
// Get message body from IMAP server
if ($body === null) {
$body = $this->get_part_body($p['object'], $part);
}
// In this way we can use fgets on string as on file handle
// Don't use php://temp for security (body may come from an encrypted part)
$fd = fopen('php://memory', 'r+');
if (!$fd) {
return;
}
fwrite($fd, $body);
rewind($fd);
$body = '';
$prefix = '';
$mode = '';
$tokens = [
'BEGIN PGP SIGNED MESSAGE' => 'signed-start',
'END PGP SIGNATURE' => 'signed-end',
'BEGIN PGP MESSAGE' => 'encrypted-start',
'END PGP MESSAGE' => 'encrypted-end',
];
$regexp = '/^-----(' . implode('|', array_keys($tokens)) . ')-----[\r\n]*/';
while (($line = fgets($fd)) !== false) {
if (strlen($line) > 5 && $line[0] === '-' && $line[4] === '-' && preg_match($regexp, $line, $m)) {
switch ($tokens[$m[1]]) {
case 'signed-start':
$body = $line;
$mode = 'signed';
break;
case 'signed-end':
if ($mode === 'signed') {
$body .= $line;
}
break 2; // ignore anything after this line
case 'encrypted-start':
$body = $line;
$mode = 'encrypted';
break;
case 'encrypted-end':
if ($mode === 'encrypted') {
$body .= $line;
}
break 2; // ignore anything after this line
}
continue;
}
if ($mode === 'signed') {
$body .= $line;
}
else if ($mode === 'encrypted') {
$body .= $line;
}
else {
$prefix .= $line;
}
}
fclose($fd);
if ($mode === 'signed') {
$this->parse_plain_signed($p, $body, $prefix);
}
else if ($mode === 'encrypted') {
$this->parse_plain_encrypted($p, $body, $prefix);
}
}
/**
* Handler for multipart/signed message.
*
* @param array &$p Reference to hook's parameters
* @param string $body Part body (will be set if used internally)
*/
function parse_signed(&$p, $body = null)
{
$struct = $p['structure'];
// S/MIME
if (!empty($struct->parts[1]) && $struct->parts[1]->mimetype == 'application/pkcs7-signature') {
$this->parse_smime_signed($p, $body);
}
// PGP/MIME: RFC3156
// The multipart/signed body MUST consist of exactly two parts.
// The first part contains the signed data in MIME canonical format,
// including a set of appropriate content headers describing the data.
// The second body MUST contain the PGP digital signature. It MUST be
// labeled with a content type of "application/pgp-signature".
else if (count($struct->parts) == 2
&& $struct->parts[1] && $struct->parts[1]->mimetype == 'application/pgp-signature'
) {
$this->parse_pgp_signed($p, $body);
}
}
/**
* Handler for multipart/encrypted message.
*
* @param array &$p Reference to hook's parameters
*/
function parse_encrypted(&$p)
{
$struct = $p['structure'];
// S/MIME
if ($p['mimetype'] == 'application/pkcs7-mime') {
$this->parse_smime_encrypted($p);
}
// PGP/MIME: RFC3156
// The multipart/encrypted MUST consist of exactly two parts. The first
// MIME body part must have a content type of "application/pgp-encrypted".
// This body contains the control information.
// The second MIME body part MUST contain the actual encrypted data. It
// must be labeled with a content type of "application/octet-stream".
else if (count($struct->parts) == 2
&& $struct->parts[0] && $struct->parts[0]->mimetype == 'application/pgp-encrypted'
&& $struct->parts[1] && $struct->parts[1]->mimetype == 'application/octet-stream'
) {
$this->parse_pgp_encrypted($p);
}
}
/**
* Handler for plain signed message.
* Excludes message and signature bodies and verifies signature.
*
* @param array &$p Reference to hook's parameters
* @param string $body Message (part) body
* @param string $prefix Body prefix (additional text before the encrypted block)
*/
private function parse_plain_signed(&$p, $body, $prefix = '')
{
if (!$this->rc->config->get('enigma_signatures', true)) {
return;
}
$this->load_pgp_driver();
$part = $p['structure'];
// Verify signature
if ($this->rc->action == 'show' || $this->rc->action == 'preview' || $this->rc->action == 'print') {
$sig = $this->pgp_verify($body);
}
// In this way we can use fgets on string as on file handle
// Don't use php://temp for security (body may come from an encrypted part)
$fd = fopen('php://memory', 'r+');
if (!$fd) {
return;
}
fwrite($fd, $body);
rewind($fd);
$body = $part->body = null;
$part->body_modified = true;
// Extract body (and signature?)
while (($line = fgets($fd, 1024)) !== false) {
if ($part->body === null) {
$part->body = '';
}
else if (preg_match('/^-----BEGIN PGP SIGNATURE-----/', $line)) {
break;
}
else {
$part->body .= $line;
}
}
fclose($fd);
// Remove "Hash" Armor Headers
$part->body = preg_replace('/^.*\r*\n\r*\n/', '', $part->body);
// de-Dash-Escape (RFC2440)
$part->body = preg_replace('/(^|\n)- -/', '\\1-', $part->body);
if ($prefix) {
$part->body = $prefix . $part->body;
}
// Store signature data for display
if (!empty($sig)) {
$sig->partial = !empty($prefix);
$this->signatures[$part->mime_id] = $sig;
}
}
/**
* Handler for PGP/MIME signed message.
* Verifies signature.
*
* @param array &$p Reference to hook's parameters
* @param string $body Part body (will be set if used internally)
*/
private function parse_pgp_signed(&$p, $body = null)
{
if (!$this->rc->config->get('enigma_signatures', true)) {
return;
}
if ($this->rc->action != 'show' && $this->rc->action != 'preview' && $this->rc->action != 'print') {
return;
}
$this->load_pgp_driver();
$struct = $p['structure'];
$msg_part = $struct->parts[0];
$sig_part = $struct->parts[1];
// Get bodies
if ($body === null) {
if (empty($struct->body_modified)) {
$body = $this->get_part_body($p['object'], $struct);
}
}
$boundary = $struct->ctype_parameters['boundary'];
// when it is a signed message forwarded as attachment
// ctype_parameters property will not be set
if (!$boundary && !empty($struct->headers['content-type'])
&& preg_match('/boundary="?([a-zA-Z0-9\'()+_,-.\/:=?]+)"?/', $struct->headers['content-type'], $m)
) {
$boundary = $m[1];
}
// set signed part body
list($msg_body, $sig_body) = $this->explode_signed_body($body, $boundary);
// Verify
if ($sig_body && $msg_body) {
$sig = $this->pgp_verify($msg_body, $sig_body);
// Store signature data for display
$this->signatures[$struct->mime_id] = $sig;
$this->signatures[$msg_part->mime_id] = $sig;
}
}
/**
* Handler for S/MIME signed message.
* Verifies signature.
*
* @param array &$p Reference to hook's parameters
* @param string $body Part body (will be set if used internally)
*/
private function parse_smime_signed(&$p, $body = null)
{
if (!$this->rc->config->get('enigma_signatures', true)) {
return;
}
// @TODO
}
/**
* Handler for plain encrypted message.
*
* @param array &$p Reference to hook's parameters
* @param string $body Message (part) body
* @param string $prefix Body prefix (additional text before the encrypted block)
*/
private function parse_plain_encrypted(&$p, $body, $prefix = '')
{
if (!$this->rc->config->get('enigma_decryption', true)) {
return;
}
$this->load_pgp_driver();
$part = $p['structure'];
// Decrypt
$result = $this->pgp_decrypt($body, $signature);
// Store decryption status
$this->decryptions[$part->mime_id] = $result;
// Store signature data for display
if ($signature) {
$this->signatures[$part->mime_id] = $signature;
}
// find parent part ID
if (strpos($part->mime_id, '.')) {
$items = explode('.', $part->mime_id);
array_pop($items);
$parent = implode('.', $items);
}
else {
$parent = 0;
}
// Parse decrypted message
if ($result === true) {
$part->body = $prefix . $body;
$part->body_modified = true;
// it maybe PGP signed inside, verify signature
$this->parse_plain($p, $body);
// Remember it was decrypted
$this->encrypted_parts[] = $part->mime_id;
// Inform the user that only a part of the body was encrypted
if ($prefix) {
$this->decryptions[$part->mime_id] = self::ENCRYPTED_PARTIALLY;
}
// Encrypted plain message may contain encrypted attachments
// in such case attachments have .pgp extension and type application/octet-stream.
// This is what happens when you select "Encrypt each attachment separately
// and send the message using inline PGP" in Thunderbird's Enigmail.
if (!empty($p['object']->mime_parts[$parent])) {
foreach ((array) $p['object']->mime_parts[$parent]->parts as $p) {
if ($p->disposition == 'attachment' && $p->mimetype == 'application/octet-stream'
&& preg_match('/^(.*)\.pgp$/i', $p->filename, $m)
) {
// modify filename
$p->filename = $m[1];
// flag the part, it will be decrypted when needed
$p->need_decryption = true;
// disable caching
$p->body_modified = true;
}
}
}
}
// decryption failed, but the message may have already
// been cached with the modified parts (see above),
// let's bring the original state back
else if (!empty($p['object']->mime_parts[$parent])) {
foreach ((array) $p['object']->mime_parts[$parent]->parts as $p) {
if ($p->need_decryption && !preg_match('/^(.*)\.pgp$/i', $p->filename, $m)) {
// modify filename
$p->filename .= '.pgp';
// flag the part, it will be decrypted when needed
unset($p->need_decryption);
}
}
}
}
/**
* Handler for PGP/MIME encrypted message.
*
* @param array &$p Reference to hook's parameters
*/
private function parse_pgp_encrypted(&$p)
{
if (!$this->rc->config->get('enigma_decryption', true)) {
return;
}
$this->load_pgp_driver();
$struct = $p['structure'];
$part = $struct->parts[1];
// Get body
$body = $this->get_part_body($p['object'], $part);
// Decrypt
$result = $this->pgp_decrypt($body, $signature);
if ($result === true) {
// Parse decrypted message
$struct = $this->parse_body($body);
// Modify original message structure
$this->modify_structure($p, $struct, strlen($body));
// Parse the structure (there may be encrypted/signed parts inside
$this->part_structure([
'object' => $p['object'],
'structure' => $struct,
'mimetype' => $struct->mimetype
], $body);
// Attach the decryption message to all parts
$this->decryptions[$struct->mime_id] = $result;
foreach ((array) $struct->parts as $sp) {
$this->decryptions[$sp->mime_id] = $result;
if ($signature) {
$this->signatures[$sp->mime_id] = $signature;
}
}
}
else {
$this->decryptions[$part->mime_id] = $result;
// Make sure decryption status message will be displayed
$part->type = 'content';
$p['object']->parts[] = $part;
// don't show encrypted part on attachments list
// don't show "cannot display encrypted message" text
$p['abort'] = true;
}
}
/**
* Handler for S/MIME encrypted message.
*
* @param array &$p Reference to hook's parameters
*/
private function parse_smime_encrypted(&$p)
{
if (!$this->rc->config->get('enigma_decryption', true)) {
return;
}
// @TODO
}
/**
* PGP signature verification.
*
* @param mixed &$msg_body Message body
* @param mixed $sig_body Signature body (for MIME messages)
*
* @return mixed enigma_signature or enigma_error
*/
private function pgp_verify(&$msg_body, $sig_body = null)
{
// @TODO: Handle big bodies using (temp) files
+ // Import sender's key from external sources, if configured
+ if ($this->sender) {
+ $this->sync_keys([$this->sender]);
+ }
+
// Get rid of possible non-ascii characters (#5962)
$sig_body = preg_replace('/[^\x00-\x7F]/', '', $sig_body);
$sig = $this->pgp_driver->verify($msg_body, $sig_body);
if (($sig instanceof enigma_error) && $sig->getCode() != enigma_error::KEYNOTFOUND) {
self::raise_error($sig, __LINE__);
}
return $sig;
}
/**
* PGP message decryption.
*
* @param mixed &$msg_body Message body
* @param enigma_signature &$signature Signature verification result
*
* @return mixed True or enigma_error
*/
private function pgp_decrypt(&$msg_body, &$signature = null)
{
// @TODO: Handle big bodies using (temp) files
+ // Import sender's key from external sources, if configured
+ if ($this->sender) {
+ $this->sync_keys([$this->sender]);
+ }
+
// Get rid of possible non-ascii characters (#5962)
$msg_body = preg_replace('/[^\x00-\x7F]/', '', $msg_body);
$keys = $this->get_passwords();
$result = $this->pgp_driver->decrypt($msg_body, $keys, $signature);
if ($result instanceof enigma_error) {
if ($result->getCode() != enigma_error::KEYNOTFOUND) {
self::raise_error($result, __LINE__);
}
return $result;
}
$msg_body = $result;
return true;
}
/**
* PGP message signing
*
* @param mixed &$msg_body Message body
* @param enigma_key $key The key (with passphrase)
* @param int $mode Signing mode
*
* @return mixed True or enigma_error
*/
private function pgp_sign(&$msg_body, $key, $mode = null)
{
// @TODO: Handle big bodies using (temp) files
$result = $this->pgp_driver->sign($msg_body, $key, $mode);
if ($result instanceof enigma_error) {
if ($result->getCode() != enigma_error::KEYNOTFOUND) {
self::raise_error($result, __LINE__);
}
return $result;
}
$msg_body = $result;
return true;
}
/**
* PGP message encrypting
*
* @param mixed &$msg_body Message body
* @param array $keys Keys (array of enigma_key objects)
* @param string $sign_key Optional signing Key ID
* @param string $sign_pass Optional signing Key password
*
* @return mixed True or enigma_error
*/
private function pgp_encrypt(&$msg_body, $keys, $sign_key = null, $sign_pass = null)
{
// @TODO: Handle big bodies using (temp) files
$result = $this->pgp_driver->encrypt($msg_body, $keys, $sign_key, $sign_pass);
if ($result instanceof enigma_error) {
if ($result->getCode() != enigma_error::KEYNOTFOUND) {
self::raise_error($result, __LINE__);
}
return $result;
}
$msg_body = $result;
return true;
}
/**
* PGP keys listing.
*
* @param mixed $pattern Key ID/Name pattern
*
* @return mixed Array of keys or enigma_error
*/
function list_keys($pattern = '')
{
$this->load_pgp_driver();
$result = $this->pgp_driver->list_keys($pattern);
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
}
return $result;
}
/**
* Find PGP private/public key
*
* @param string $email E-mail address
* @param bool $can_sign Need a key for signing?
*
* @return enigma_key The key
*/
function find_key($email, $can_sign = false)
{
if ($can_sign && array_key_exists($email, $this->cache)) {
return $this->cache[$email];
}
$this->load_pgp_driver();
$result = $this->pgp_driver->list_keys($email);
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
return;
}
- $mode = $can_sign ? enigma_key::CAN_SIGN : enigma_key::CAN_ENCRYPT;
- $ret = null;
+ $mode = $can_sign ? enigma_key::CAN_SIGN : enigma_key::CAN_ENCRYPT;
+ $found = [];
// check key validity and type
foreach ($result as $key) {
if (($subkey = $key->find_subkey($email, $mode))
&& (!$can_sign || $key->get_type() == enigma_key::TYPE_KEYPAIR)
) {
- $ret = $key;
- break;
+ $found[$subkey->get_creation_date(true)] = $key;
}
}
+ // Use the most recent one
+ if (count($found) > 1) {
+ ksort($found, SORT_NUMERIC);
+ }
+
+ $ret = count($found) > 0 ? array_pop($found) : null;
+
// cache private key info for better performance
// we can skip one list_keys() call when signing and attaching a key
if ($can_sign) {
$this->cache[$email] = $ret;
}
return $ret;
}
/**
* PGP key details.
*
* @param mixed $keyid Key ID
*
* @return mixed enigma_key or enigma_error
*/
function get_key($keyid)
{
$this->load_pgp_driver();
$result = $this->pgp_driver->get_key($keyid);
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
}
return $result;
}
/**
* PGP key delete.
*
* @param string $keyid Key ID
*
* @return enigma_error|bool True on success
*/
function delete_key($keyid)
{
$this->load_pgp_driver();
$result = $this->pgp_driver->delete_key($keyid);
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
}
return $result;
}
/**
* PGP keys pair generation.
*
* @param array $data Key pair parameters
*
* @return mixed enigma_key or enigma_error
*/
function generate_key($data)
{
$this->load_pgp_driver();
$result = $this->pgp_driver->gen_key($data);
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
}
return $result;
}
/**
* PGP keys/certs import.
*
* @param mixed $content Import file name or content
* @param bool $isfile True if first argument is a filename
*
* @return mixed Import status data array or enigma_error
*/
function import_key($content, $isfile = false)
{
$this->load_pgp_driver();
$result = $this->pgp_driver->import($content, $isfile, $this->get_passwords());
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
}
else {
$result['imported'] = $result['public_imported'] + $result['private_imported'];
$result['unchanged'] = $result['public_unchanged'] + $result['private_unchanged'];
}
return $result;
}
/**
* PGP keys/certs export.
*
* @param string $key Key ID
* @param resource $fp Optional output stream
* @param bool $include_private Include private key
*
* @return mixed Key content or enigma_error
*/
function export_key($key, $fp = null, $include_private = false)
{
$this->load_pgp_driver();
$result = $this->pgp_driver->export($key, $include_private, $this->get_passwords());
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
return $result;
}
if ($fp) {
fwrite($fp, $result);
}
else {
return $result;
}
}
/**
* Registers password for specified key/cert sent by the password prompt.
*/
function password_handler()
{
$keyid = rcube_utils::get_input_string('_keyid', rcube_utils::INPUT_POST);
$passwd = rcube_utils::get_input_string('_passwd', rcube_utils::INPUT_POST, true);
if ($keyid && strlen($passwd)) {
$this->save_password(strtoupper($keyid), $passwd);
}
}
/**
* Saves key/cert password in user session
*/
function save_password($keyid, $password)
{
// we store passwords in session for specified time
if (!empty($_SESSION['enigma_pass'])) {
$config = $this->rc->decrypt($_SESSION['enigma_pass']);
$config = unserialize($config);
} else {
$config = [];
}
$config[$keyid] = [$password, time()];
$_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
}
/**
* Returns currently stored passwords
*/
function get_passwords()
{
if (!empty($_SESSION['enigma_pass'])) {
$config = $this->rc->decrypt($_SESSION['enigma_pass']);
$config = @unserialize($config);
}
$threshold = $this->password_time ? time() - $this->password_time : 0;
$keys = [];
// delete expired passwords
if (!empty($config)) {
foreach ($config as $key => $value) {
if ($threshold && $value[1] < $threshold) {
unset($config[$key]);
$modified = true;
}
else {
$keys[$key] = $value[0];
}
}
if (!empty($modified)) {
$_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
}
}
return $keys;
}
/**
* Get message part body.
*
* @param rcube_message $msg Message object
* @param rcube_message_part $part Message part
*/
private function get_part_body($msg, $part)
{
// @TODO: Handle big bodies using file handles
// This is a special case when we want to get the whole body
// using direct IMAP access, in other cases we prefer
// rcube_message::get_part_body() as the body may be already in memory
if (!$part->mime_id) {
// fake the size which may be empty for multipart/* parts
// otherwise get_message_part() below will fail
if (!$part->size) {
$reset = true;
$part->size = 1;
}
$storage = $this->rc->get_storage();
$body = $storage->get_message_part($msg->uid, $part->mime_id, $part,
null, null, true, 0, false);
if (!empty($reset)) {
$part->size = 0;
}
}
else {
$body = $msg->get_part_body($part->mime_id, false);
}
return $body;
}
/**
* Parse decrypted message body into structure
*
* @param string &$body Message body
*
* @return array Message structure
*/
private function parse_body(&$body)
{
// Mail_mimeDecode need \r\n end-line, but gpg may return \n
$body = preg_replace('/\r?\n/', "\r\n", $body);
// parse the body into structure
return rcube_mime::parse_message($body);
}
/**
* Replace message encrypted structure with decrypted message structure
*
* @param array &$p Hook arguments
* @param rcube_message_part $struct Part structure
* @param int $size Part size
*/
private function modify_structure(&$p, $struct, $size = 0)
{
// modify mime_parts property of the message object
$old_id = $p['structure']->mime_id;
foreach (array_keys($p['object']->mime_parts) as $idx) {
if (!$old_id || $idx == $old_id || strpos($idx, $old_id . '.') === 0) {
unset($p['object']->mime_parts[$idx]);
}
}
// set some part params used by Roundcube core
$struct->headers = array_merge($p['structure']->headers, $struct->headers);
$struct->size = $size;
$struct->filename = $p['structure']->filename;
// modify the new structure to be correctly handled by Roundcube
$this->modify_structure_part($struct, $p['object'], $old_id);
// replace old structure with the new one
$p['structure'] = $struct;
$p['mimetype'] = $struct->mimetype;
}
/**
* Modify decrypted message part
*
* @param rcube_message_part $part
* @param rcube_message $msg
* @param string $old_id
*/
private function modify_structure_part($part, $msg, $old_id)
{
// never cache the body
$part->body_modified = true;
$part->encoding = 'stream';
// modify part identifier
if ($old_id) {
$part->mime_id = !$part->mime_id ? $old_id : ($old_id . '.' . $part->mime_id);
}
// Cache the fact it was decrypted
$this->encrypted_parts[] = $part->mime_id;
$msg->mime_parts[$part->mime_id] = $part;
// modify sub-parts
foreach ((array) $part->parts as $p) {
$this->modify_structure_part($p, $msg, $old_id);
}
}
/**
* Extracts body and signature of multipart/signed message body
*/
private function explode_signed_body($body, $boundary)
{
if (!$body) {
return [];
}
$boundary = '--' . $boundary;
$boundary_len = strlen($boundary) + 2;
// Find boundaries
$start = strpos($body, $boundary) + $boundary_len;
$end = strpos($body, $boundary, $start);
// Get signed body and signature
$sig = substr($body, $end + $boundary_len);
$body = substr($body, $start, $end - $start - 2);
// Cleanup signature
$sig = substr($sig, strpos($sig, "\r\n\r\n") + 4);
$sig = substr($sig, 0, strpos($sig, $boundary));
return [$body, $sig];
}
/**
* Checks if specified message part is a PGP-key or S/MIME cert data
*
* @param rcube_message_part $part Part object
*
* @return bool True if part is a key/cert
*/
public function is_keys_part($part)
{
// @TODO: S/MIME
return (
// Content-Type: application/pgp-keys
$part->mimetype == 'application/pgp-keys'
);
}
/**
* Removes all user keys and assigned data
*
* @param string $username Username
*
* @return bool True on success, False on failure
*/
public function delete_user_data($username)
{
$homedir = $this->rc->config->get('enigma_pgp_homedir', INSTALL_PATH . 'plugins/enigma/home');
$homedir .= DIRECTORY_SEPARATOR . $username;
return file_exists($homedir) ? self::delete_dir($homedir) : true;
}
/**
* Recursive method to remove directory with its content
*
* @param string $dir Directory
*/
public static function delete_dir($dir)
{
// This code can be executed from command line, make sure
// we have permissions to delete keys directory
if (!is_writable($dir)) {
rcube::raise_error("Unable to delete $dir", false, true);
return false;
}
if ($content = scandir($dir)) {
foreach ($content as $filename) {
if ($filename != '.' && $filename != '..') {
$filename = $dir . DIRECTORY_SEPARATOR . $filename;
if (is_dir($filename)) {
self::delete_dir($filename);
}
else {
unlink($filename);
}
}
}
rmdir($dir);
}
return true;
}
/**
* Check if specified driver feature is supported
*/
public function is_supported($feature)
{
$this->load_pgp_driver();
return in_array($feature, $this->pgp_driver->capabilities());
}
/**
* Raise/log (relevant) errors
*/
protected static function raise_error($result, $line, $abort = false)
{
if ($result->getCode() != enigma_error::BADPASS) {
rcube::raise_error([
'code' => 600,
'file' => __FILE__,
'line' => $line,
'message' => "Enigma plugin: " . $result->getMessage()
], true, $abort
);
}
}
+
+ /**
+ * Import public keys from DNS according to Kolab Web-Of-Anti-Trust
+ *
+ * @param array $recipients List of email addresses
+ */
+ protected function sync_keys($recipients)
+ {
+ $import = [];
+ $woat = $this->rc->config->get('enigma_woat');
+
+ if (empty($woat)) {
+ return;
+ }
+
+ foreach ($recipients as $recipient) {
+ if (!strpos($recipient, '@')) {
+ continue;
+ }
+
+ list($local, $domain) = explode('@', $recipient);
+
+ // Do this for configured domains only
+ if (is_array($woat) && !in_array_nocase($domain, $woat)) {
+ continue;
+ }
+
+ // remove parts behind a recipient delimiter ("jeroen+Trash" => "jeroen")
+ $local = preg_replace('/\+.*$/', '', $local);
+
+ $fqdn = sha1($local) . '._woat.' . $domain;
+
+ // Fetch the TXT record(s)
+ if (($records = dns_get_record($fqdn, DNS_TXT)) === false) {
+ continue;
+ }
+
+ foreach ($records as $record) {
+ if (strpos($record['txt'], 'v=woat1,') === 0) {
+ $entry = explode('public_key=', $record['txt']);
+ if (count($entry) == 2) {
+ $import[] = $entry[1];
+ // For now we support only one key
+ break;
+ }
+ }
+ }
+ }
+
+ // Import the fetched keys
+ if (!empty($import)) {
+ $this->import_key(implode("\n", $import));
+ }
+ }
}
diff --git a/plugins/enigma/lib/enigma_subkey.php b/plugins/enigma/lib/enigma_subkey.php
index 2a09ff5a1..b1ba78ecd 100644
--- a/plugins/enigma/lib/enigma_subkey.php
+++ b/plugins/enigma/lib/enigma_subkey.php
@@ -1,124 +1,130 @@
<?php
/**
+-------------------------------------------------------------------------+
| SubKey class for the Enigma Plugin |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
class enigma_subkey
{
public $id;
public $fingerprint;
public $expires;
public $created;
public $revoked;
public $has_private;
public $algorithm;
public $length;
public $usage;
/**
* Converts internal ID to short ID
* Crypt_GPG uses internal, but e.g. Thunderbird's Enigmail displays short ID
*
* @return string Key ID
*/
function get_short_id()
{
// E.g. 04622F2089E037A5 => 89E037A5
return enigma_key::format_id($this->id);
}
/**
* Getter for formatted fingerprint
*
* @return string Formatted fingerprint
*/
function get_fingerprint()
{
return enigma_key::format_fingerprint($this->fingerprint);
}
/**
* Returns human-readable name of the key's algorithm
*
* @return string Algorithm name
*/
function get_algorithm()
{
// https://datatracker.ietf.org/doc/html/rfc4880#section-9.1
switch ($this->algorithm) {
case 1:
case 2:
case 3:
return 'RSA';
case 16:
case 20:
return 'Elgamal';
case 17:
return 'DSA';
case 18:
return 'Elliptic Curve';
case 19:
return 'ECDSA';
case 21:
return 'Diffie-Hellman';
case 22:
return 'EdDSA';
}
}
/**
* Checks if the subkey has expired
*
* @return bool
*/
function is_expired()
{
$now = new DateTime('now');
return !empty($this->expires) && $this->expires < $now;
}
/**
* Returns subkey creation date-time string
*
- * @return string|null
+ * @param bool $asInt Return the date as an integer
+ *
+ * @return string|null|int
*/
- function get_creation_date()
+ function get_creation_date($asInt = false)
{
if (empty($this->created)) {
- return null;
+ return $asInt ? 0 : null;
+ }
+
+ if ($asInt) {
+ return (int) $this->created->format('U');
}
$date_format = rcube::get_instance()->config->get('date_format', 'Y-m-d');
return $this->created->format($date_format);
}
/**
* Returns subkey expiration date-time string
*
* @return string|null
*/
function get_expiration_date()
{
if (empty($this->expires)) {
return null;
}
$date_format = rcube::get_instance()->config->get('date_format', 'Y-m-d');
return $this->expires->format($date_format);
}
}
diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php
index dd0efdcf9..94c3dae1d 100644
--- a/program/lib/Roundcube/rcube_message.php
+++ b/program/lib/Roundcube/rcube_message.php
@@ -1,1258 +1,1258 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Logical representation of a mail message with all its data |
| and related functions |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Logical representation of a mail message with all its data
* and related functions
*
* @package Framework
* @subpackage Storage
*/
class rcube_message
{
/**
* Instance of framework class.
*
* @var rcube
*/
protected $app;
/**
* Instance of storage class
*
* @var rcube_storage
*/
protected $storage;
/**
* Instance of mime class
*
* @var rcube_mime
*/
protected $mime;
protected $opt = [];
protected $parse_alternative = false;
protected $got_html_part = false;
protected $tnef_decode = false;
public $uid;
public $folder;
public $headers;
public $sender;
public $context;
public $body;
public $parts = [];
public $mime_parts = [];
public $inline_parts = [];
public $attachments = [];
public $subject = '';
public $is_safe = false;
public $pgp_mime = false;
public $encrypted_part;
const BODY_MAX_SIZE = 1048576; // 1MB
/**
* __construct
*
* Provide a uid, and parse message structure.
*
* @param string $uid The message UID.
* @param string $folder Folder name
* @param bool $is_safe Security flag
*/
function __construct($uid, $folder = null, $is_safe = false)
{
// decode combined UID-folder identifier
if (preg_match('/^[0-9.]+-.+/', $uid)) {
list($uid, $folder) = explode('-', $uid, 2);
}
$context = null;
if (preg_match('/^([0-9]+)\.([0-9.]+)$/', $uid, $matches)) {
$uid = $matches[1];
$context = $matches[2];
}
$this->uid = $uid;
$this->context = $context;
$this->app = rcube::get_instance();
$this->storage = $this->app->get_storage();
$this->folder = is_string($folder) && strlen($folder) ? $folder : $this->storage->get_folder();
// Set current folder
$this->storage->set_folder($this->folder);
$this->storage->set_options(['all_headers' => true]);
$this->headers = $this->storage->get_message($uid);
if (!$this->headers) {
return;
}
$this->tnef_decode = (bool) $this->app->config->get('tnef_decode', true);
$this->set_safe($is_safe || !empty($_SESSION['safe_messages'][$this->folder.':'.$uid]));
$this->opt = [
'safe' => $this->is_safe,
'prefer_html' => $this->app->config->get('prefer_html'),
'get_url' => $this->app->url([
'action' => 'get',
'mbox' => $this->folder,
'uid' => $uid
],
false, false, true
)
];
+ $this->mime = new rcube_mime($this->headers->charset);
+ $this->subject = str_replace("\n", '', (string) $this->headers->get('subject'));
+ $from = $this->mime->decode_address_list($this->headers->from, 1);
+ $this->sender = current($from);
+
if (!empty($this->headers->structure)) {
$this->get_mime_numbers($this->headers->structure);
$this->parse_structure($this->headers->structure);
}
else if ($this->context === null) {
$this->body = $this->storage->get_body($uid);
}
- $this->mime = new rcube_mime($this->headers->charset);
- $this->subject = str_replace("\n", '', (string) $this->headers->get('subject'));
- $from = $this->mime->decode_address_list($this->headers->from, 1);
- $this->sender = current($from);
-
// notify plugins and let them analyze this structured message object
$this->app->plugins->exec_hook('message_load', ['object' => $this]);
}
/**
* Return a (decoded) message header
*
* @param string $name Header name
* @param bool $raw Don't mime-decode the value
*
* @return string Header value
*/
public function get_header($name, $raw = false)
{
if (empty($this->headers)) {
return null;
}
return $this->headers->get($name, !$raw);
}
/**
* Set is_safe var and session data
*
* @param bool $safe enable/disable
*/
public function set_safe($safe = true)
{
$_SESSION['safe_messages'][$this->folder.':'.$this->uid] = $this->is_safe = $safe;
}
/**
* Compose a valid URL for getting a message part
*
* @param string $mime_id Part MIME-ID
* @param mixed $embed Mimetype class for parts to be embedded
*
* @return string|false URL or false if part does not exist
*/
public function get_part_url($mime_id, $embed = false)
{
if (!empty($this->mime_parts[$mime_id])) {
return $this->opt['get_url'] . '&_part=' . $mime_id
. ($embed ? '&_embed=1&_mimeclass=' . $embed : '');
}
return false;
}
/**
* Get content of a specific part of this message
*
* @param string $mime_id Part MIME-ID
* @param resource $fp File pointer to save the message part
* @param bool $skip_charset_conv Disables charset conversion
* @param int $max_bytes Only read this number of bytes
* @param bool $formatted Enables formatting of text/* parts bodies
*
* @return string Part content
* @deprecated
*/
public function get_part_content($mime_id, $fp = null, $skip_charset_conv = false, $max_bytes = 0, $formatted = true)
{
if ($part = $this->mime_parts[$mime_id]) {
// stored in message structure (winmail/inline-uuencode)
if (!empty($part->body) || $part->encoding == 'stream') {
if ($fp) {
fwrite($fp, $part->body);
}
return $fp ? true : $part->body;
}
// get from IMAP
$this->storage->set_folder($this->folder);
return $this->storage->get_message_part($this->uid, $mime_id, $part,
null, $fp, $skip_charset_conv, $max_bytes, $formatted);
}
}
/**
* Get content of a specific part of this message
*
* @param string $mime_id Part ID
* @param bool $formatted Enables formatting of text/* parts bodies
* @param int $max_bytes Only return/read this number of bytes
* @param mixed $mode NULL to return a string, -1 to print body
* or file pointer to save the body into
*
* @return string|bool Part content or operation status
*/
public function get_part_body($mime_id, $formatted = false, $max_bytes = 0, $mode = null)
{
if (empty($this->mime_parts[$mime_id])) {
return;
}
$part = $this->mime_parts[$mime_id];
// allow plugins to modify part body
$plugin = $this->app->plugins->exec_hook('message_part_body',
['object' => $this, 'part' => $part]);
// only text parts can be formatted
$formatted = $formatted && $part->ctype_primary == 'text';
// part body not fetched yet... save in memory if it's small enough
if ($part->body === null && is_numeric($mime_id) && $part->size < self::BODY_MAX_SIZE) {
$this->storage->set_folder($this->folder);
// Warning: body here should be always unformatted
$part->body = $this->storage->get_message_part($this->uid, $mime_id, $part,
null, null, true, 0, false);
}
$charset = !empty($this->headers) ? $this->headers->charset : null;
// body stored in message structure (winmail/inline-uuencode)
if ($part->body !== null || $part->encoding == 'stream') {
$body = $part->body;
if ($formatted && $body) {
$body = self::format_part_body($body, $part, $charset);
}
if ($max_bytes && strlen($body) > $max_bytes) {
$body = substr($body, 0, $max_bytes);
}
if (is_resource($mode)) {
if ($body !== false) {
fwrite($mode, $body);
@rewind($mode);
}
return $body !== false;
}
if ($mode === -1) {
if ($body !== false) {
print($body);
}
return $body !== false;
}
return $body;
}
// get the body from IMAP
$this->storage->set_folder($this->folder);
$body = $this->storage->get_message_part($this->uid, $mime_id, $part,
$mode === -1, is_resource($mode) ? $mode : null,
!($mode && $formatted), $max_bytes, $mode && $formatted);
if (is_resource($mode)) {
@rewind($mode);
return $body !== false;
}
if (!$mode && $body && $formatted) {
$body = self::format_part_body($body, $part, $charset);
}
return $body;
}
/**
* Format text message part for display
*
* @param string $body Part body
* @param rcube_message_part $part Part object
* @param string $default_charset Fallback charset if part charset is not specified
*
* @return string Formatted body
*/
public static function format_part_body($body, $part, $default_charset = null)
{
// remove useless characters
$body = preg_replace('/[\t\r\0\x0B]+\n/', "\n", $body);
// remove NULL characters if any (#1486189)
if (strpos($body, "\x00") !== false) {
$body = str_replace("\x00", '', $body);
}
// detect charset...
if (empty($part->charset) || strtoupper($part->charset) == 'US-ASCII') {
// try to extract charset information from HTML meta tag (#1488125)
if ($part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
$part->charset = strtoupper($m[1]);
}
else if ($default_charset) {
$part->charset = $default_charset;
}
else {
$rcube = rcube::get_instance();
$part->charset = $rcube->config->get('default_charset', RCUBE_CHARSET);
}
}
// ..convert charset encoding
$body = rcube_charset::convert($body, $part->charset);
return $body;
}
/**
* Determine if the message contains a HTML part. This must to be
* a real part not an attachment (or its part)
*
* @param bool $enriched Enables checking for text/enriched parts too
* @param rcube_message_part &$part Reference to the part if found
*
* @return bool True if a HTML is available, False if not
*/
public function has_html_part($enriched = false, &$part = null)
{
// check all message parts
foreach ($this->mime_parts as $part) {
if ($part->mimetype == 'text/html' || ($enriched && $part->mimetype == 'text/enriched')) {
// Skip if part is an attachment, don't use is_attachment() here
if ($part->filename) {
continue;
}
if (!$part->size) {
continue;
}
if (!$this->check_context($part)) {
continue;
}
// The HTML body part extracted from a winmail.dat attachment part
if (strpos($part->mime_id, 'winmail.') === 0) {
return true;
}
$level = explode('.', $part->mime_id);
$depth = count($level);
$last = '';
// Check if the part does not belong to a message/rfc822 part
while (array_pop($level) !== null) {
if (!count($level)) {
return true;
}
$parent = $this->mime_parts[implode('.', $level)];
if (!$this->check_context($parent)) {
return true;
}
if ($parent->mimetype == 'message/rfc822') {
continue 2;
}
}
return true;
}
}
$part = null;
return false;
}
/**
* Determine if the message contains a text/plain part. This must to be
* a real part not an attachment (or its part)
*
* @param rcube_message_part &$part Reference to the part if found
*
* @return bool True if a plain text part is available, False if not
*/
public function has_text_part(&$part = null)
{
// check all message parts
foreach ($this->mime_parts as $part) {
if ($part->mimetype == 'text/plain') {
// Skip if part is an attachment, don't use is_attachment() here
if (!empty($part->filename)) {
continue;
}
if (empty($part->size)) {
continue;
}
if (!$this->check_context($part)) {
continue;
}
$level = explode('.', $part->mime_id);
// Check if the part does not belong to a message/rfc822 part
while (array_pop($level) !== null) {
if (!count($level)) {
return true;
}
$parent = $this->mime_parts[implode('.', $level)];
if (!$this->check_context($parent)) {
return true;
}
if ($parent->mimetype == 'message/rfc822') {
continue 2;
}
}
return true;
}
}
$part = null;
return false;
}
/**
* Return the first HTML part of this message
*
* @param rcube_message_part &$part Reference to the part if found
* @param bool $enriched Enables checking for text/enriched parts too
*
* @return string|null HTML message part content
*/
public function first_html_part(&$part = null, $enriched = false)
{
if ($this->has_html_part($enriched, $part)) {
$body = $this->get_part_body($part->mime_id, true);
if ($part->mimetype == 'text/enriched') {
$body = rcube_enriched::to_html($body);
}
return $body;
}
}
/**
* Return the first text part of this message.
* If there's no text/plain part but $strict=true and text/html part
* exists, it will be returned in text/plain format.
*
* @param rcube_message_part &$part Reference to the part if found
* @param bool $strict Check only text/plain parts
*
* @return string|null Plain text message/part content
*/
public function first_text_part(&$part = null, $strict = false)
{
// no message structure, return complete body
if (empty($this->parts)) {
return $this->body;
}
if ($this->has_text_part($part)) {
return $this->get_part_body($part->mime_id, true);
}
if (!$strict && ($body = $this->first_html_part($part, true))) {
// create instance of html2text class
$h2t = new rcube_html2text($body);
return $h2t->get_text();
}
}
/**
* Return message parts in current context
*/
public function mime_parts()
{
if ($this->context === null) {
return $this->mime_parts;
}
$parts = [];
foreach ($this->mime_parts as $part_id => $part) {
if ($this->check_context($part)) {
$parts[$part_id] = $part;
}
}
return $parts;
}
/**
* Checks if part of the message is an attachment (or part of it)
*
* @param rcube_message_part $part Message part
*
* @return bool True if the part is an attachment part
*/
public function is_attachment($part)
{
foreach ($this->attachments as $att_part) {
if ($att_part->mime_id === $part->mime_id) {
return true;
}
// check if the part is a subpart of another attachment part (message/rfc822)
if ($att_part->mimetype == 'message/rfc822') {
if (in_array($part, (array)$att_part->parts)) {
return true;
}
}
}
return false;
}
/**
* In a multipart/encrypted encrypted message,
* find the encrypted message payload part.
*
* @return rcube_message_part
*/
public function get_multipart_encrypted_part()
{
foreach ($this->mime_parts as $mime_id => $mpart) {
if ($mpart->mimetype == 'multipart/encrypted') {
$this->pgp_mime = true;
}
if ($this->pgp_mime && ($mpart->mimetype == 'application/octet-stream' ||
(!empty($mpart->filename) && $mpart->filename != 'version.txt'))
) {
$this->encrypted_part = $mime_id;
return $mpart;
}
}
return false;
}
/**
* Read the message structure returned by the IMAP server
* and build flat lists of content parts and attachments
*
* @param rcube_message_part $structure Message structure node
* @param bool $recursive True when called recursively
*/
private function parse_structure($structure, $recursive = false)
{
// real content-type of message/rfc822 part
if ($structure->mimetype == 'message/rfc822' && !empty($structure->real_mimetype)) {
$mimetype = $structure->real_mimetype;
// parse headers from message/rfc822 part
if (!isset($structure->headers['subject']) && !isset($structure->headers['from'])) {
$part_body = $this->get_part_body($structure->mime_id, false, 32768);
list($headers, ) = rcube_utils::explode("\r\n\r\n", $part_body, 2);
$structure->headers = rcube_mime::parse_headers($headers);
if ($this->context === $structure->mime_id) {
$this->headers = rcube_message_header::from_array($structure->headers);
}
// For small text messages we can optimize, so an additional FETCH is not needed
if ($structure->size < 32768) {
$decoder = new rcube_mime_decode();
$decoded = $decoder->decode($part_body);
// Non-multipart message
if (isset($decoded->body) && count($structure->parts) == 1) {
$structure->parts[0]->body = $decoded->body;
}
// Multipart message
else {
foreach ($decoded->parts as $idx => $p) {
if (array_key_exists($idx, $structure->parts)) {
$structure->parts[$idx]->body = $p->body;
}
}
}
}
}
}
else {
$mimetype = $structure->mimetype;
}
// show message headers
if (
$recursive
&& is_array($structure->headers)
&& (
isset($structure->headers['subject'])
|| !empty($structure->headers['from'])
|| !empty($structure->headers['to'])
)
) {
$c = new rcube_message_part();
$c->type = 'headers';
$c->headers = $structure->headers;
$this->add_part($c);
}
// Allow plugins to handle message parts
$plugin = $this->app->plugins->exec_hook('message_part_structure', [
'object' => $this,
'structure' => $structure,
'mimetype' => $mimetype,
'recursive' => $recursive
]);
if ($plugin['abort']) {
return;
}
$structure = $plugin['structure'];
$mimetype = $plugin['mimetype'];
$recursive = $plugin['recursive'];
list($message_ctype_primary, $message_ctype_secondary) = explode('/', $mimetype);
// print body if message doesn't have multiple parts
if ($message_ctype_primary == 'text' && !$recursive) {
// parts with unsupported type add to attachments list
if (!in_array($message_ctype_secondary, ['plain', 'html', 'enriched'])) {
$this->add_part($structure, 'attachment');
return;
}
$structure->type = 'content';
$this->add_part($structure);
// Parse simple (plain text) message body
if ($message_ctype_secondary == 'plain') {
foreach ((array)$this->uu_decode($structure) as $uupart) {
$this->mime_parts[$uupart->mime_id] = $uupart;
$this->add_part($uupart, 'attachment');
}
}
}
// the same for pgp signed messages
else if ($mimetype == 'application/pgp' && !$recursive) {
$structure->type = 'content';
$this->add_part($structure);
}
// message contains (more than one!) alternative parts
else if ($mimetype == 'multipart/alternative'
&& is_array($structure->parts) && count($structure->parts) > 1
) {
// get html/plaintext parts, other add to attachments list
foreach ($structure->parts as $p => $sub_part) {
$sub_mimetype = $sub_part->mimetype;
$is_multipart = preg_match('/^multipart\/(related|relative|mixed|alternative)/', $sub_mimetype);
// skip empty text parts
if (!$sub_part->size && !$is_multipart) {
continue;
}
// We've encountered (malformed) messages with more than
// one text/plain or text/html part here. There's no way to choose
// which one is better, so we'll display first of them and add
// others as attachments (#1489358)
// check if sub part is
if ($is_multipart) {
$related_part = $p;
}
else if ($sub_mimetype == 'text/plain' && !isset($plain_part)) {
$plain_part = $p;
}
else if ($sub_mimetype == 'text/html' && !isset($html_part)) {
$html_part = $p;
$this->got_html_part = true;
}
else if ($sub_mimetype == 'text/enriched' && !isset($enriched_part)) {
$enriched_part = $p;
}
else {
// add unsupported/unrecognized parts to attachments list
$this->add_part($sub_part, 'attachment');
}
}
// parse related part (alternative part could be in here)
if (isset($related_part) && !$this->parse_alternative) {
$this->parse_alternative = true;
$this->parse_structure($structure->parts[$related_part], true);
$this->parse_alternative = false;
// if plain part was found, we should unset it if html is preferred
if (!empty($this->opt['prefer_html']) && count($this->parts)) {
$plain_part = null;
}
}
// choose html/plain part to print
$print_part = null;
if (isset($html_part) && !empty($this->opt['prefer_html'])) {
$print_part = $structure->parts[$html_part];
}
else if (isset($enriched_part)) {
$print_part = $structure->parts[$enriched_part];
}
else if (isset($plain_part)) {
$print_part = $structure->parts[$plain_part];
}
// add the right message body
if (is_object($print_part)) {
$print_part->type = 'content';
// Allow plugins to handle also this part
$plugin = $this->app->plugins->exec_hook('message_part_structure', [
'object' => $this,
'structure' => $print_part,
'mimetype' => $print_part->mimetype,
'recursive' => true
]);
if (!$plugin['abort']) {
$this->add_part($print_part);
}
}
// show plaintext warning
else if (isset($html_part) && empty($this->parts)) {
$c = new rcube_message_part();
$c->type = 'content';
$c->ctype_primary = 'text';
$c->ctype_secondary = 'plain';
$c->mimetype = 'text/plain';
$c->realtype = 'text/html';
$this->add_part($c);
}
}
// this is an encrypted message -> create a plaintext body with the according message
else if ($mimetype == 'multipart/encrypted') {
$p = new rcube_message_part();
$p->type = 'content';
$p->ctype_primary = 'text';
$p->ctype_secondary = 'plain';
$p->mimetype = 'text/plain';
$p->realtype = 'multipart/encrypted';
$p->mime_id = $structure->mime_id;
$this->add_part($p);
// add encrypted payload part as attachment
if (!empty($structure->parts)) {
for ($i=0; $i < count($structure->parts); $i++) {
$subpart = $structure->parts[$i];
if ($subpart->mimetype == 'application/octet-stream' || !empty($subpart->filename)) {
$this->add_part($subpart, 'attachment');
}
}
}
}
// this is an S/MIME encrypted message -> create a plaintext body with the according message
else if ($mimetype == 'application/pkcs7-mime') {
$p = new rcube_message_part();
$p->type = 'content';
$p->ctype_primary = 'text';
$p->ctype_secondary = 'plain';
$p->mimetype = 'text/plain';
$p->realtype = 'application/pkcs7-mime';
$p->mime_id = $structure->mime_id;
$this->add_part($p);
if (!empty($structure->filename)) {
$this->add_part($structure, 'attachment');
}
}
// message contains multiple parts
else if (is_array($structure->parts) && !empty($structure->parts)) {
// iterate over parts
for ($i=0; $i < count($structure->parts); $i++) {
$mail_part = &$structure->parts[$i];
$primary_type = $mail_part->ctype_primary;
$secondary_type = $mail_part->ctype_secondary;
$part_mimetype = $mail_part->mimetype;
// multipart/alternative or message/rfc822
if ($primary_type == 'multipart' || $part_mimetype == 'message/rfc822') {
// list message/rfc822 as attachment as well
if ($part_mimetype == 'message/rfc822') {
$this->add_part($mail_part, 'attachment');
}
$this->parse_structure($mail_part, true);
}
// part text/[plain|html] or delivery status
else if ((($part_mimetype == 'text/plain' || $part_mimetype == 'text/html') && $mail_part->disposition != 'attachment')
|| in_array($part_mimetype, ['message/delivery-status', 'text/rfc822-headers', 'message/disposition-notification'])
) {
// Allow plugins to handle also this part
$plugin = $this->app->plugins->exec_hook('message_part_structure', [
'object' => $this,
'structure' => $mail_part,
'mimetype' => $part_mimetype,
'recursive' => true
]);
if ($plugin['abort']) {
continue;
}
if ($part_mimetype == 'text/html' && $mail_part->size) {
$this->got_html_part = true;
}
$mail_part = $plugin['structure'];
list($primary_type, $secondary_type) = explode('/', $plugin['mimetype']);
// add text part if it matches the prefs
if (!$this->parse_alternative
|| ($secondary_type == 'html' && $this->opt['prefer_html'])
|| ($secondary_type == 'plain' && !$this->opt['prefer_html'])
) {
$mail_part->type = 'content';
$this->add_part($mail_part);
}
// list as attachment as well
if (!empty($mail_part->filename)) {
$this->add_part($mail_part, 'attachment');
}
}
// ignore "virtual" protocol parts
else if ($primary_type == 'protocol') {
continue;
}
// part is Microsoft Outlook TNEF (winmail.dat)
else if ($part_mimetype == 'application/ms-tnef' && $this->tnef_decode) {
$tnef_parts = (array) $this->tnef_decode($mail_part);
$tnef_body = '';
foreach ($tnef_parts as $tpart) {
$this->mime_parts[$tpart->mime_id] = $tpart;
if (strpos($tpart->mime_id, '.html')) {
$tnef_body = $tpart->body;
if ($this->opt['prefer_html']) {
$tpart->type = 'content';
// Reset type on the plain text part that usually is added to winmail.dat messages
// (on the same level in the structure as the attachment itself)
$level = count(explode('.', $mail_part->mime_id));
foreach ($this->parts as $p) {
if ($p->type == 'content' && $p->mimetype == 'text/plain'
&& count(explode('.', $p->mime_id)) == $level
) {
$p->type = null;
}
}
}
$this->add_part($tpart);
}
else {
$inline = !empty($tpart->content_id) && strpos($tnef_body, "cid:{$tpart->content_id}") !== false;
$this->add_part($tpart, $inline ? 'inline' : 'attachment');
}
}
// add winmail.dat to the list if it's content is unknown
if (empty($tnef_parts) && !empty($mail_part->filename)) {
$this->mime_parts[$mail_part->mime_id] = $mail_part;
$this->add_part($mail_part, 'attachment');
}
}
// part is a file/attachment
else if (
preg_match('/^(inline|attach)/', $mail_part->disposition)
|| !empty($mail_part->headers['content-id'])
|| ($mail_part->filename &&
(empty($mail_part->disposition) || preg_match('/^[a-z0-9!#$&.+^_-]+$/i', $mail_part->disposition)))
) {
// skip apple resource forks
if ($message_ctype_secondary == 'appledouble' && $secondary_type == 'applefile') {
continue;
}
if (!empty($mail_part->headers['content-id'])) {
$mail_part->content_id = preg_replace(['/^</', '/>$/'], '', $mail_part->headers['content-id']);
}
if (!empty($mail_part->headers['content-location'])) {
$mail_part->content_location = '';
if (!empty($mail_part->headers['content-base'])) {
$mail_part->content_location = $mail_part->headers['content-base'];
}
$mail_part->content_location .= $mail_part->headers['content-location'];
}
// application/smil message's are known to use inline images that aren't really inline (#8870)
// TODO: This code probably does not belong here. I.e. we should not default to
// disposition=inline in rcube_imap::structure_part().
if ($primary_type === 'image'
&& !empty($structure->ctype_parameters['type'])
&& $structure->ctype_parameters['type'] === 'application/smil'
) {
$mail_part->disposition = 'attachment';
}
// part belongs to a related message and is linked
// Note: mixed is not supposed to contain inline images, but we've found such examples (#5905)
if (
preg_match('/^multipart\/(related|relative|mixed)/', $mimetype)
&& (!empty($mail_part->content_id) || !empty($mail_part->content_location))
) {
$this->add_part($mail_part, 'inline');
}
// Any non-inline attachment
if (!preg_match('/^inline/i', $mail_part->disposition) || empty($mail_part->headers['content-id'])) {
// Content-Type name regexp according to RFC4288.4.2
if (!preg_match('/^[a-z0-9!#$&.+^_-]+\/[a-z0-9!#$&.+^_-]+$/i', $part_mimetype)) {
// replace malformed content type with application/octet-stream (#1487767)
$mail_part->ctype_primary = 'application';
$mail_part->ctype_secondary = 'octet-stream';
$mail_part->mimetype = 'application/octet-stream';
}
$this->add_part($mail_part, 'attachment');
}
}
// calendar part not marked as attachment (#1490325)
else if ($part_mimetype == 'text/calendar') {
if (!$mail_part->filename) {
$mail_part->filename = 'calendar.ics';
}
$this->add_part($mail_part, 'attachment');
}
// Last resort, non-inline and non-text part of multipart/mixed message (#7117)
else if ($mimetype == 'multipart/mixed'
&& $mail_part->disposition != 'inline'
&& $primary_type && $primary_type != 'text' && $primary_type != 'multipart'
) {
$this->add_part($mail_part, 'attachment');
}
}
// if this is a related part try to resolve references
// Note: mixed is not supposed to contain inline images, but we've found such examples (#5905)
if (preg_match('/^multipart\/(related|relative|mixed)/', $mimetype) && count($this->inline_parts)) {
$a_replaces = [];
$img_regexp = '/^image\/(gif|jpe?g|png|tiff|bmp|svg)/';
foreach ($this->inline_parts as $inline_object) {
$part_url = $this->get_part_url($inline_object->mime_id, $inline_object->ctype_primary);
if (isset($inline_object->content_id)) {
$a_replaces['cid:'.$inline_object->content_id] = $part_url;
}
if (!empty($inline_object->content_location)) {
$a_replaces[$inline_object->content_location] = $part_url;
}
if (!empty($inline_object->filename)) {
// MS Outlook sends sometimes non-related attachments as related
// In this case multipart/related message has only one text part
// We'll add all such attachments to the attachments list
if ($this->got_html_part === false) {
$this->add_part($inline_object, 'attachment');
}
// MS Outlook sometimes also adds non-image attachments as related
// We'll add all such attachments to the attachments list
// Warning: some browsers support pdf in <img/>
else if (!preg_match($img_regexp, $inline_object->mimetype)) {
$this->add_part($inline_object, 'attachment');
}
// @TODO: we should fetch HTML body and find attachment's content-id
// to handle also image attachments without reference in the body
// @TODO: should we list all image attachments in text mode?
}
}
// add replace array to each content part
// (will be applied later when part body is available)
foreach ($this->parts as $i => $part) {
if ($part->type == 'content') {
$this->parts[$i]->replaces = $a_replaces;
}
}
}
}
// message is a single part non-text
else if ($structure->filename || preg_match('/^application\//i', $mimetype)) {
$this->add_part($structure, 'attachment');
}
}
/**
* Fill a flat array with references to all parts, indexed by part numbers
*
* @param rcube_message_part $part Message body structure
*/
private function get_mime_numbers(&$part)
{
if (strlen($part->mime_id)) {
$this->mime_parts[$part->mime_id] = &$part;
}
if (is_array($part->parts)) {
for ($i=0; $i<count($part->parts); $i++) {
$this->get_mime_numbers($part->parts[$i]);
}
}
}
/**
* Add a part to object parts array(s) (with context check)
*
* @param rcube_message_part $part Message part
* @param string $type Part type (inline/attachment)
*/
private function add_part($part, $type = null)
{
if ($this->check_context($part)) {
// It may happen that we add the same part to the array many times
// use part ID index to prevent from duplicates
switch ($type) {
case 'inline': $this->inline_parts[(string) $part->mime_id] = $part; break;
case 'attachment': $this->attachments[(string) $part->mime_id] = $part; break;
default: $this->parts[] = $part; break;
}
}
}
/**
* Check if specified part belongs to the current context
*
* @param rcube_message_part $part Message part
*
* @return bool True if the part belongs to the current context, False otherwise
*/
private function check_context($part)
{
return $this->context === null || strpos($part->mime_id, $this->context . '.') === 0;
}
/**
* Decode a Microsoft Outlook TNEF part (winmail.dat)
*
* @param rcube_message_part $part Message part to decode
*
* @return rcube_message_part[] List of message parts extracted from TNEF
*/
function tnef_decode(&$part)
{
// @TODO: attachment may be huge, handle body via file
$body = $this->get_part_body($part->mime_id);
$tnef = new rcube_tnef_decoder;
$tnef_arr = $tnef->decompress($body, true);
$parts = [];
unset($body);
// HTML body
if (!empty($tnef_arr['message'])) {
$tpart = new rcube_message_part;
$tpart->encoding = 'stream';
$tpart->ctype_primary = 'text';
$tpart->ctype_secondary = 'html';
$tpart->mimetype = 'text/html';
$tpart->mime_id = 'winmail.' . $part->mime_id . '.html';
$tpart->size = strlen($tnef_arr['message']);
$tpart->body = $tnef_arr['message'];
$tpart->charset = RCUBE_CHARSET;
$parts[] = $tpart;
}
// Attachments
foreach ($tnef_arr['attachments'] as $pid => $winatt) {
$tpart = new rcube_message_part;
$tpart->filename = $this->fix_attachment_name(trim($winatt['name']), $part);
$tpart->encoding = 'stream';
$tpart->ctype_primary = trim(strtolower($winatt['type']));
$tpart->ctype_secondary = trim(strtolower($winatt['subtype']));
$tpart->mimetype = $tpart->ctype_primary . '/' . $tpart->ctype_secondary;
$tpart->mime_id = 'winmail.' . $part->mime_id . '.' . $pid;
$tpart->size = $winatt['size'] ?? 0;
$tpart->body = $winatt['stream'];
if (!empty($winatt['content-id'])) {
$tpart->content_id = $winatt['content-id'];
}
$parts[] = $tpart;
unset($tnef_arr[$pid]);
}
return $parts;
}
/**
* Parse message body for UUencoded attachments bodies
*
* @param rcube_message_part $part Message part to decode
*
* @return rcube_message_part[] List of message parts extracted from the file
*/
function uu_decode(&$part)
{
// @TODO: messages may be huge, handle body via file
$part->body = $this->get_part_body($part->mime_id);
$parts = [];
$pid = 0;
// FIXME: line length is max.65?
$uu_regexp_begin = '/begin [0-7]{3,4} ([^\r\n]+)\r?\n/s';
$uu_regexp_end = '/`\r?\nend((\r?\n)|($))/s';
while (preg_match($uu_regexp_begin, $part->body, $matches, PREG_OFFSET_CAPTURE)) {
$startpos = $matches[0][1];
if (!preg_match($uu_regexp_end, $part->body, $m, PREG_OFFSET_CAPTURE, $startpos)) {
break;
}
$endpos = $m[0][1];
$begin_len = strlen($matches[0][0]);
$end_len = strlen($m[0][0]);
// extract attachment body
$filebody = substr($part->body, $startpos + $begin_len, $endpos - $startpos - $begin_len - 1);
$filebody = str_replace("\r\n", "\n", $filebody);
// remove attachment body from the message body
$part->body = substr_replace($part->body, '', $startpos, $endpos + $end_len - $startpos);
// mark body as modified so it will not be cached by rcube_imap_cache
$part->body_modified = true;
// add attachments to the structure
$uupart = new rcube_message_part;
$uupart->filename = trim($matches[1][0]);
$uupart->encoding = 'stream';
$uupart->body = convert_uudecode($filebody);
$uupart->size = strlen($uupart->body);
$uupart->mime_id = 'uu.' . $part->mime_id . '.' . $pid;
$ctype = rcube_mime::file_content_type($uupart->body, $uupart->filename, 'application/octet-stream', true);
$uupart->mimetype = $ctype;
list($uupart->ctype_primary, $uupart->ctype_secondary) = explode('/', $ctype);
$parts[] = $uupart;
$pid++;
}
return $parts;
}
/**
* Fix attachment name encoding if needed and possible
*
* @param string $name Attachment name
* @param rcube_message_part $part Message part
*
* @return string Fixed attachment name
*/
protected function fix_attachment_name($name, $part)
{
if ($name == rcube_charset::clean($name)) {
return $name;
}
// find charset from part or its parent(s)
if ($part->charset) {
$charsets[] = $part->charset;
}
else {
// check first part (common case)
$n = strpos($part->mime_id, '.') ? preg_replace('/\.[0-9]+$/', '', $part->mime_id) . '.1' : 1;
if (($_part = $this->mime_parts[$n]) && $_part->charset) {
$charsets[] = $_part->charset;
}
// check parents' charset
$items = explode('.', $part->mime_id);
for ($i = count($items)-1; $i > 0; $i--) {
array_pop($items);
$parent = $this->mime_parts[implode('.', $items)];
if ($parent && $parent->charset) {
$charsets[] = $parent->charset;
}
}
}
if ($this->headers->charset) {
$charsets[] = $this->headers->charset;
}
if ($charset = rcube_charset::check($name, $charsets)) {
$name = rcube_charset::convert($name, $charset);
$part->charset = $charset;
}
return $name;
}
/**
* Deprecated methods (to be removed)
*/
public static function unfold_flowed($text)
{
return rcube_mime::unfold_flowed($text);
}
public static function format_flowed($text, $length = 72)
{
return rcube_mime::format_flowed($text, $length);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Apr 6, 1:33 AM (2 d, 12 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831848
Default Alt Text
(103 KB)
Attached To
Mode
R113 roundcubemail
Attached
Detach File
Event Timeline