Completed
Pull Request — master (#1200)
by Christoph
09:35
created

IMAPMessage::hasAttachments()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 17.9449

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 25
ccs 4
cts 13
cp 0.3076
rs 8.439
cc 6
eloc 13
nc 6
nop 1
crap 17.9449
1
<?php
2
3
namespace OCA\Mail\Model;
4
5
/**
6
 * ownCloud - Mail app
7
 *
8
 * @author Thomas Müller
9
 * @copyright 2012, 2013 Thomas Müller [email protected]
10
 *
11
 * @author Christoph Wurst <[email protected]>
12
 * @copyright Christoph Wurst 2015
13
 *
14
 * This library is free software; you can redistribute it and/or
15
 * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
16
 * License as published by the Free Software Foundation; either
17
 * version 3 of the License, or any later version.
18
 *
19
 * This library is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
23
 *
24
 * You should have received a copy of the GNU Lesser General Public
25
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
26
 *
27
 */
28
29
use Closure;
30
use Exception;
31
use Horde_Imap_Client;
32
use Horde_Imap_Client_Data_Fetch;
33
use Horde_Mail_Rfc822_List;
34
use OCP\Files\File;
35
use OCA\Mail\Service\Html;
36
use OCP\AppFramework\Db\DoesNotExistException;
37
use OCP\Util;
38
39
class IMAPMessage implements IMessage {
40
41
	use ConvertAddresses;
42
43
	/**
44
	 * @var string[]
45
	 */
46
	private $attachmentsToIgnore = ['signature.asc', 'smime.p7s'];
47
48
	/** @var string */
49
	private $uid;
50
51
	/**
52
	 * @param \Horde_Imap_Client_Socket|null $conn
53
	 * @param \Horde_Imap_Client_Mailbox $mailBox
54
	 * @param integer $messageId
55
	 * @param \Horde_Imap_Client_Data_Fetch|null $fetch
56
	 * @param boolean $loadHtmlMessage
57
	 * @param Html|null $htmlService
58
	 */
59 9
	public function __construct($conn, $mailBox, $messageId, $fetch=null,
60
		$loadHtmlMessage=false, $htmlService = null) {
61 9
		$this->conn = $conn;
62 9
		$this->mailBox = $mailBox;
63 9
		$this->messageId = $messageId;
64 9
		$this->loadHtmlMessage = $loadHtmlMessage;
65
66 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...
67 9
		if (is_null($htmlService)) {
68 8
			$urlGenerator = \OC::$server->getURLGenerator();
69 8
			$request = \OC::$server->getRequest();
70 8
			$this->htmlService = new Html($urlGenerator, $request);
71 9
		}
72
73 9
		if ($fetch === null) {
74 1
			$this->loadMessageBodies();
75 1
		} else {
76 8
			$this->fetch = $fetch;
77
		}
78 9
	}
79
80
	// output all the following:
81
	public $header = null;
82
	public $htmlMessage = '';
83
	public $plainMessage = '';
84
	public $attachments = [];
85
	private $loadHtmlMessage = false;
86
	private $hasHtmlMessage = false;
87
88
	/**
89
	 * @var \Horde_Imap_Client_Socket
90
	 */
91 7
	private $conn;
92
93
	/**
94
	 * @var \Horde_Imap_Client_Mailbox
95
	 */
96 1
	private $mailBox;
97
	private $messageId;
98
99
	/**
100
	 * @var \Horde_Imap_Client_Data_Fetch
101
	 */
102
	private $fetch;
103
104
	/**
105
	 * @return int
106
	 */
107 7
	public function getUid() {
108 6
		if (!is_null($this->uid)) {
109
			return $this->uid;
110
		}
111 7
		return $this->fetch->getUid();
112 1
	}
113
114 8
	public function setUid($uid) {
115
		$this->uid = $uid;
116 8
		$this->attachments = array_map(function($attachment) use ($uid) {
117
			$attachment['messageId'] = $uid;
118 8
			return $attachment;
119
		}, $this->attachments);
120 1
	}
121
122
	/**
123
	 * @return array
124
	 */
125 6
	public function getFlags() {
126 6
		$flags = $this->fetch->getFlags();
127
		return [
128 6
			'unseen' => !in_array(Horde_Imap_Client::FLAG_SEEN, $flags),
129 6
			'flagged' => in_array(Horde_Imap_Client::FLAG_FLAGGED, $flags),
130 6
			'answered' => in_array(Horde_Imap_Client::FLAG_ANSWERED, $flags),
131 6
			'deleted' => in_array(Horde_Imap_Client::FLAG_DELETED, $flags),
132 6
			'draft' => in_array(Horde_Imap_Client::FLAG_DRAFT, $flags),
133 6
			'forwarded' => in_array(Horde_Imap_Client::FLAG_FORWARDED, $flags),
134 6
			'hasAttachments' => $this->hasAttachments($this->fetch->getStructure())
135 6
		];
136
	}
137
138
	/**
139
	 * @param array $flags
140
	 */
141 7
	public function setFlags(array $flags) {
142
		// TODO: implement
143 7
		throw new Exception('Not implemented');
144
	}
145
146
	/**
147
	 * @return \Horde_Imap_Client_Data_Envelope
148
	 */
149 8
	public function getEnvelope() {
150 8
		return $this->fetch->getEnvelope();
151
	}
152
153
	/**
154
	 * @return string
155
	 */
156 7
	public function getFromEmail() {
157 7
		$e = $this->getEnvelope();
158 7
		$from = $e->from[0];
159 7
		return $from ? $from->bare_address : null;
160
	}
161
162
	/**
163
	 * @return string
164
	 */
165 7
	public function getFrom() {
166 7
		$e = $this->getEnvelope();
167 7
		$from = $e->from[0];
168 7
		return $from ? $from->label : null;
169
	}
170
171
	/**
172
	 * @param string $from
173
	 * @throws Exception
174
	 */
175
	public function setFrom($from) {
176
		throw new Exception('IMAP message is immutable');
177
	}
178
179
	/**
180
	 * @return array
181
	 */
182 6
	public function getFromList() {
183 6
		$e = $this->getEnvelope();
184 6
		return $this->convertAddressList($e->from);
185 1
	}
186
187
	/**
188
	 * @return string
189
	 */
190 6
	public function getToEmail() {
191 6
		$e = $this->getEnvelope();
192 6
		$to = $e->to[0];
193 6
		return $to ? $to->bare_address : null;
194
	}
195
196 6
	public function getTo() {
197 6
		$e = $this->getEnvelope();
198 6
		$to = $e->to[0];
199 6
		return $to ? $to->label : null;
200
	}
201
202
	/**
203
	 * @param Horde_Mail_Rfc822_List $to
204
	 * @throws Exception
205
	 */
206
	public function setTo(Horde_Mail_Rfc822_List $to) {
207
		throw new Exception('IMAP message is immutable');
208
	}
209
210
	/**
211
	 * @return array
212
	 */
213 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...
214 6
		$e = $this->getEnvelope();
215 6
		if ($assoc) {
216 6
			return $this->convertAddressList($e->to);
217
		} else {
218
			return $this->hordeListToStringArray($e->to);
219
		}
220
	}
221
222 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...
223 6
		$e = $this->getEnvelope();
224 6
		if ($assoc) {
225 6
			return $this->convertAddressList($e->cc);
226
		} else {
227
			return $this->hordeListToStringArray($e->cc);
228
		}
229
	}
