Completed
Push — master ( 6de0d6...5e02f7 )
by Christoph
10:48
created

IMAPMessage::getFullMessage()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 15.2377

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 24
ccs 2
cts 18
cp 0.1111
rs 8.6845
c 1
b 0
f 0
cc 4
eloc 18
nc 6
nop 2
crap 15.2377
1
<?php
2
/**
3
 * @author Alexander Weidinger <[email protected]>
4
 * @author Christoph Wurst <[email protected]>
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Jan-Christoph Borchardt <[email protected]>
7
 * @author Robin McCorkell <[email protected]>
8
 * @author Scrutinizer Auto-Fixer <[email protected]>
9
 * @author Thomas Mueller <[email protected]>
10
 * @author Thomas Müller <[email protected]>
11
 *
12
 * ownCloud - Mail
13
 *
14
 * This code is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License, version 3,
16
 * as published by the Free Software Foundation.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License, version 3,
24
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
25
 *
26
 */
27
namespace OCA\Mail\Model;
28
29
/**
30
 * ownCloud - Mail app
31
 *
32
 * @author Thomas Müller
33
 * @copyright 2012, 2013 Thomas Müller [email protected]
34
 *
35
 * @author Christoph Wurst <[email protected]>
36
 * @copyright Christoph Wurst 2015
37
 *
38
 * This library is free software; you can redistribute it and/or
39
 * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
40
 * License as published by the Free Software Foundation; either
41
 * version 3 of the License, or any later version.
42
 *
43
 * This library is distributed in the hope that it will be useful,
44
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
45
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
46
 * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
47
 *
48
 * You should have received a copy of the GNU Lesser General Public
49
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
50
 *
51
 */
