Completed
Push — 16.1 ( b5b1ff...6fee3b )
by Klaus
29:58 queued 15:55
created

Mailer::convertMessageTextParts()   C

Complexity

Conditions 12
Paths 60

Size

Total Lines 56
Code Lines 33

Duplication

Lines 29
Ratio 51.79 %

Importance

Changes 0
Metric Value
cc 12
eloc 33
nc 60
nop 4
dl 29
loc 56
rs 6.7092
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * EGroupware API: Sending mail via Horde_Mime_Mail
4
 *
5
 * @link http://www.egroupware.org
6
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
7
 * @package api
8
 * @subpackage mail
9
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
10
 * @version $Id$
11
 */
12
13
namespace EGroupware\Api;
14
15
use Horde_Mime_Mail;
16
use Horde_Mime_Part;
17
use Horde_Mail_Rfc822_List;
18
use Horde_Mail_Exception;
19
use Horde_Mail_Transport;
20
use Horde_Mail_Transport_Null;
21
use Horde_Mime_Headers_MessageId;
22
use Horde_Text_Flowed;
23
use Horde_Stream;
24
use Horde_Stream_Wrapper_Combine;
25
use Horde_Mime_Headers;
26
27
/**
28
 * Sending mail via Horde_Mime_Mail
29
 *
30
 * Log mails to log file specified in $GLOBALS['egw_info']['server']['log_mail']
31
 * or regular error_log for true (can be set either in DB or header.inc.php).
32
 */
