Completed
Pull Request — master (#1277)
by Christoph
42:07
created

IMAPMessage::setUid()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1.0787

Importance

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