52
53
use Closure;
54
use Exception;
55
use Horde_Imap_Client;
56
use Horde_Imap_Client_Data_Fetch;
57
use Horde_Mail_Rfc822_List;
58
use OCP\Files\File;
59
use OCA\Mail\Service\Html;
60
use OCP\AppFramework\Db\DoesNotExistException;
61
use OCP\Util;
62
63
class IMAPMessage implements IMessage {
64
65
	use ConvertAddresses;
66
67
	/**
68
	 * @var string[]
69
	 */
70
	private $attachmentsToIgnore = ['signature.asc', 'smime.p7s'];
71
72
	/** @var string */
73
	private $uid;
74
75
	/**
76
	 * @param \Horde_Imap_Client_Socket|null $conn
77
	 * @param \Horde_Imap_Client_Mailbox $mailBox
78
	 * @param integer $messageId
79
	 * @param \Horde_Imap_Client_Data_Fetch|null $fetch
80
	 * @param boolean $loadHtmlMessage
81
	 * @param Html|null $htmlService
82
	 */
83 9
	public function __construct($conn, $mailBox, $messageId, $fetch=null,
84
		$loadHtmlMessage=false, $htmlService = null) {
85 9
		$this->conn = $conn;
86 9
		$this->mailBox = $mailBox;
87 9
		$this->messageId = $messageId;
88 9
		$this->loadHtmlMessage = $loadHtmlMessage;
89
90 9
		$this->htmlService = $htmlService;
0 ignored issues
show
Bug introduced by
The property htmlService does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
91 9
		if (is_null($htmlService)) {
92 8
			$urlGenerator = \OC::$server->getURLGenerator();
93 8
			$request = \OC::$server->getRequest();
94 8
			$this->htmlService = new Html($urlGenerator, $request);
95 8
		}
96
97 9
		if ($fetch === null) {
98 1
			$this->loadMessageBodies();
99 1
		} else {
100 8
			$this->fetch = $fetch;
101
		}
102 9
	}
103
104
	// output all the following:
105
	public $header = null;
106
	public $htmlMessage = '';
107
	public $plainMessage = '';
108 1
	public $attachments = [];
109
	private $loadHtmlMessage = false;
110
	private $hasHtmlMessage = false;
111
112
	/**
113
	 * @var \Horde_Imap_Client_Socket
114
	 */
115
	private $conn;
116
117
	/**
118
	 * @var \Horde_Imap_Client_Mailbox
119
	 */
120
	private $mailBox;
121
	private $messageId;
122
123
	/**
124
	 * @var \Horde_Imap_Client_Data_Fetch
125
	 */
126
	private $fetch;
127
128
	/**
129
	 * @return int
130
	 */
131 7
	public function getUid() {
132 7
		if (!is_null($this->uid)) {
133
			return $this->uid;
134
		}
135 6
		return $this->fetch->getUid();
136
	}
137
138 7
	public function setUid($uid) {
139
		$this->uid = $uid;
140 7
		$this->attachments = array_map(function($attachment) use ($uid) {
141
			$attachment['messageId'] = $uid;
142 6
			return $attachment;
143 7
		}, $this->attachments);
144
	}
145
146
	/**
147
	 * @return array
148
	 */
149 7
	public function getFlags() {
150 7
		$flags = $this->fetch->getFlags();
151
		return [
152 6
			'unseen' => !in_array(Horde_Imap_Client::FLAG_SEEN, $flags),
153 6
			'flagged' => in_array(Horde_Imap_Client::FLAG_FLAGGED, $flags),
154 6
			'answered' => in_array(Horde_Imap_Client::FLAG_ANSWERED, $flags),
155 6
			'deleted' => in_array(Horde_Imap_Client::FLAG_DELETED, $flags),
156 6
			'draft' => in_array(Horde_Imap_Client::FLAG_DRAFT, $flags),
157 6
			'forwarded' => in_array(Horde_Imap_Client::FLAG_FORWARDED, $flags),
158 6
			'hasAttachments' => $this->hasAttachments($this->fetch->getStructure())
159 6
		];
160
	}
161
162
	/**
163
	 * @param array $flags
164
	 */
165
	public function setFlags(array $flags) {
166
		// TODO: implement
167
		throw new Exception('Not implemented');
168
	}
169
170
	/**
171
	 * @return \Horde_Imap_Client_Data_Envelope
172
	 */
173 8
	public function getEnvelope() {
174 8
		return $this->fetch->getEnvelope();
175
	}
176
177
	/**
178
	 * @return string
179
	 */
180 7
	public function getFromEmail() {
181 7
		$e = $this->getEnvelope();
182 7
		$from = $e->from[0];
183 7
		return $from ? $from->bare_address : null;
184
	}
185
186
	/**
187
	 * @return string
188
	 */
189 7
	public function getFrom() {
190 7
		$e = $this->getEnvelope();
191 7
		$from = $e->from[0];
192 7
		return $from ? $from->label : null;
193
	}
194
195
	/**
196
	 * @param string $from
197
	 * @throws Exception
198
	 */
199
	public function setFrom($from) {
200
		throw new Exception('IMAP message is immutable');
201
	}
202
203
	/**
204
	 * @return array
205
	 */
206 6
	public function getFromList() {
207 6
		$e = $this->getEnvelope();
208 6
		return $this->convertAddressList($e->from);
209
	}
210
211
	/**
212
	 * @return string
213
	 */
214 6
	public function getToEmail() {
215 6
		$e = $this->getEnvelope();
216 6
		$to = $e->to[0];
217 6
		return $to ? $to->bare_address : null;
218
	}
219
220 6
	public function getTo() {
221 6
		$e = $this->getEnvelope();
222 6
		$to = $e->to[0];
223 6
		return $to ? $to->label : null;
224
	}
225
226
	/**
227
	 * @param Horde_Mail_Rfc822_List $to
228
	 * @throws Exception
229
	 */
230
	public function setTo(Horde_Mail_Rfc822_List $to) {
231
		throw new Exception('IMAP message is immutable');
232
	}
233
234
	/**
235
	 * @return string
236
	 */
237 6 View Code Duplication
	public function getToList($assoc = false) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
238 6
		$e = $this->getEnvelope();
239 6
		if ($assoc) {
240 6
			return $this->convertAddressList($e->to);
241
		} else {
242
			return $this->hordeListToStringArray($e->to);
243
		}
244
	}
245
246 6 View Code Duplication
	public function getCCList($assoc = false) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
247 6
		$e = $this->getEnvelope();
248 6
		if ($assoc) {
249 6
			return $this->convertAddressList($e->cc);
250
		} else {
251
			return $this->hordeListToStringArray($e->cc);
252
		}
253
	}
254
255
	public function setCC(Horde_Mail_Rfc822_List $cc) {
256
		throw new Exception('IMAP message is immutable');
257
	}
258
259 View Code Duplication
	public function getBCCList($assoc = false) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
260
		$e = $this->getEnvelope();
261
		if ($assoc) {
262
			return $this->convertAddressList($e->bcc);
263
		} else {
264
			return $this->hordeListToStringArray($e->bcc);
265
		}
266
	}