33
class Mailer extends Horde_Mime_Mail
34
{
35
	/**
36
	 * Mail account used for sending mail
37
	 *
38
	 * @var Mail\Account
39
	 */
40
	protected $account;
41
42
	/**
43
	 * Header / recipients set via Add(Address|Cc|Bcc|Replyto)
44
	 *
45
	 * @var Horde_Mail_Rfc822_List
46
	 */
47
	protected $to;
48
	protected $cc;
49
	protected $bcc;
50
	protected $replyto;
51
	/**
52
	 * Translates between interal Horde_Mail_Rfc822_List attributes and header names
53
	 *
54
	 * @var array
55
	 */
56
	static $type2header = array(
57
		'to' => 'To',
58
		'cc' => 'Cc',
59
		'bcc' => 'Bcc',
60
		'replyto' => 'Reply-To',
61
	);
62
63
	/**
64
	 * Constructor: always throw exceptions instead of echoing errors and EGw pathes
65
	 *
66
	 * @param int|Mail\Account|boolean $account =null mail account to use, default use Mail\Account::get_default($smtp=true)
67
	 *	false: no NOT initialise account and set other EGroupware specific headers, used to parse mails (not sending them!)
68
	 *	initbasic: return $this
69
	 */
70
	function __construct($account=null)
71
	{
72
		// Horde use locale for translation of error messages
73
		Preferences::setlocale(LC_MESSAGES);
74
75
		parent::__construct();
76
		if ($account ==='initbasic')
77
		{
78
			$this->_headers = new Horde_Mime_Headers();
79
			$this->clearAllRecipients();
80
			$this->clearReplyTos();
81
			error_log(__METHOD__.__LINE__.array2string($this));
82
			return $this;
0 ignored issues
show
Bug introduced by
Constructors do not have meaningful return values, anything that is returned from here is discarded. Are you sure this is correct?
Loading history...
83
		}
84
		if ($account !== false)
85
		{
86
			$this->_headers->setUserAgent('EGroupware API '.$GLOBALS['egw_info']['server']['versions']['api']);
87
88
			$this->setAccount($account);
89
90
			$this->is_html = false;
91
92
			$this->clearAllRecipients();
93
			$this->clearReplyTos();
94
95
			$this->clearParts();
96
		}
97
	}
98
99
	/**
100
	 * Clear all recipients: to, cc, bcc (but NOT reply-to!)
101
	 */
102
	function clearAllRecipients()
103
	{
104
		// clear all addresses
105
		$this->clearAddresses();
106
		$this->clearCCs();
107
		$this->clearBCCs();
108
	}
109
110
	/**
111
	 * Set mail account to use for sending
112
	 *
113
	 * @param int|Mail\Account $account =null mail account to use, default use Mail\Account::get_default($smtp=true)
114
	 * @throws Exception\NotFound if account was not found (or not valid for current user)
115
	 */
116
	function  setAccount($account=null)
117
	{
118
		if ($account instanceof Mail\Account)
119
		{
120
			$this->account = $account;
121
		}
122
		elseif ($account > 0)
123
		{
124
			$this->account = Mail\Account::read($account);
125
		}
126
		else
127
		{
128
			if (!($this->account = Mail\Account::get_default(true)))	// true = need an SMTP (not just IMAP) account
0 ignored issues
show
Documentation Bug introduced by
It seems like \EGroupware\Api\Mail\Account::get_default(true) can also be of type integer or string. However, the property $account is declared as type object<EGroupware\Api\Mail\Account>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
129
			{
130
				throw new Exception\NotFound('SMTP: '.lang('Account not found!'));
131
			}
132
		}
133
134
		try
135
		{
136
			$identity = Mail\Account::read_identity($this->account->ident_id, true, null, $this->account);
137
		}
138
		catch(Exception $e)
139
		{
140
			unset($e);
141
			error_log(__METHOD__.__LINE__.' Could not read_identity for account:'.$account['acc_id'].' with IdentID:'.$account['ident_id']);
142
			$identity['ident_email'] = $this->account->ident_email;
143
			$identity['ident_realname'] = $this->account->ident_realname ? $this->account->ident_realname : $this->account->ident_email;
144
		}
145
146
		// use smpt-username as sender/return-path, if available, but only if it is a full email address
147
		$sender = $this->account->acc_smtp_username && strpos($this->account->acc_smtp_username, '@') !== false ?
148
			$this->account->acc_smtp_username : $identity['ident_email'];
149
		$this->addHeader('Return-Path', '<'.$sender.'>', true);
150
151
		$this->setFrom($identity['ident_email'], $identity['ident_realname']);
152
	}
153
154
	/**
155
	 * Set From header
156
	 *
157
	 * @param string $address
158
	 * @param string $personal =''
159
	 */
160
	public function setFrom($address, $personal='')
161
	{
162
		$this->addHeader('From', self::add_personal($address, $personal));
163
	}
164
165
	/**
166
	 * Add one or multiple addresses to To, Cc, Bcc or Reply-To
167
	 *
168
	 * @param string|array|Horde_Mail_Rfc822_List $address
169
	 * @param string $personal ='' only used if $address is a string
170
	 * @param string $type ='to' type of address to add "to", "cc", "bcc" or "replyto"
171
	 */
172
	function addAddress($address, $personal='', $type='to')
173
	{
174
		if (!isset(self::$type2header[$type]))
175
		{
176
			throw new Exception\WrongParameter("Unknown type '$type'!");
177
		}
178
		if ($personal) $address = self::add_personal ($address, $personal);
179
180
		// add to our local list
181
		$this->$type->add($address);
182
183
		// add as header
184
		$this->addHeader(self::$type2header[$type], $this->$type, true);
185
	}
186
187
	/**
188
	 * Remove all addresses from To, Cc, Bcc or Reply-To
189
	 *
190
	 * @param string $type ='to' type of address to add "to", "cc", "bcc" or "replyto"
191
	 */
192
	function clearAddresses($type='to')
193
	{
194
		$this->$type = new Horde_Mail_Rfc822_List();
195
196
		$this->removeHeader(self::$type2header[$type]);
197
	}
198
199
	/**
200
	 * Get set to addressses
201
	 *
202
	 * @param string $type ='to' type of address to add "to", "cc", "bcc" or "replyto"
203
	 * @param boolean $return_array =false true: return array of string, false: Horde_Mail_Rfc822_List
204
	 * @return array|Horde_Mail_Rfc822_List supporting arrayAccess and Iterable
205
	 */
206
	function getAddresses($type='to', $return_array=false)
207
	{
208
		if ($return_array)
209
		{
210
			$addresses = array();
211
			foreach($this->$type as $addr)
212
			{
213
				$addresses[] = (string)$addr;
214
			}
215
			return $addresses;
216
		}
217
		return $this->$type;
218
	}
219
220
	/**
221
	 * Write Bcc as header for storing in sent or as draft
222
	 *
223
	 * Bcc is normally only add to recipients while sending, but not added visible as header.
224
	 *
225
	 * This function is should only be called AFTER calling send, or when NOT calling send at all!
226
	 */
227
	function forceBccHeader()
228
	{
229
		$this->_headers->removeHeader('Bcc');
230
231
		// only add Bcc header, if we have bcc's
232
		if (count($this->bcc))
233
		{
234
			$this->_headers->addHeader('Bcc', $this->bcc);
235
		}
236
	}
237
238
	/**
239
	 * Add personal part to email address
240
	 *
241
	 * @param string $address
242
	 * @param string $personal
243
	 * @return string Rfc822 address
244
	 */
245
	static function add_personal($address, $personal)
246
	{
247
		if (is_string($address) && !empty($personal))
248
		{
249
			//if (!preg_match('/^[!#$%&\'*+/0-9=?A-Z^_`a-z{|}~-]+$/u', $personal))	// that's how I read the rfc(2)822
250 View Code Duplication
			if ($personal && !preg_match('/^[0-9A-Z -]*$/iu', $personal))	// but quoting is never wrong, so quote more then necessary
251
			{
252
				$personal = '"'.str_replace(array('\\', '"'),array('\\\\', '\\"'), $personal).'"';
253
			}
254
			$address = ($personal ? $personal.' <' : '').$address.($personal ? '>' : '');
255
		}
256
		return $address;
257
	}
258
259
	/**
260
	 * Add one or multiple addresses to Cc
261
	 *
262
	 * @param string|array|Horde_Mail_Rfc822_List $address
263
	 * @param string $personal ='' only used if $address is a string
264
	 */
265
	function addCc($address, $personal=null)
266
	{
267
		$this->addAddress($address, $personal, 'cc');
268
	}
269
270
	/**
271
	 * Clear all cc
272
	 */
273
	function clearCCs()
274
	{
275
		$this->clearAddresses('cc');
276
	}
277
278
	/**
279
	 * Add one or multiple addresses to Bcc
280
	 *
281
	 * @param string|array|Horde_Mail_Rfc822_List $address
282
	 * @param string $personal ='' only used if $address is a string
283
	 */
284
	function addBcc($address, $personal=null)
285
	{
286
		$this->addAddress($address, $personal, 'bcc');
287
	}
288
289
	/**
290
	 * Clear all bcc
291
	 */
292
	function clearBCCs()
293
	{
294
		$this->clearAddresses('bcc');
295
	}
296
297
	/**
298
	 * Add one or multiple addresses to Reply-To
299
	 *
300
	 * @param string|array|Horde_Mail_Rfc822_List $address
301
	 * @param string $personal ='' only used if $address is a string
302
	 */
303
	function addReplyTo($address, $personal=null)
304
	{
305
		$this->addAddress($address, $personal, 'replyto');
306
	}
307
308
	/**
309
	 * Clear all reply-to
310
	 */
311
	function clearReplyTos()
312
	{
313
		$this->clearAddresses('replyto');
314
	}
315
316
	/**
317
	 * Get set ReplyTo addressses
318
	 *
319
	 * @return Horde_Mail_Rfc822_List supporting arrayAccess and Iterable
320
	 */
321
	function getReplyTo()
322
	{
323
		return $this->replyto;
324
	}
325
326
	/**
327
	 * Adds an attachment
328
	 *
329
	 * "text/calendar; method=..." get automatic detected and added as highest priority alternative
330
	 *
331
	 * @param string|resource $data Path to the attachment or open file-descriptor
332
	 * @param string $name =null file name to use for the attachment
333
	 * @param string $type =null content type of the file, incl. parameters eg. "text/plain; charset=utf-8"
334
	 * @param string $old_type =null used to support phpMailer signature (deprecated)
335
	 * @return integer part-number
336
	 * @throws Exception\NotFound if $file could not be opened for reading
337
	 */
338
	public function addAttachment($data, $name = null, $type = null, $old_type=null)
339
	{
340
		// deprecated PHPMailer::AddAttachment($path, $name = '', $encoding = 'base64', $type = 'application/octet-stream') call
341
		if ($type === 'base64')
342
		{
343
			$type = $old_type;
344
		}
345
346
		// pass file as resource to Horde_Mime_Part::setContent()
347
		if (is_resource($data))
348
		{
349
			$resource = $data;
350
		}
351
		elseif (!($resource = fopen($data, 'r')))
352
		{
353
			throw new Exception\NotFound("File '$data' not found!");
354
		}
355
356
		if (empty($type) && !is_resource($data)) $type = Vfs::mime_content_type($data);
357
358
		// set "text/calendar; method=*" as alternativ body
359
		$matches = null;
360 View Code Duplication
		if (preg_match('|^text/calendar; method=([^;]+)|i', $type, $matches))
361
		{
362
			$this->setAlternativBody($resource, $type, array('method' => $matches[1]), 'utf-8');
363
			return;
364
		}
365
366
		$part = new Horde_Mime_Part();
367
		$part->setType($type);
368
		// set content-type parameters, which get ignored by setType
369
		if (preg_match_all('/;\s*([^=]+)=([^;]*)/', $type, $matches))
370
		{
371
			foreach($matches[1] as $n => $label)
372
			{
373
				$part->setContentTypeParameter($label, $matches[2][$n]);
374
			}
375
		}
376
		$part->setContents($resource);
377
378
		// setting name, also sets content-disposition attachment (!), therefore we have to do it after "text/calendar; method=" handling
379
		if ($name || !is_resource($data)) $part->setName($name ? $name : Vfs::basename($data));
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 338 can also be of type resource; however, EGroupware\Api\Vfs::basename() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
380
381
		// this should not be necessary, because binary data get detected by mime-type,
382
		// but at least Cyrus complains about NUL characters
383
		if (substr($type, 0, 5) != 'text/') $part->setTransferEncoding('base64', array('send' => true));
384
		$part->setDisposition('attachment');
385
386
		return $this->addMimePart($part);
387
	}
388
389
	/**
390
	 * Adds an embedded image or other inline attachment
391
	 *
392
	 * @param string|resource $data Path to the attachment or open file-descriptor
393
	 * @param string $cid Content ID of the attachment.  Use this to identify
394
	 *        the Id for accessing the image in an HTML form.
395
	 * @param string $name Overrides the attachment name.
396
	 * @param string $type File extension (MIME) type.
397
	 * @return integer part-number
398
	 */
399
	public function addEmbeddedImage($data, $cid, $name = '', $type = 'application/octet-stream')
400
	{
401
		// deprecated PHPMailer::AddEmbeddedImage($path, $cid, $name='', $encoding='base64', $type='application/octet-stream') call
402
		if ($type === 'base64' || func_num_args() == 5)
403
		{
404
			$type = func_get_arg(4);
405
		}
406
407
		$part_id = $this->addAttachment($data, $name, $type);
408
		//error_log(__METHOD__."(".array2string($data).", '$cid', '$name', '$type') added with (temp.) part_id=$part_id");
409
410
		$part = $this->_parts[$part_id];
411
		$part->setDisposition('inline');
412
		$part->setContentId($cid);
413
414
		return $part_id;
415
	}
416
417
	/**
418
	 * Adds a string or binary attachment (non-filesystem) to the list.
419
	 *
420
	 * "text/calendar; method=..." get automatic detected and added as highest priority alternative,
421
	 * overwriting evtl. existing html body!
422
	 *
423
	 * @param string|resource $content String attachment data or open file descriptor
424
	 * @param string $filename Name of the attachment. We assume that this is NOT a path
425
	 * @param string $type File extension (MIME) type.
426
	 * @return int part-number
427
	 */
428
	public function addStringAttachment($content, $filename, $type = 'application/octet-stream')
429
	{
430
		// deprecated PHPMailer::AddStringAttachment($content, $filename = '', $encoding = 'base64', $type = 'application/octet-stream') call
431
		if ($type === 'base64' || func_num_args() == 4)
432
		{
433
			$type = func_get_arg(3);
434
		}
435
436
		// set "text/calendar; method=*" as alternativ body
437
		$matches = null;
438 View Code Duplication
		if (preg_match('|^text/calendar; method=([^;]+)|i', $type, $matches))
439
		{
440
			$this->setAlternativBody($content, $type, array('method' => $matches[1]), 'utf-8');
441
			return;
442
		}
443
444
		$part = new Horde_Mime_Part();
445
		$part->setType($type);
446
		$part->setCharset('utf-8');
447
		$part->setContents($content);
448
449
		// this should not be necessary, because binary data get detected by mime-type,
450
		// but at least Cyrus complains about NUL characters
451
		$part->setTransferEncoding('base64', array('send' => true));
452
		$part->setName($filename);
453
		$part->setDisposition('attachment');
454
455
		return $this->addMimePart($part);
456
	}
457
458
	/**
459
	 * Highest/last alternativ body part.
460
	 *
461
	 * @var Horde_Mime_Part
462
	 */
463
	protected $_alternativBody;
464
465
	/**
466
	 * Sets an alternativ body, eg. text/calendar has highest / last alternativ
467
	 *
468
	 * @param string|resource $content
469
	 * @param string $type eg. "text/calendar"
470
	 * @param array $parameters =array() eg. array('method' => 'REQUEST')
471
	 * @param string $charset =null default to $this->_charset="utf-8"
472
	 */
473
	function setAlternativBody($content, $type, $parameters=array(), $charset=null)
474
	{
475
		$this->_alternativBody = new Horde_Mime_Part();
476
		$this->_alternativBody->setType($type);
477
		foreach($parameters as $label => $data)
478
		{
479
			$this->_alternativBody->setContentTypeParameter($label, $data);
480
		}
481
		$this->_alternativBody->setCharset($charset ? $charset : $this->_charset);
482
		$this->_alternativBody->setContents($content);
483
		$this->_base = null;
484
	}
485
486
	/**
487
	 * Send mail, injecting mail transport from account
488
	 *
489
	 * Log mails to log file specified in $GLOBALS['egw_info']['server']['log_mail']
490
	 * or regular error_log for true (can be set either in DB or header.inc.php).
491
	 *
492
     * @param Horde_Mail_Transport $transport =null using transport from mail-account
493
	 *		specified in construct, or default one, if not specified
494
     * @param boolean $resend =true allways true in EGroupware!
495
     * @param boolean $flowed =null send message in flowed text format,
496
	 *		default null used flowed by default for everything but multipart/encrypted,
497
	 *		unless disabled in site configuration ("disable_rfc3676_flowed")
498
	 *
499
	 * @throws Exception\NotFound for no smtp account available
500
	 * @throws Horde_Mime_Exception
501
	 */
502
	function send(Horde_Mail_Transport $transport=null, $resend=true, $flowed=null)
503
	{
504
		unset($resend);	// parameter is not used, but required by function signature
505
506
		if (!($message_id = $this->getHeader('Message-ID')) &&
507
			class_exists('Horde_Mime_Headers_MessageId'))	// since 2.5.0
508
		{
509
			$message_id = Horde_Mime_Headers_MessageId::create('EGroupware');
510
			$this->addHeader('Message-ID', $message_id);
511
		}
512
		$body_sha1 = null;	// skip sha1, it requires whole mail in memory, which we traing to avoid now
513
514
		$mail_id = Hooks::process(array(
515
			'location' => 'send_mail',
516
			'subject' => $subject=$this->getHeader('Subject'),
517
			'from' => $this->getHeader('Return-Path') ? $this->getHeader('Return-Path') : $this->getHeader('From'),
518
			'to' => $to=$this->getAddresses('to', true),
519
			'cc' => $cc=$this->getAddresses('cc', true),
520
			'bcc' => $bcc=$this->getAddresses('bcc', true),
521
			'body_sha1' => $body_sha1,
522
			'message_id' => (string)$message_id,
523
		), array(), true);	// true = call all apps
524
525
		// check if we are sending an html mail with inline images
526
		if (!empty($this->_htmlBody) && count($this->_parts))
527
		{
528
			$related = null;
529
			foreach($this->_parts as $n => $part)
530
			{
531
				if ($part->getDisposition() == 'inline' && $part->getContentId())
532
				{
533
					// we need to send a multipart/related with html-body as first part and inline images as further parts
534
					if (!isset($related))
535
					{
536
						$related = new Horde_Mime_Part();
537
						$related->setType('multipart/related');
538
						$related[] = $this->_htmlBody;
539
						$this->_htmlBody = $related;
540
					}
541
					$related[] = $part;
542
					unset($this->_parts[$n]);
543
				}
544
			}
545
		}
546
547
		try {
548
			// no flowed for encrypted messages
549
			if (!isset($flowed)) $flowed = $this->_body && $this->_body->getType() != 'multipart/encrypted';
550
551
			// check if flowed is disabled in mail site configuration
552
			if (($config = Config::read('mail')) && $config['disable_rfc3676_flowed'])
553
			{
554
				$flowed = false;
555
			}
556
557
			// handling of alternativ body
558
			if (!empty($this->_alternativBody))
559
			{
560
				$body = new Horde_Mime_Part();
561
				$body->setType('multipart/alternative');
562
				if (!empty($this->_body))
563
				{
564
					// Send in flowed format.
565
					if ($flowed)
566
					{
567
						$text_flowed = new Horde_Text_Flowed($this->_body->getContents(), $this->_body->getCharset());
568
						$text_flowed->setDelSp(true);
569
						$this->_body->setContentTypeParameter('format', 'flowed');
570
						$this->_body->setContentTypeParameter('DelSp', 'Yes');
571
						$this->_body->setContents($text_flowed->toFlowed());
572
					}
573
					$body[] = $this->_body;
574
				}
575
				if (!empty($this->_htmlBody))
576
				{
577
					$body[] = $this->_htmlBody;
578
					unset($this->_htmlBody);
579
				}
580
				$body[] = $this->_alternativBody;
581
				unset($this->_alternativBody);
582
				$this->_body = $body;
583
				$flowed = false;
584
			}
585
			parent::send($transport ? $transport : $this->account->smtpTransport(), true,	$flowed);	// true: keep Message-ID
586
		}
587
		catch (\Exception $e) {
588
			// in case of errors/exceptions call hook again with previous returned mail_id and error-message to log
589
			Hooks::process(array(
590
				'location' => 'send_mail',
591
				'subject' => $subject,
592
				'from' => $this->getHeader('Return-Path') ? $this->getHeader('Return-Path') : $this->getHeader('From'),
593
				'to' => $to,
594
				'cc' => $cc,
595
				'bcc' => $bcc,
596
				'body_sha1' => $body_sha1,
597
				'message_id' => (string)$message_id,
598
				'mail_id' => $mail_id,
599
				'error' => $e->getMessage(),
600
			), array(), true);	// true = call all apps
601
		}
602
603
		// log mails to file specified in $GLOBALS['egw_info']['server']['log_mail'] or error_log for true
604
		if ($GLOBALS['egw_info']['server']['log_mail'])
605
		{
606
			$msg = $GLOBALS['egw_info']['server']['log_mail'] !== true ? date('Y-m-d H:i:s')."\n" : '';
607
			$msg .= (!isset($e) ? 'Mail send' : 'Mail NOT send').
608
				' to '.implode(', ', $to).' with subject: "'.$subject.'"';
609
610
			$msg .= ' from instance '.$GLOBALS['egw_info']['user']['domain'].' and IP '.Session::getuser_ip();
611
			$msg .= ' from user #'.$GLOBALS['egw_info']['user']['account_id'];
612
613
			if ($GLOBALS['egw_info']['user']['account_id'] && class_exists(__NAMESPACE__.'\\Accounts',false))
614
			{
615
				$msg .= ' ('.Accounts::username($GLOBALS['egw_info']['user']['account_id']).')';
616
			}
617
			if (isset($e))
618
			{
619
				$msg .= $GLOBALS['egw_info']['server']['log_mail'] !== true ? "\n" : ': ';
620
				$msg .= 'ERROR '.$e->getMessage();
621
			}
622
			$msg .= ' cc='.implode(', ', $cc).', bcc='.implode(', ', $bcc);
623
			if ($GLOBALS['egw_info']['server']['log_mail'] !== true) $msg .= "\n\n";
624
625
			error_log($msg,$GLOBALS['egw_info']['server']['log_mail'] === true ? 0 : 3,
626
				$GLOBALS['egw_info']['server']['log_mail']);
627
		}
628
		// rethrow error
629
		if (isset($e)) throw $e;
630
	}
631
632
633
	/**
634
	 * Reset all Settings to send multiple Messages
635
	 */
636
	function clearAll()
637
	{
638
		$this->__construct($this->account);
639
	}
640
641
	/**
642
	 * Get value of a header set with addHeader()
643
	 *
644
	 * @param string $header
645
	 * @return string|array
646
	 */
647
	function getHeader($header)
648
	{
649
		return $this->_headers ? $this->_headers->getValue($header) : null;
650
	}
651
652
	/**
653
     * Get the raw email data sent by this object.
654
     *
655
	 * Reimplement to be able to call it for saveAsDraft by calling
656
	 * $this->send(new Horde_Mail_Transport_Null()),
657
	 * if no base-part is set, because send is not called before.
658
	 *
659
     * @param  boolean $stream  If true, return a stream resource, otherwise
660
     * @return stream|string  The raw email data.
661
     */
662
	function getRaw($stream=true)
663
	{
664
		try {
665
			$this->getBasePart();
666
		}
667
		catch(Horde_Mail_Exception $e)
0 ignored issues
show
Bug introduced by
The class Horde_Mail_Exception does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
668
		{
669
			unset($e);
670
			parent::send(new Horde_Mail_Transport_Null(), true);	// true: keep Message-ID
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (send() instead of getRaw()). Are you sure this is correct? If so, you might want to change this to $this->send().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
671
		}
672
		// code copied from Horde_Mime_Mail::getRaw(), as there is no way to inject charset in
673
		// _headers->toString(), which is required to encode headers containing non-ascii chars correct
674
        if ($stream) {
675
            $hdr = new Horde_Stream();
676
            $hdr->add($this->_headers->toString(array('charset' => 'utf-8', 'canonical' => true)), true);
677
            return Horde_Stream_Wrapper_Combine::getStream(
678
                array($hdr->stream,
679
                      $this->getBasePart()->toString(
680
                        array('stream' => true, 'canonical' => true, 'encode' => Horde_Mime_Part::ENCODE_7BIT | Horde_Mime_Part::ENCODE_8BIT | Horde_Mime_Part::ENCODE_BINARY))
681
                )
682
            );
683
        }
684
685
        return $this->_headers->toString(array('charset' => 'utf-8', 'canonical' => true)) .
686
			$this->getBasePart()->toString(array('canonical' => true));
687
    }
688
689
	/**
690
	 * Convert charset of text-parts of message to utf-8. non static AND include Bcc
691
	 *
692
	 * @param string|resource $message
693
	 * @param boolean $stream =false return stream or string (default)
694
	 * @param string $charset ='utf-8' charset to convert to
695
	 * @param boolean &$converted =false on return if conversation was necessary
696
	 * @return string|stream
697
	 */
698
	function convertMessageTextParts($message, $stream=false, $charset='utf-8', &$converted=false)
699
        {
700
		$headers = Horde_Mime_Headers::parseHeaders($message);
701
		$this->addHeaders($headers);
702
		$base = Horde_Mime_Part::parseMessage($message);
703
		foreach($headers->toArray(array('nowrap' => true)) as $header => $value)
704
		{
705
			foreach((array)$value as $n => $val)
706
			{
707
				switch($header)
708
				{
709
					case 'Bcc':
710
					case 'bcc':
711
						//error_log(__METHOD__.__LINE__.':'.$header.'->'.$val);
712
						$this->addBcc($val);
713
						break;
714
				}
715
			}
716
		}
717 View Code Duplication
		foreach($base->partIterator() as $part)
718
		{
719
			if ($part->getPrimaryType()== 'text')
720
			{
721
				$charset = $part->getContentTypeParameter('charset');
722
				if ($charset && $charset != 'utf-8')
723
				{
724
					$content = Translation::convert($part->toString(array(
725
						'encode' => Horde_Mime_Part::ENCODE_BINARY,     // otherwise we cant recode charset
726
						)), $charset, 'utf-8');
727
					$part->setContents($content, array(
728
						'encode' => Horde_Mime_Part::ENCODE_BINARY,     // $content is NOT encoded
729
						));
730
					$part->setContentTypeParameter('charset', 'utf-8');
731
					if ($part === $base)
732
					{
733
						$this->addHeader('Content-Type', $part->getType(true));
734
						// need to set Transfer-Encoding used by base-part, it always seems to be "quoted-printable"
735
						$this->addHeader('Content-Transfer-Encoding', 'quoted-printable');
736
					}
737
					$converted = true;
738
				}
739
			}
740
			elseif ($part->getType() == 'message/rfc822')
741
			{
742
				$mailerWithIn = new Mailer('initbasic');
743
				$part->setContents($mailerWithIn->convertMessageTextParts($part->toString(), $stream, $charset, $converted));
744
			}
745
		}
746
		if ($converted)
747
		{
748
			$this->setBasePart($base);
749
			$this->forceBccHeader();
750
			return $this->getRaw($stream);
751
		}
752
		return $message;
753
	}
754
755
	/**
756
	 * Convert charset of text-parts of message to utf-8
757
	 *
758
	 * @param string|resource $message
759
	 * @param boolean $stream =false return stream or string (default)
760
	 * @param string $charset ='utf-8' charset to convert to
761
	 * @param boolean &$converted =false on return if conversation was necessary
762
	 * @return string|stream
763
	 */
764
	static function convert($message, $stream=false, $charset='utf-8', &$converted=false)
765
	{
766
		$mailer = new Mailer(false);	// false = no default headers and mail account
767
		$mailer->addHeaders(Horde_Mime_Headers::parseHeaders($message));
768
		$base = Horde_Mime_Part::parseMessage($message);
769 View Code Duplication
		foreach($base->partIterator() as $part)
770
		{
771
			if ($part->getPrimaryType()== 'text')
772
			{
773
				$charset = $part->getContentTypeParameter('charset');
774
				if ($charset && $charset != 'utf-8')
775
				{
776
					$content = Translation::convert($part->toString(array(
777
						'encode' => Horde_Mime_Part::ENCODE_BINARY,	// otherwise we cant recode charset
778
					)), $charset, 'utf-8');
779
					$part->setContents($content, array(
780
						'encode' => Horde_Mime_Part::ENCODE_BINARY,	// $content is NOT encoded
781
					));
782
					$part->setContentTypeParameter('charset', 'utf-8');
783
					if ($part === $base)
784
					{
785
						$mailer->addHeader('Content-Type', $part->getType(true));
786
						// need to set Transfer-Encoding used by base-part, it always seems to be "quoted-printable"
787
						$mailer->addHeader('Content-Transfer-Encoding', 'quoted-printable');
788
					}
789
					$converted = true;
790
				}
791
			}
792
			elseif ($part->getType() == 'message/rfc822')
793
			{
794
				$part->setContents(self::convert($part->toString(), $stream, $charset, $converted));
795
			}
796
		}
797
		if ($converted)
798
		{
799
			$mailer->setBasePart($base);
800
			return $mailer->getRaw($stream);
801
		}
802
		return $message;
803
	}
804
805
	/**
806
	 * Find body: 1. part with mimetype "text/$subtype"
807
	 *
808
	 * Use getContents() on non-null return-value to get string content
809
	 *
810
	 * @param string $subtype =null
811
	 * @return Horde_Mime_Part part with body or null
812
	 */
813
	function findBody($subtype=null)
814
	{
815
		try {
816
			$base = $this->getBasePart();
817
			if (!($part_id = $base->findBody($subtype))) return null;
818
			return $base->getPart($part_id);
819
		}
820
		catch (Exception $e) {
821
			unset($e);
822
			return $subtype == 'html' ? $this->_htmlBody : $this->_body;
823
		}
824
	}
825
826
	/**
827
	 * Parse base-part into _body, _htmlBody, _alternativBody and _parts to eg. add further attachments
828
	 */
829 View Code Duplication
	function parseBasePart()
830
	{
831
		try {
832
			$base = $this->getBasePart();
833
			$plain_id = $base->findBody('plain');
834
			$html_id = $base->findBody('html');
835
836
			// find further alternativ part
837
			if ($base->getType() == 'multipart/alternativ' && count($base) !== ($html_id ? $html_id : $plain_id))
838
			{
839
				$alternativ_id = (string)count($base);
840
			}
841
842
			$this->_body = $this->_htmlBody = $this->_alternativBody = null;
843
			$this->clearParts();
844
845
			foreach($base->partIterator() as $part)
846
			{
847
				$id = $part->getMimeId();
848
				//error_log(__METHOD__."() plain=$plain_id, html=$html_id: $id: ".$part->getType());
849
				switch($id)
850
				{
851
					case '0':	// base-part itself
852
						continue 2;
853
					case $plain_id:
854
						$this->_body = $part;
855
						break;
856
					case $html_id:
857
						$this->_htmlBody = $part;
858
						break;
859
					case $alternativ_id:
860
						$this->_alternativBody = $part;
861
						break;
862
					default:
863
						$this->_parts[] = $part;
864
				}
865
			}
866
			$this->setBasePart(null);
867
		}
868
		catch (\Exception $e) {
869
			// ignore that there is no base-part yet, so nothing to do
870
			unset($e);
871
		}
872
	}
873
874
	/**
875
	 * clearAttachments, does the same as parseBasePart, but does not add possible attachments
876
	 */
877 View Code Duplication
	function ClearAttachments()
878
	{
879
		try {
880
			$base = $this->getBasePart();
881
			$plain_id = $base->findBody('plain');
882
			$html_id = $base->findBody('html');
883
884
			// find further alternativ part
885
			if ($base->getType() == 'multipart/alternativ' && count($base) !== ($html_id ? $html_id : $plain_id))
886
			{
887
				$alternativ_id = (string)count($base);
888
			}
889
890
			$this->_body = $this->_htmlBody = $this->_alternativBody = null;
891
			$this->clearParts();
892
893
			foreach($base->partIterator() as $part)
894
			{
895
				$id = $part->getMimeId();
896
				//error_log(__METHOD__."() plain=$plain_id, html=$html_id: $id: ".$part->getType());
897
				switch($id)
898
				{
899
					case '0':	// base-part itself
900
						continue 2;
901
					case $plain_id:
902
						$this->_body = $part;
903
						break;
904
					case $html_id:
905
						$this->_htmlBody = $part;
906
						break;
907
					case $alternativ_id:
908
						$this->_alternativBody = $part;
909
						break;
910
					default:
911
				}
912
			}
913
			$this->setBasePart(null);
914
		}
915
		catch (\Exception $e) {
916
			// ignore that there is no base-part yet, so nothing to do
917
			unset($e);
918
		}
919
	}
920
921
	/**
922
	 * Adds a MIME message part.
923
	 *
924
	 * Reimplemented to add parts / attachments if message was parsed / already has a base-part
925
	 *
926
	 * @param Horde_Mime_Part $part  A Horde_Mime_Part object.
927
	 * @return integer  The part number.
928
	 */
929
	public function addMimePart($part)
930
	{
931
		if ($this->_base) $this->parseBasePart();
932
933
		return parent::addMimePart($part);
934
	}
935
936
	/**
937
	 * Sets OpenPGP encrypted body according to rfc3156, section 4
938
	 *
939
	 * @param string $body             The message content.
940
	 * @link https://tools.ietf.org/html/rfc3156#section-4
941
	 */
942
	public function setOpenPgpBody($body)
943
	{
944
		$this->_body = new Horde_Mime_Part();
945
		$this->_body->setType('multipart/encrypted');
946
		$this->_body->setContentTypeParameter('protocol', 'application/pgp-encrypted');
947
		$this->_body->setContents('');
948
949
		$part1 = new Horde_Mime_Part();
950
		$part1->setType('application/pgp-encrypted');
951
		$part1->setContents("Version: 1\r\n", array('encoding' => '7bit'));
952
		$this->_body->addPart($part1);
953
954
		$part2 = new Horde_Mime_Part();
955
		$part2->setType('application/octet-stream');
956
		$part2->setContents($body, array('encoding' => '7bit'));
957
		$this->_body->addPart($part2);
958
959
		$this->_base = null;
960
	}
961
962
	/**
963
	 * Clear all non-standard headers
964
	 *
965
	 * Used in merge-print to remove headers before sending "new" mail
966
	 */
967
	function clearCustomHeaders()
968
	{
969
		foreach($this->_headers->toArray() as $header => $value)
970
		{
971
			if (stripos($header, 'x-') === 0 || $header == 'Received')
972
			{
973
				$this->_headers->removeHeader($header);
974
			}
975
			unset($value);
976
		}
977
	}
978
}
979