230
231
	public function setCC(Horde_Mail_Rfc822_List $cc) {
232
		throw new Exception('IMAP message is immutable');
233
	}
234
235 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...
236
		$e = $this->getEnvelope();
237
		if ($assoc) {
238
			return $this->convertAddressList($e->bcc);
239
		} else {
240
			return $this->hordeListToStringArray($e->bcc);
241
		}
242
	}
243
244
	public function setBcc(Horde_Mail_Rfc822_List $bcc) {
245
		throw new Exception('IMAP message is immutable');
246
	}
247
248
	public function getReplyToList() {
249
		$e = $this->getEnvelope();
250
		return $this->convertAddressList($e->from);
251
	}
252
253
	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...
254
		throw new Exception('IMAP message is immutable');
255
	}
256
257
	/**
258
	 * on reply, fill cc with everyone from to and cc except yourself
259
	 *
260
	 * @param string $ownMail
261
	 */
262 1
	public function getReplyCcList($ownMail) {
263 1
		$e = $this->getEnvelope();
264 1
		$list = new \Horde_Mail_Rfc822_List();
265 1
		$list->add($e->to);
266 1
		$list->add($e->cc);
267 1
		$list->unique();
268 1
		$list->remove($ownMail);
269 1
		return $this->convertAddressList($list);
270
	}