267
268
	public function setBcc(Horde_Mail_Rfc822_List $bcc) {
269
		throw new Exception('IMAP message is immutable');
270
	}
271
272
	public function getReplyToList() {
273
		$e = $this->getEnvelope();
274
		return $this->convertAddressList($e->from);
275
	}
276
277
	public function setReplyTo(array $replyTo) {
0 ignored issues
show
Unused Code introduced by
The parameter $replyTo is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
278
		throw new Exception('IMAP message is immutable');
279
	}
280
281
	/**
282
	 * on reply, fill cc with everyone from to and cc except yourself
283
	 *
284
	 * @param string $ownMail
285
	 */
286 1
	public function getReplyCcList($ownMail) {
287 1
		$e = $this->getEnvelope();
288 1
		$list = new \Horde_Mail_Rfc822_List();
289 1
		$list->add($e->to);
290 1
		$list->add($e->cc);
291 1
		$list->unique();
292 1
		$list->remove($ownMail);
293 1
		return $this->convertAddressList($list);
294
	}
295
296
	/**
297
	 * Get the ID if available
298
	 *
299
	 * @return int|null
300
	 */
301
	public function getMessageId() {
302
		$e = $this->getEnvelope();
303
		return $e->message_id;
304
	}
305
306
	/**
307
	 * @return string
308
	 */
309 6
	public function getSubject() {
310 6
		$e = $this->getEnvelope();
311 6
		return $e->subject;
312
	}
313
314
	/**
315
	 * @param string $subject
316
	 * @throws Exception
317
	 */
318
	public function setSubject($subject) {
319
		throw new Exception('IMAP message is immutable');
320
	}
321
322
	/**
323
	 * @return \Horde_Imap_Client_DateTime
324
	 */
325 6
	public function getSentDate() {
326 6
		return $this->fetch->getImapDate();
327
	}
328
329 6
	public function getSize() {
330 6
		return $this->fetch->getSize();
331
	}
332
333
	/**
334
	 * @param \Horde_Mime_Part $part
335
	 * @return bool
336
	 */
337 6
	private function hasAttachments($part) {
338 6
		foreach($part->getParts() as $p) {
339
			/**
340
			 * @var \Horde_Mime_Part $p
341
			 */
342
			$filename = $p->getName();
343
344
			if(!is_null($p->getContentId())) {
345
				continue;
346
			}
347
			if(isset($filename)) {
348
				// do not show technical attachments
349
				if(in_array($filename, $this->attachmentsToIgnore)) {
350
					continue;
351
				} else {
352
					return true;
353
				}
354
			}
355
			if($this->hasAttachments($p)) {
356
				return true;
357
			}
358 6
		}
359
360 6
		return false;
361
	}
362
363 1
	private function loadMessageBodies() {
364 1
		$headers = [];
365
366 1
		$fetch_query = new \Horde_Imap_Client_Fetch_Query();
367 1
		$fetch_query->envelope();
368 1
		$fetch_query->structure();
369 1
		$fetch_query->flags();
370 1
		$fetch_query->size();
371 1
		$fetch_query->imapDate();
372
373 1
		$headers = array_merge($headers, [
374 1
			'importance',
375 1
			'list-post',
376
			'x-priority'
377 1
		]);
378 1
		$headers[] = 'content-type';
379
380 1
		$fetch_query->headers('imp', $headers, [
381 1
			'cache' => true,
382
			'peek'  => true
383 1
		]);
384
385
		// $list is an array of Horde_Imap_Client_Data_Fetch objects.
386 1
		$ids = new \Horde_Imap_Client_Ids($this->messageId);
387 1
		$headers = $this->conn->fetch($this->mailBox, $fetch_query, ['ids' => $ids]);
388
		/** @var $fetch \Horde_Imap_Client_Data_Fetch */
389 1
		$fetch = $headers[$this->messageId];
390 1
		if (is_null($fetch)) {
391
			throw new DoesNotExistException("This email ($this->messageId) can't be found. Probably it was deleted from the server recently. Please reload.");
392
		}
393
394
		// set $this->fetch to get to, from ...
395 1
		$this->fetch = $fetch;
396
397
		// analyse the body part
398 1
		$structure = $fetch->getStructure();
399
400
		// debugging below
401 1
		$structure_type = $structure->getPrimaryType();
402 1
		if ($structure_type == 'multipart') {
403 1
			$i = 1;
404 1
			foreach($structure->getParts() as $p) {
405 1
				$this->getPart($p, $i++);
406 1
			}
407 1
		} else {
408
			if ($structure->findBody() != null) {
409
				// get the body from the server
410
				$partId = $structure->findBody();
411
				$this->getPart($structure->getPart($partId), $partId);
412
			}
413
		}
414 1
	}
