Passed
Push — master ( c8f911...aaeb5b )
by Maxence
01:39
created

MailService::parseMailAddress()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 14
rs 10
cc 3
nc 3
nop 1
1
<?php declare(strict_types=1);
2
3
4
/**
5
 * Files_FromMail - Recover your email attachments from your cloud.
6
 *
7
 * This file is licensed under the Affero General Public License version 3 or
8
 * later. See the COPYING file.
9
 *
10
 * @author Maxence Lange <[email protected]>
11
 * @copyright 2017
12
 * @license GNU AGPL version 3 or any later version
13
 *
14
 * This program is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License as
16
 * published by the Free Software Foundation, either version 3 of the
17
 * License, or (at your option) any later version.
18
 *
19
 * This program 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 Affero General Public License
25
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
26
 *
27
 */
28
29
30
namespace OCA\Files_FromMail\Service;
31
32
33
use Exception;
34
use OC;
35
use OCA\Files_FromMail\Exceptions\AddressAlreadyExistException;
36
use OCA\Files_FromMail\Exceptions\AddressInfoException;
37
use OCA\Files_FromMail\Exceptions\InvalidAddressException;
38
use OCA\Files_FromMail\Exceptions\NotAFolderException;
39
use OCA\Files_FromMail\Exceptions\UnknownAddressException;
40
use OCP\Files\FileInfo;
0 ignored issues
show
Bug introduced by
The type OCP\Files\FileInfo was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
41
use OCP\Files\Folder;
0 ignored issues
show
Bug introduced by
The type OCP\Files\Folder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
42
use OCP\Files\GenericFileException;
0 ignored issues
show
Bug introduced by
The type OCP\Files\GenericFileException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
43
use OCP\Files\NotFoundException;
0 ignored issues
show
Bug introduced by
The type OCP\Files\NotFoundException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
44
use OCP\Files\NotPermittedException;
0 ignored issues
show
Bug introduced by
The type OCP\Files\NotPermittedException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
45
use OCP\Lock\LockedException;
0 ignored issues
show
Bug introduced by
The type OCP\Lock\LockedException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
46
use PhpMimeMailParser\Attachment;
47
use PhpMimeMailParser\Parser;
48
49
50
/**
51
 * Class MailService
52
 *
53
 * @package OCA\Files_FromMail\Service
54
 */