271
272
	/**
273
	 * Get the ID if available
274
	 *
275
	 * @return int|null
276
	 */
277
	public function getMessageId() {
278
		$e = $this->getEnvelope();
279
		return $e->message_id;
280
	}
281
282
	/**
283
	 * @return string
284
	 */
285 6
	public function getSubject() {
286 6
		$e = $this->getEnvelope();
287 6
		return $e->subject;
288
	}
289
290
	/**
291
	 * @param string $subject
292
	 * @throws Exception
293
	 */
294
	public function setSubject($subject) {
295
		throw new Exception('IMAP message is immutable');
296
	}
297
298
	/**
299
	 * @return \Horde_Imap_Client_DateTime
300
	 */
301 6
	public function getSentDate() {
302 6
		return $this->fetch->getImapDate();
303
	}
304
305 6
	public function getSize() {
306 6
		return $this->fetch->getSize();
307
	}
308
309
	/**
310
	 * @param \Horde_Mime_Part $part
311
	 * @return bool
312
	 */
313 6
	private function hasAttachments($part) {
314 6
		foreach($part->getParts() as $p) {
315
			/**
316
			 * @var \Horde_Mime_Part $p
317
			 */
318
			$filename = $p->getName();
319
320
			if(!is_null($p->getContentId())) {
321
				continue;
322
			}
323
			if(isset($filename)) {
324
				// do not show technical attachments
325
				if(in_array($filename, $this->attachmentsToIgnore)) {
326
					continue;
327
				} else {
328
					return true;
329
				}
330
			}
331
			if($this->hasAttachments($p)) {
332
				return true;
333
			}
334 6
		}
335
336 6
		return false;
337
	}
338
339 1
	private function loadMessageBodies() {
340 1
		$headers = [];
341
342 1
		$fetch_query = new \Horde_Imap_Client_Fetch_Query();
343 1
		$fetch_query->envelope();
344 1
		$fetch_query->structure();
345 1
		$fetch_query->flags();
346 1
		$fetch_query->size();
347 1
		$fetch_query->imapDate();
348
349 1
		$headers = array_merge($headers, [
350 1
			'importance',
351 1
			'list-post',
352
			'x-priority'
353 1
		]);
354 1
		$headers[] = 'content-type';
355
356 1
		$fetch_query->headers('imp', $headers, [
357 1
			'cache' => true,
358
			'peek'  => true
359 1
		]);
360
361
		// $list is an array of Horde_Imap_Client_Data_Fetch objects.
362 1
		$ids = new \Horde_Imap_Client_Ids($this->messageId);
363 1
		$headers = $this->conn->fetch($this->mailBox, $fetch_query, ['ids' => $ids]);
364
		/** @var $fetch \Horde_Imap_Client_Data_Fetch */
365 1
		$fetch = $headers[$this->messageId];
366 1
		if (is_null($fetch)) {
367
			throw new DoesNotExistException("This email ($this->messageId) can't be found. Probably it was deleted from the server recently. Please reload.");
368
		}
369
370
		// set $this->fetch to get to, from ...
371 1
		$this->fetch = $fetch;
372
373
		// analyse the body part
374 1
		$structure = $fetch->getStructure();
375
376
		// debugging below
377 1
		$structure_type = $structure->getPrimaryType();
378 1
		if ($structure_type == 'multipart') {
379 1
			$i = 1;
380 1
			foreach($structure->getParts() as $p) {
381 1
				$this->getPart($p, $i++);
382 1
			}
383 1
		} else {
384
			if ($structure->findBody() != null) {
385
				// get the body from the server
386
				$partId = $structure->findBody();
387
				$this->getPart($structure->getPart($partId), $partId);
388
			}
389
		}
390 1
	}
391
392
	/**
393
	 * @param $p \Horde_Mime_Part
394
	 * @param $partNo
395
	 */