415
416
	/**
417
	 * @param $p \Horde_Mime_Part
418
	 * @param $partNo
419
	 */
420 7
	private function getPart($p, $partNo) {
421
		// ATTACHMENT
422
		// Any part with a filename is an attachment,
423
		// so an attached text file (type 0) is not mistaken as the message.
424 1
		$filename = $p->getName();
425 1
		if(isset($filename)) {
426
			if(in_array($filename, $this->attachmentsToIgnore)) {
427
				return;
428
			}
429
			$this->attachments[]= [
430
				'id' => $p->getMimeId(),
431
				'messageId' => $this->messageId,
432
				'fileName' => $filename,
433
				'mime' => $p->getType(),
434
				'size' => $p->getBytes(),
435
				'cid' => $p->getContentId()
436
			];
437
			return;
438
		}
439
440 1
		if ($p->getPrimaryType() === 'multipart') {
441
			$this->handleMultiPartMessage($p, $partNo);
442
			return;
443
		}
444
445 1
		if ($p->getType() === 'text/plain') {
446 1
			$this->handleTextMessage($p, $partNo);
447 1
			return;
448
		}
449
450
		// TEXT
451 7
		if ($p->getType() === 'text/calendar') {
452
			// TODO: skip inline ics for now
453
			return;
454
		}
455
456 1
		if ($p->getType() === 'text/html') {
457 1
			$this->handleHtmlMessage($p, $partNo);
458 1
			return;
459
		}
460
461
		// EMBEDDED MESSAGE
462
		// Many bounce notifications embed the original message as type 2,
463
		// but AOL uses type 1 (multipart), which is not handled here.
464
		// There are no PHP functions to parse embedded messages,
465
		// so this just appends the raw source to the main message.
466
		if ($p[0]=='message') {
467
			$data = $this->loadBodyData($p, $partNo);
468
			$this->plainMessage .= trim($data) ."\n\n";
469
		}
470
	}
471
472
	/**
473
	 * @param string $ownMail
474
	 * @param string $specialRole
475
	 */
476 1
	public function getFullMessage($ownMail, $specialRole=null) {
477
		$mailBody = $this->plainMessage;
478
479
		$data = $this->getListArray();
480
		if ($this->hasHtmlMessage) {
481
			$data['hasHtmlBody'] = true;
482
		} else {
483
			$mailBody = $this->htmlService->convertLinks($mailBody);
484
			list($mailBody, $signature) = $this->htmlService->parseMailBody($mailBody);
485
			$data['body'] = $specialRole === 'drafts' ? $mailBody : nl2br($mailBody);
486
			$data['signature'] = $signature;
487
		}
488
489
		$data['attachments'] = $this->attachments;
490
491
		if ($specialRole === 'sent') {
492
			$data['replyToList'] = $this->getToList(true);
493
			$data['replyCcList'] = $this->getCCList(true);
494
		} else {
495
			$data['replyToList'] = $this->getReplyToList(true);
0 ignored issues
show
Unused Code introduced by
The call to IMAPMessage::getReplyToList() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
496 1
			$data['replyCcList'] = $this->getReplyCcList($ownMail);
497
		}
498
		return $data;
499
	}
500
501 6
	public function getListArray() {
502 6
		$data = [];
503 6
		$data['id'] = $this->getUid();
504 6
		$data['from'] = $this->getFrom();
505 6
		$data['fromEmail'] = $this->getFromEmail();
506 6
		$data['fromList'] = $this->getFromList();
507 6
		$data['to'] = $this->getTo();
508 6
		$data['toEmail'] = $this->getToEmail();
509 6
		$data['toList'] = $this->getToList(true);
510 6
		$data['subject'] = $this->getSubject();
511 6
		$data['date'] = Util::formatDate($this->getSentDate()->format('U'));
512 6
		$data['size'] = Util::humanFileSize($this->getSize());
513 6
		$data['flags'] = $this->getFlags();
514 6
		$data['dateInt'] = $this->getSentDate()->getTimestamp();
515 6
		$data['dateIso'] = $this->getSentDate()->format('c');
516 6
		$data['ccList'] = $this->getCCList(true);
517 6
		return $data;
518
	}
519
520
	/**
521
	 * @param int     $accountId
522
	 * @param string  $folderId
523
	 * @param int     $messageId
524
	 * @param Closure $attachments
525
	 * @return string
526
	 */