55
class MailService {
56
57
58
	/** @var ConfigService */
59
	private $configService;
60
61
	/** @var MiscService */
62
	private $miscService;
63
64
	/** @var int */
65
	private $count = 0;
66
67
68
	/**
69
	 * MailService constructor.
70
	 *
71
	 * @param ConfigService $configService
72
	 * @param MiscService $miscService
73
	 */
74
	function __construct(ConfigService $configService, MiscService $miscService) {
75
		$this->configService = $configService;
76
		$this->miscService = $miscService;
77
	}
78
79
80
	/**
81
	 * parse the mail content.
82
	 *
83
	 * will create a local text file containing the headers and the content of the mail for each one of
84
	 * the 'to' or 'cc' mail address correspond to a mail added using the
85
	 * "./occ files_frommail:address --add"
86
	 *
87
	 * Attachments will also be saved on the cloud in the path:
88
	 * "Mails sent to [email protected]/From [email protected]/"
89
	 *
90
	 * @param string $content
91
	 * @param string $userId
92
	 */
93
	public function parseMail(string $content, string $userId): void {
94
		$mail = new Parser();
95
		$mail->setText($content);
96
97
		$data = $this->parseMailHeaders($mail);
98
		$data['id'] = date($this->configService->getAppValue(ConfigService::FROMMAIL_FILENAMEID));
99
		$data['userId'] = $userId;
100
101
		$done = [];
102
		$toAddresses = array_merge($mail->getAddresses('to'), $mail->getAddresses('cc'));
103
		foreach ($toAddresses as $toAddress) {
104
			$to = $toAddress['address'];
105
			if (in_array($to, $done)) {
106
				continue;
107
			}
108
109
			try {
110
				$this->generateLocalContentFromMail($mail, $to, $data);
111
			} catch (Exception $e) {
112
				$this->miscService->log('could not generate LocalContent from Mail - ' . $e->getMessage());
113
			}
114
115
			$done[] = $to;
116
		}
117
	}
118
119
120
	/**
121
	 * @param Parser $mail
122
	 * @param string $to
123
	 * @param array $data
124
	 *
125
	 * @throws AddressInfoException
126
	 * @throws GenericFileException
127
	 * @throws NotAFolderException
128
	 * @throws NotFoundException
129
	 * @throws NotPermittedException
130
	 * @throws LockedException
131
	 */
132
	private function generateLocalContentFromMail(Parser $mail, string $to, array $data): void {
133
		$toInfo = $this->getMailAddressInfo($to);
134
		$this->miscService->log($to . ' ' . json_encode($toInfo));
135
		if (empty($toInfo)) {
136
			return;
137
		}
138
139
		$text = $data['text'];
140
		$subject = $data['subject'];
141
		$from = $data['from'];
142
		$userId = $data['userId'];
143
		$id = $data['id'];
144
145
		$this->verifyInfoAndPassword($text, $toInfo);
146
147
		$this->count = 0;
148
		$folder = $this->getMailFolder($userId, $to, $from);
149
		$this->createLocalFile($folder, $id, 'mail-' . $subject . '.txt', $text);
150
		$this->createLocalFileFromAttachments($id, $folder, $mail->getAttachments());
151
	}
152
153
154
	/**
155
	 * @param string $content
156
	 * @param array $toInfo
157
	 *
158
	 * @throws AddressInfoException
159
	 */
160
	private function verifyInfoAndPassword(string $content, array $toInfo): void {
161
		if ($toInfo === null) {
0 ignored issues
show
introduced by
The condition $toInfo === null is always false.
Loading history...
162
			throw new AddressInfoException('address is not known');
163
		}
164
165
		if (!array_key_exists('password', $toInfo) || $toInfo['password'] === '') {
166
			return;
167
		}
168
169
		if (strpos($content, ':' . $toInfo['password']) !== false) {
170
			return;
171
		}
172
173
		throw new AddressInfoException('password is set but not used in mail');
174
	}
175
176
177
	/**
178
	 * @param string $userId
179
	 * @param string $to
180
	 * @param string $from
181
	 *
182
	 * @return Folder
183
	 * @throws NotAFolderException
184
	 * @throws NotFoundException
185
	 * @throws NotPermittedException
186
	 */
187
	private function getMailFolder(string $userId, string $to, string $from): Folder {
188
		$node = OC::$server->getUserFolder($userId);
189
		$to = $this->parseMailAddress($to);
190
		$from = $this->parseMailAddress($from);
191
192
		$folderPath = 'Mails sent to ' . $to . '/From ' . $from . '/';
193
194
		if (!$node->nodeExists($folderPath)) {
195
			$node->newFolder($folderPath);
196
		}
197
198
		$folder = $node->get($folderPath);
199
		if ($folder->getType() !== FileInfo::TYPE_FOLDER) {
200
			throw new NotAFolderException($folderPath . ' is not a folder');
201
		}
202
203
		/** @var Folder $folder */
204
		return $folder;
205
	}
206
207
208
	/**
209
	 * @param Parser $mail
210
	 *
211
	 * @return array
212
	 */
213
	private function parseMailHeaders(Parser $mail): array {
214
		$from = $mail->getAddresses('from')[0]['address'];
215
		$subject = $mail->getHeader('subject');
216
		$text = $mail->getHeadersRaw() . $mail->getMessageBody('text');
217
218
		//TODO: check that data are enough
219
		return [
220
			'from'    => $from,
221
			'subject' => $subject,
222
			'text'    => $text
223
		];
224
	}
225
226
227
	/**
228
	 * @param string $id
229
	 * @param Folder $folder
230
	 * @param Attachment[] $attachments
231
	 *
232
	 * @throws GenericFileException
233
	 * @throws NotPermittedException
234
	 * @throws LockedException
235
	 */
236
	private function createLocalFileFromAttachments(string $id, Folder $folder, array $attachments): void {
237
		foreach ($attachments as $attachment) {
238
			$this->createLocalFile(
239
				$folder, $id, 'attachment-' . $attachment->getFilename(),
240
				$attachment->getContent()
241
			);
242
		}
243
	}
244
245
246
	/**
247
	 * @param Folder $folder
248
	 * @param string $id
249
	 * @param string $filename
250
	 * @param string $content
251
	 *
252
	 * @throws NotPermittedException
253
	 * @throws GenericFileException
254
	 * @throws LockedException
255
	 */
256
	private function createLocalFile(Folder $folder, string $id, string $filename, string $content): void {
257
		$new = $folder->newFile($id . '-' . $this->count . '_' . $filename);
258
		$new->putContent($content);
259
260
		$this->count++;
261
	}
262
263
264
	/**
265
	 * @param string $address
266
	 * @param string $password
267
	 *
268
	 * @throws UnknownAddressException
269
	 */
270
	public function setMailPassword(string $address, string $password): void {
271
		if (!$this->mailAddressExist($address)) {
272
			throw new UnknownAddressException('address is not known');
273
		}
274
275
		$addresses = $this->getMailAddresses();
276
		$new = [];
277
		foreach ($addresses as $entry) {
278
			if ($entry['address'] === $address) {
279
				$entry['password'] = $password;
280
			}
281
			$new[] = $entry;
282
		}
283
284
		$this->saveMailAddresses($new);
285
	}
286
287
288
	/**
289
	 * @param string $address
290
	 *
291
	 * @throws UnknownAddressException
292
	 */
293
	public function removeMailAddress(string $address): void {
294
		$addresses = $this->getMailAddresses();
295
		if (!$this->mailAddressExist($address)) {
296
			throw new UnknownAddressException('address is not known');
297
		}
298
299
		$new = [];
300
		foreach ($addresses as $entry) {
301
			if ($entry['address'] !== $address) {
302
				$new[] = $entry;
303
			}
304
		}
305
306
		$this->saveMailAddresses($new);
307
	}
308
309
310
	/**
311
	 * @param string $address
312
	 * @param string $password
313
	 *
314
	 * @throws AddressAlreadyExistException
315
	 * @throws InvalidAddressException
316
	 */
317
	public function addMailAddress(string $address, string $password = ''): void {
318
		$this->hasToBeAValidMailAddress($address);
319
		if ($this->mailAddressExist($address)) {
320
			throw new AddressAlreadyExistException('address already exist');
321
		}
322
323
		$addresses = $this->getMailAddresses();
324
		array_push($addresses, ['address' => $address, 'password' => $password]);
325
		$this->saveMailAddresses($addresses);
326
	}
327
328
329
	/**
330
	 * @param $address
331
	 *
332
	 * @return bool
333
	 */
334
	private function mailAddressExist(string $address): bool {
335
		return !empty($this->getMailAddressInfo($address));
336
	}
337
338
339
	/**
340
	 * @param string $address
341
	 *
342
	 * @return array
343
	 */
344
	private function getMailAddressInfo(string $address): array {
345
		$addresses = $this->getMailAddresses();
346
		foreach ($addresses as $entry) {
347
			if ($entry['address'] === $address) {
348
				return $entry;
349
			}
350
		}
351
352
		return [];
353
	}
354
355
356
	/**
357
	 * @param string $address
358
	 *
359
	 * @throws InvalidAddressException
360
	 */
361
	private function hasToBeAValidMailAddress(string $address): void {
362
		if (filter_var($address, FILTER_VALIDATE_EMAIL)) {
363
			return;
364
		}
365
366
		throw new InvalidAddressException('this mail address is not valid');
367
	}
368
369
370
	/**
371
	 * @return array
372
	 */
373
	public function getMailAddresses(): array {
374
		$curr = json_decode($this->configService->getAppValue(ConfigService::FROMMAIL_ADDRESSES), true);
375
		if ($curr === null) {
376
			return [];
377
		}
378
379
		return $curr;
380
	}
381
382
383
	/**
384
	 * @param array $addresses
385
	 */
386
	private function saveMailAddresses(array $addresses): void {
387
		$this->configService->setAppValue(ConfigService::FROMMAIL_ADDRESSES, json_encode($addresses));
388
	}
389
390
391
	/**
392
	 * @param string $address
393
	 *
394
	 * @return string
395
	 */
396
	private function parseMailAddress(string $address): string {
397
		$acceptedChars = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789@.-_+';
398
399
		$fixed = '';
400
		for ($i = 0; $i < strlen($address); $i++) {
401
			$c = $address[$i];
402
			if (strpos($acceptedChars, $c) !== false) {
403
				$fixed .= $c;
404
			}
405
		}
406
407
		$fixed = str_replace('..', '.', $fixed);
408
409
		return $fixed;
410
	}
411
412
}
413
414