396 1
	private function getPart($p, $partNo) {
397
		// ATTACHMENT
398
		// Any part with a filename is an attachment,
399
		// so an attached text file (type 0) is not mistaken as the message.
400 1
		$filename = $p->getName();
401 1
		if(isset($filename)) {
402
			if(in_array($filename, $this->attachmentsToIgnore)) {
403
				return;
404
			}
405
			$this->attachments[]= [
406
				'id' => $p->getMimeId(),
407
				'messageId' => $this->messageId,
408
				'fileName' => $filename,
409
				'mime' => $p->getType(),
410
				'size' => $p->getBytes(),
411
				'cid' => $p->getContentId()
412
			];
413
			return;
414
		}
415
416 1
		if ($p->getPrimaryType() === 'multipart') {
417
			$this->handleMultiPartMessage($p, $partNo);
418
			return;
419
		}
420
421 1
		if ($p->getType() === 'text/plain') {
422 1
			$this->handleTextMessage($p, $partNo);
423 1
			return;
424
		}
425
426
		// TEXT
427 1
		if ($p->getType() === 'text/calendar') {
428
			// TODO: skip inline ics for now
429
			return;
430
		}
431
432 1
		if ($p->getType() === 'text/html') {
433 1
			$this->handleHtmlMessage($p, $partNo);
434 1
			return;
435
		}
436
437
		// EMBEDDED MESSAGE
438
		// Many bounce notifications embed the original message as type 2,
439
		// but AOL uses type 1 (multipart), which is not handled here.
440
		// There are no PHP functions to parse embedded messages,
441
		// so this just appends the raw source to the main message.
442
		if ($p[0]=='message') {
443
			$data = $this->loadBodyData($p, $partNo);
444
			$this->plainMessage .= trim($data) ."\n\n";
445
		}
446
	}
447
448
	/**
449
	 * @param string $ownMail
450
	 * @param string $specialRole
451
	 */
452
	public function getFullMessage($ownMail, $specialRole=null) {
453
		$mailBody = $this->plainMessage;
454
455
		$data = $this->getListArray();
456
		if ($this->hasHtmlMessage) {
457
			$data['hasHtmlBody'] = true;
458
		} else {
459
			$mailBody = $this->htmlService->convertLinks($mailBody);
460
			list($mailBody, $signature) = $this->htmlService->parseMailBody($mailBody);
461
			$data['body'] = $specialRole === 'drafts' ? $mailBody : nl2br($mailBody);
462
			$data['signature'] = $signature;
463
		}
464
465
		if (count($this->attachments) === 1) {
466
			$data['attachment'] = $this->attachments[0];
467
		}
468
		if (count($this->attachments) > 1) {
469
			$data['attachments'] = $this->attachments;
470
		}
471
472
		if ($specialRole === 'sent') {
473
			$data['replyToList'] = $this->getToList(true);
474
			$data['replyCcList'] = $this->getCCList(true);
475
		} else {
476
			$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...
477
			$data['replyCcList'] = $this->getReplyCcList($ownMail);
478
		}
479
		return $data;
480
	}
481
482 7
	public function getListArray() {
483 6
		$data = [];
484 6
		$data['id'] = $this->getUid();
485 6
		$data['from'] = $this->getFrom();
486 6
		$data['fromEmail'] = $this->getFromEmail();
487 6
		$data['fromList'] = $this->getFromList();
488 6
		$data['to'] = $this->getTo();
489 6
		$data['toEmail'] = $this->getToEmail();
490 6
		$data['toList'] = $this->getToList(true);
491 6
		$data['subject'] = $this->getSubject();
492 6
		$data['date'] = Util::formatDate($this->getSentDate()->format('U'));
493 6
		$data['size'] = Util::humanFileSize($this->getSize());
494 6
		$data['flags'] = $this->getFlags();
495 6
		$data['dateInt'] = $this->getSentDate()->getTimestamp();
496 7
		$data['dateIso'] = $this->getSentDate()->format('c');
497 6
		$data['ccList'] = $this->getCCList(true);
498 6
		return $data;
499
	}
500
501
	/**
502
	 * @param int     $accountId
503
	 * @param string  $folderId
504
	 * @param int     $messageId
505
	 * @param Closure $attachments
506
	 * @return string
507
	 */