527 1
	public function getHtmlBody($accountId, $folderId, $messageId, Closure $attachments) {
528 1
		return $this->htmlService->sanitizeHtmlMailBody($this->htmlMessage, [
529 1
			'accountId' => $accountId,
530 1
			'folderId' => $folderId,
531 1
			'messageId' => $messageId,
532 1
		], $attachments);
533
	}
534
535
	/**
536
	 * @return string
537
	 */
538 1
	public function getPlainBody() {
539 1
		return $this->plainMessage;
540
	}
541
542
	/**
543
	 * @param \Horde_Mime_Part $part
544
	 * @param int $partNo
545
	 */
546
	private function handleMultiPartMessage($part, $partNo) {
547
		$i = 1;
548
		foreach ($part->getParts() as $p) {
549
			$this->getPart($p, "$partNo.$i");
550
			$i++;
551
		}
552
	}
553
554
	/**
555
	 * @param \Horde_Mime_Part $p
556
	 * @param int $partNo
557
	 */
558 1
	private function handleTextMessage($p, $partNo) {
559 1
		$data = $this->loadBodyData($p, $partNo);
560 1
		$data = Util::sanitizeHTML($data);
561 1
		$this->plainMessage .= trim($data) ."\n\n";
562 1
	}
563
564
	/**
565
	 * @param \Horde_Mime_Part $p
566
	 * @param int $partNo
567
	 */
568 1
	private function handleHtmlMessage($p, $partNo) {
569 1
		$this->hasHtmlMessage = true;
570 1
		if ($this->loadHtmlMessage) {
571 1
			$data = $this->loadBodyData($p, $partNo);
572 1
			$this->htmlMessage .= $data . "<br><br>";
573 1
		}
574 1
	}
575
576
	/**
577
	 * @param \Horde_Mime_Part $p
578
	 * @param int $partNo
579
	 * @return string
580
	 * @throws DoesNotExistException
581
	 * @throws \Exception
582
	 */
583 1
	private function loadBodyData($p, $partNo) {
584
		// DECODE DATA
585 1
		$fetch_query = new \Horde_Imap_Client_Fetch_Query();
586 1
		$ids = new \Horde_Imap_Client_Ids($this->messageId);
587
588 1
		$fetch_query->bodyPart($partNo, [
589
		    'peek' => true
590 1
		]);
591 1
		$fetch_query->bodyPartSize($partNo);
592 1
		$fetch_query->mimeHeader($partNo, [
593
		    'peek' => true
594 1
		]);
595
596 1
		$headers = $this->conn->fetch($this->mailBox, $fetch_query, ['ids' => $ids]);
597
		/** @var $fetch \Horde_Imap_Client_Data_Fetch */
598 1
		$fetch = $headers[$this->messageId];
599 1
		if (is_null($fetch)) {
600
			throw new DoesNotExistException("Mail body for this mail($this->messageId) could not be loaded");
601
		}
602
603 1
		$mimeHeaders = $fetch->getMimeHeader($partNo, Horde_Imap_Client_Data_Fetch::HEADER_PARSE);
604 1
		if ($enc = $mimeHeaders->getValue('content-transfer-encoding')) {
605
			$p->setTransferEncoding($enc);
606
		}
607
608 1
		$data = $fetch->getBodyPart($partNo);
609
610 1
		$p->setContents($data);
611 1
		$data = $p->getContents();
612
613 1
		$data = iconv($p->getCharset(), 'utf-8//IGNORE', $data);
614 1
		return $data;
615
	}
616
617
	public function getContent() {
618
		return $this->getPlainBody();
619
	}
620
621
	public function setContent($content) {
622
		throw new Exception('IMAP message is immutable');
623
	}
624
625
	/**
626
	 * @return array
627
	 */
628
	public function getAttachments() {
629
		throw new Exception('not implemented');
630
	}
631
632
	/**
633
	 * @param File $file
634
	 */
635
	public function addAttachmentFromFiles(File $file) {
636
		throw new Exception('IMAP message is immutable');
637
	}
638
639
	/**
640
	 * @return IMessage
641
	 */
642
	public function getRepliedMessage() {
643
		throw new Exception('not implemented');
644
	}
645
646
	/**
647
	 * @param IMessage $message
648
	 */
649
	public function setRepliedMessage(IMessage $message) {
650
		throw new Exception('not implemented');
651
	}
652
653
}
654