Completed
Pull Request — master (#1272)
by Christoph
02:47
created

IMAPMessage::setCC()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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