508 1
	public function getHtmlBody($accountId, $folderId, $messageId, Closure $attachments) {
509 1
		return $this->htmlService->sanitizeHtmlMailBody($this->htmlMessage, [
510 1
			'accountId' => $accountId,
511 1
			'folderId' => $folderId,
512 1
			'messageId' => $messageId,
513 1
		], $attachments);
514
	}
515
516
	/**
517
	 * @return string
518
	 */
519 1
	public function getPlainBody() {
520 1
		return $this->plainMessage;
521
	}
522
523
	/**
524
	 * @param \Horde_Mime_Part $part
525
	 * @param int $partNo
526
	 */
527 1
	private function handleMultiPartMessage($part, $partNo) {
528
		$i = 1;
529 1
		foreach ($part->getParts() as $p) {
530
			$this->getPart($p, "$partNo.$i");
531
			$i++;
532
		}
533
	}
534
535
	/**
536
	 * @param \Horde_Mime_Part $p
537
	 * @param int $partNo
538
	 */
539 1
	private function handleTextMessage($p, $partNo) {
540 1
		$data = $this->loadBodyData($p, $partNo);
541 1
		$data = Util::sanitizeHTML($data);
542 1
		$this->plainMessage .= trim($data) ."\n\n";
543 1
	}
544
545
	/**
546
	 * @param \Horde_Mime_Part $p
547
	 * @param int $partNo
548
	 */
549 1
	private function handleHtmlMessage($p, $partNo) {
550 1
		$this->hasHtmlMessage = true;
551 1
		if ($this->loadHtmlMessage) {
552 1
			$data = $this->loadBodyData($p, $partNo);
553 1
			$this->htmlMessage .= $data . "<br><br>";
554 1
		}
555 1
	}
556
557
	/**
558
	 * @param \Horde_Mime_Part $p
559
	 * @param int $partNo
560
	 * @return string
561
	 * @throws DoesNotExistException
562
	 * @throws \Exception
563
	 */
564 1
	private function loadBodyData($p, $partNo) {
565
		// DECODE DATA
566 1
		$fetch_query = new \Horde_Imap_Client_Fetch_Query();
567 1
		$ids = new \Horde_Imap_Client_Ids($this->messageId);
568
569 1
		$fetch_query->bodyPart($partNo, [
570
		    'peek' => true
571 1
		]);
572 1
		$fetch_query->bodyPartSize($partNo);
573 1
		$fetch_query->mimeHeader($partNo, [
574
		    'peek' => true
575 1
		]);
576
577 1
		$headers = $this->conn->fetch($this->mailBox, $fetch_query, ['ids' => $ids]);
578
		/** @var $fetch \Horde_Imap_Client_Data_Fetch */
579 1
		$fetch = $headers[$this->messageId];
580 1
		if (is_null($fetch)) {
581
			throw new DoesNotExistException("Mail body for this mail($this->messageId) could not be loaded");
582
		}
583
584 1
		$mimeHeaders = $fetch->getMimeHeader($partNo, Horde_Imap_Client_Data_Fetch::HEADER_PARSE);
585 1
		if ($enc = $mimeHeaders->getValue('content-transfer-encoding')) {
586
			$p->setTransferEncoding($enc);
587
		}
588
589 1
		$data = $fetch->getBodyPart($partNo);
590
591 1
		$p->setContents($data);
592 1
		$data = $p->getContents();
593
594 1
		$data = iconv($p->getCharset(), 'utf-8//IGNORE', $data);
595 1
		return $data;
596
	}
597
598
	public function getContent() {
599
		return $this->getPlainBody();
600
	}
601
602
	public function setContent($content) {
603
		throw new Exception('IMAP message is immutable');
604
	}
605
606
	/**
607
	 * @return array
608
	 */
609
	public function getAttachments() {
610
		throw new Exception('not implemented');
611
	}
612
613
	/**
614
	 * @param File $file
615
	 */
616
	public function addAttachmentFromFiles(File $file) {
617
		throw new Exception('IMAP message is immutable');
618
	}
619
620
	/**
621
	 * @return IMessage
622
	 */
623
	public function getRepliedMessage() {
624
		throw new Exception('not implemented');
625
	}
626
627
	/**
628
	 * @param IMessage $message
629
	 */
630
	public function setRepliedMessage(IMessage $message) {
631
		throw new Exception('not implemented');
632
	}
633
634
}
635