Passed
Push — master ( 367bbb...b6e19f )
by
unknown
07:38
created

KendoxModule::execute()   B

Complexity

Conditions 6
Paths 11

Size

Total Lines 25
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 21
c 1
b 0
f 0
nc 11
nop 0
dl 0
loc 25
rs 8.9617
1
<?php
2
3
use Kendox\Client;
4
5
require_once __DIR__ . '/vendor/autoload.php';
6
require_once __DIR__ . "/kendox-client/class.kendox-client.php";
7
require_once __DIR__ . "/class.attachment-info.php";
8
require_once __DIR__ . "/class.uploadfile.php";
9
require_once __DIR__ . '/../config.php';
10
11
class KendoxModule extends Module {
12
	// Certificate paths/passwords for prod and test environments
13
	public $pfxFile = '';
14
	public $pfxPw = '';
15
	public $pfxFileTest = '';
16
	public $pfxPwTest = '';
17
18
	public $dialogUrl;
19
	public $mapiMessage;
20
21
	public $kendoxClient;
22
23
	/**
24
	 * @constructor
25
	 *
26
	 * @param mixed $id
27
	 * @param mixed $data
28
	 */
29
	public function __construct($id, $data) {
30
		parent::__construct($id, $data);
31
		$this->store = $GLOBALS['mapisession']->getDefaultMessageStore();
0 ignored issues
show
Bug Best Practice introduced by
The property store does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
32
		$this->pfxFile = $this->resolvePfxPath($this->getConfigValue('PLUGIN_KENDOX_PFX_FILE'));
33
		$this->pfxPw = $this->getConfigValue('PLUGIN_KENDOX_PFX_PASSWORD');
34
		$this->pfxFileTest = $this->resolvePfxPath($this->getConfigValue('PLUGIN_KENDOX_PFX_FILE_TEST'));
35
		$this->pfxPwTest = $this->getConfigValue('PLUGIN_KENDOX_PFX_PASSWORD_TEST');
36
37
		if ($this->pfxFileTest === '') {
38
			$this->pfxFileTest = $this->pfxFile;
39
		}
40
		if ($this->pfxPwTest === '') {
41
			$this->pfxPwTest = $this->pfxPw;
42
		}
43
	}
44
45
	/**
46
	 * Executes all the actions in the $data variable.
47
	 * Exception part is used for authentication errors also.
48
	 *
49
	 * @return bool true on success or false on failure
50
	 */
51
	#[Override]
52
	public function execute() {
53
		foreach ($this->data as $actionType => $actionData) {
54
			if (isset($actionType)) {
55
				try {
56
					switch ($actionType) {
57
						case "attachmentinfo":
58
							$response = $this->getAttachmentInfo($actionData["storeId"], $actionData["mailEntryId"]);
59
							$this->addActionData($actionType, $response);
60
							$GLOBALS['bus']->addData($this->getResponseData());
61
							break;
62
63
						case "upload":
64
							$response = $this->upload($actionData["storeId"], $actionData["mailEntryId"], $actionData["uploadType"], $actionData["selectedAttachments"], $actionData["environment"], $actionData["apiUrl"], $actionData["userEMail"]);
65
							$this->addActionData($actionType, $response);
66
							$GLOBALS['bus']->addData($this->getResponseData());
67
							break;
68
					}
69
				}
70
				catch (Exception $e) {
71
					$response = [];
72
					$response["Successful"] = false;
73
					$response["errorMessage"] = $e->getMessage();
74
					$this->addActionData($actionType, $response);
75
					$GLOBALS['bus']->addData($this->getResponseData());
76
				}
77
			}
78
		}
79
	}
80
81
	/**
82
	 * Get attachment information (meta data) of mail.
83
	 *
84
	 * @param mixed $storeId
85
	 * @param mixed $mailEntryId
86
	 */
87
	private function getAttachmentInfo($storeId, $mailEntryId) {
88
		$this->loadMapiMessage($storeId, $mailEntryId);
89
		$items = [];
90
		$attachmentTable = mapi_message_getattachmenttable($this->mapiMessage);
91
		$messageAttachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME]);
92
		foreach ($messageAttachments as $att) {
93
			$item = new AttachmentInfo();
94
			$item->id = $att[PR_ATTACH_NUM];
95
			$item->name = $att[PR_ATTACH_LONG_FILENAME];
96
			$item->size = $att[PR_ATTACH_SIZE];
97
			$items[] = $item;
98
		}
99
		$response = [];
100
		$response["Successful"] = true;
101
		$response["attachments"] = $items;
102
103
		return $response;
104
	}
105
106
	/**
107
	 * Upload of e-mail to InfoShare.
108
	 *
109
	 * @param string $mailEntryId         Mail Entry ID
110
	 * @param mixed  $storeId
111
	 * @param mixed  $uploadType
112
	 * @param mixed  $selectedAttachments
113
	 * @param mixed  $environment
114
	 * @param mixed  $apiUrl
115
	 * @param mixed  $userEMail
116
	 *
117
	 * @return object
118
	 */
119
	private function upload($storeId, $mailEntryId, $uploadType, $selectedAttachments, $environment, $apiUrl, $userEMail) {
120
		$emlFile = null;
121
		$this->loadMapiMessage($storeId, $mailEntryId);
122
		// Write temporary message file (.EML)
123
		// Send to Kendox InfoShare
124
		$uploadFiles = [];
125
		if ($uploadType == "fullEmail") {
126
			$emlFile = $this->createTempEmlFileFromMapiMessage($mailEntryId);
127
			if (!file_exists($emlFile)) {
128
				throw new Exception("EML file " . $emlFile . " not available.");
129
			}
130
			$file = new UploadFile();
131
			$file->tempFile = $emlFile;
132
			$file->fileType = "email";
133
			$file->fileName = "email.eml";
134
			$file->fileLength = filesize($emlFile);
0 ignored issues
show
Bug introduced by
The property fileLength does not seem to exist on UploadFile.
Loading history...
135
			$file->kendoxFileId = null;
136
			$uploadFiles[] = $file;
137
		}
138
		if ($uploadType == "attachmentsOnly") {
139
			$uploadFiles = $this->getUploadFilesFromSelectedAttachments($selectedAttachments);
140
		}
141
142
		try {
143
			$this->sendFiles($environment, $uploadFiles, $apiUrl, $userEMail);
144
		}
145
		catch (Exception $ex) {
146
			throw $ex;
147
		}
148
		finally {
149
			if ($emlFile != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $emlFile of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
150
				@unlink($emlFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

150
				/** @scrutinizer ignore-unhandled */ @unlink($emlFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
151
			}
152
			foreach ($uploadFiles as $uploadFile) {
153
				@unlink($uploadFile->tempFile);
154
			}
155
		}
156
157
		try {
158
			// Return response
159
			$response = [];
160
			$response["Successful"] = true;
161
			$response["apiUrl"] = $apiUrl;
162
			$response["userEMail"] = $userEMail;
163
			$response["messageId"] = $mailEntryId;
164
			$response["kendoxConnectionId"] = $this->kendoxClient->ConnectionId;
165
			$response["kendoxFiles"] = $uploadFiles;
166
		}
167
		catch (Exception $ex) {
168
			if ($emlFile != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $emlFile of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
169
				@unlink($emlFile);
170
			}
171
			if ($uploadFiles != null) {
172
				foreach ($uploadFiles as $uploadFile) {
173
					@unlink($uploadFile->tempFile);
174
				}
175
			}
176
			$this->logErrorAndThrow("Error on building response message", $ex);
177
		}
178
179
		return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type array which is incompatible with the documented return type object.
Loading history...
180
	}
181
182
	private function loadMapiMessage($storeId, $mailEntryId) {
183
		try {
184
			// Read message store
185
			$store = $GLOBALS['mapisession']->openMessageStore(hex2bin((string) $storeId));
186
		}
187
		catch (Exception $ex) {
188
			$this->logErrorAndThrow("Error on open MAPI store", $ex);
189
		}
190
191
		try {
192
			$this->mapiMessage = mapi_msgstore_openentry($store, hex2bin((string) $mailEntryId));
193
		}
194
		catch (Exception $ex) {
195
			$this->logErrorAndThrow("Error on open MAPI message", $ex);
196
		}
197
	}
198
199
	private function createTempEmlFileFromMapiMessage($mailEntryId) {
200
		// Read message properties
201
		try {
202
			$messageProps = mapi_getprops($this->mapiMessage, [PR_SUBJECT, PR_MESSAGE_CLASS]);
203
		}
204
		catch (Exception $ex) {
205
			$this->logErrorAndThrow("Error on getting MAPI message properties", $ex);
206
		}
207
208
		// Get EML-Stream
209
		try {
210
			$fileName = $this->sanitizeValue($mailEntryId, '', ID_REGEX) . '.eml';
0 ignored issues
show
Unused Code introduced by
The assignment to $fileName is dead and can be removed.
Loading history...
211
			$stream = $this->getEmlStream($messageProps);
212
			$stat = mapi_stream_stat($stream);
213
		}
214
		catch (Exception $ex) {
215
			$this->logErrorAndThrow("Error on reading EML stream from MAPI Message", $ex);
216
		}
217
218
		// Create temporary file
219
		try {
220
			// Set the file length
221
			$fileLength = $stat['cb'];
222
			$tempFile = $this->createTempFilename();
223
			// Read stream for whole message
224
			for ($i = 0; $i < $fileLength; $i += BLOCK_SIZE) {
225
				$appendData = mapi_stream_read($stream, BLOCK_SIZE);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $stream does not seem to be defined for all execution paths leading up to this point.
Loading history...
226
				file_put_contents($tempFile, $appendData, FILE_APPEND);
227
			}
228
229
			return $tempFile;
230
		}
231
		catch (Exception $ex) {
232
			$this->logErrorAndThrow("Error on writing temporary EML file", $ex);
233
		}
234
	}
235
236
	private function createTempFilename() {
237
		$tempPath = TMP_PATH;
238
239
		return $tempPath . $this->getGUID() . ".tmp";
240
	}
241
242
	public function getGUID() {
243
		if (function_exists('com_create_guid')) {
244
			return com_create_guid();
245
		}
246
		mt_srand((float) microtime() * 10000); // optional for php 4.2.0 and up.
0 ignored issues
show
Bug introduced by
(double)microtime() * 10000 of type double is incompatible with the type integer expected by parameter $seed of mt_srand(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

246
		mt_srand(/** @scrutinizer ignore-type */ (float) microtime() * 10000); // optional for php 4.2.0 and up.
Loading history...
247
		$charid = strtoupper(md5(uniqid(random_int(0, mt_getrandmax()), true)));
248
		$hyphen = chr(45); // "-"
249
250
		return chr(123) . // "{"
251
			substr($charid, 0, 8) . $hyphen .
252
			substr($charid, 8, 4) . $hyphen .
253
			substr($charid, 12, 4) . $hyphen .
254
			substr($charid, 16, 4) . $hyphen .
255
			substr($charid, 20, 12) .
256
			chr(125); // "}"
257
	}
258
259
	private function getUploadFilesFromSelectedAttachments($selectedAttachments) {
260
		try {
261
			$uploadFiles = [];
262
			$attachmentTable = mapi_message_getattachmenttable($this->mapiMessage);
263
			$messageAttachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME]);
264
			foreach ($selectedAttachments as $att) {
265
				$tmpFile = $this->createTempFilename();
266
267
				try {
268
					$this->saveAttachmentToTempFile($tmpFile, $att["attachmentNumber"]);
269
					$file = new UploadFile();
270
					$file->tempFile = $tmpFile;
271
					$file->fileType = "attachment";
272
					$file->fileName = $messageAttachments[$att["attachmentNumber"]][PR_ATTACH_LONG_FILENAME];
273
					$file->fileLength = filesize($tmpFile);
0 ignored issues
show
Bug introduced by
The property fileLength does not seem to exist on UploadFile.
Loading history...
274
					$file->kendoxFileId = null;
275
					$uploadFiles[] = $file;
276
				}
277
				catch (Exception $exFile) {
278
					@unlink($tmpFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

278
					/** @scrutinizer ignore-unhandled */ @unlink($tmpFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
279
280
					throw $exFile;
281
				}
282
			}
283
			if (count($uploadFiles) == 0) {
284
				throw new Exception("No attachments selected.");
285
			}
286
287
			return $uploadFiles;
288
		}
289
		catch (Exception $ex) {
290
			$this->logErrorAndThrow("Error on creating upload files from attachment.", $ex);
291
		}
292
	}
293
294
	/**
295
	 * Sending file to Kendox InfoShare.
296
	 *
297
	 * @param UploadFile[] $uploadFiles Files to upload
298
	 * @param mixed        $environment
299
	 * @param mixed        $apiUrl
300
	 * @param mixed        $userEMail
301
	 */
302
	private function sendFiles($environment, $uploadFiles, $apiUrl, $userEMail) {
303
		try {
304
			$targetEnvironment = strtolower((string) $environment) === "prod" ? "prod" : "test";
305
			$pfx = $targetEnvironment === "prod" ? $this->pfxFile : $this->pfxFileTest;
306
			$pfxPw = $targetEnvironment === "prod" ? $this->pfxPw : $this->pfxPwTest;
307
			if ($pfx === '') {
308
				throw new Exception(_("Kendox certificate path is not configured."));
309
			}
310
			if ($pfxPw === '') {
311
				throw new Exception(_("Kendox certificate password is not configured."));
312
			}
313
			if (!file_exists($pfx)) {
314
				throw new Exception(_("Kendox certificate is not available."));
315
			}
316
			if (!is_readable($pfx)) {
317
				throw new Exception(_("Kendox certificate is not readable."));
318
			}
319
			$this->kendoxClient = new Client($apiUrl);
320
			$uid = $this->kendoxClient->loginWithToken($pfx, $pfxPw, "svc_grommunio");
0 ignored issues
show
Unused Code introduced by
The assignment to $uid is dead and can be removed.
Loading history...
321
			$query = [
322
				[
323
					"ColumnName" => "email",
324
					"RelationalOperator" => "Equals",
325
					"Value" => $userEMail],
326
			];
327
			$result = $this->kendoxClient->userTableQuery("grommunio", $query, false);
328
			if (count($result) == 0) {
329
				throw new Exception("User with e-mail address " . $userEMail . " not found in Kendox user table grommunio.");
330
			}
331
			$uid = $result[0][1];
332
			$this->kendoxClient->logout();
333
			// Login and upload
334
			$this->kendoxClient->loginWithToken($pfx, $pfxPw, $uid);
335
			foreach ($uploadFiles as $uploadFile) {
336
				try {
337
					$uploadFile->kendoxFileId = $this->kendoxClient->uploadFile($uploadFile->tempFile);
338
				}
339
				catch (Exception $exUpload) {
340
					$this->logErrorAndThrow("Upload of file " . $uploadFile->fileName . " failed.", $exUpload);
341
				}
342
			}
343
		}
344
		catch (Exception $ex) {
345
			$this->logErrorAndThrow("Sending files failed", $ex);
346
		}
347
	}
348
349
	private function getConfigValue($constantName) {
350
		if (!defined($constantName)) {
351
			return '';
352
		}
353
354
		$value = constant($constantName);
355
356
		return is_string($value) ? trim($value) : '';
357
	}
358
359
	private function resolvePfxPath($path) {
360
		if ($path === '') {
361
			return '';
362
		}
363
364
		if ($path[0] === '/' || preg_match('/^[A-Za-z]:[\\\\\\/]/', $path) === 1) {
365
			return $path;
366
		}
367
368
		return dirname(__DIR__) . '/' . ltrim(str_replace('\\', '/', $path), '/');
369
	}
370
371
	/**
372
	 * Logging an error and throw the exception.
373
	 *
374
	 * @param string    $displayMessage
375
	 * @param Exception $ex
376
	 */
377
	public function logErrorAndThrow($displayMessage, $ex): never {
378
		$errMsg = $displayMessage . ": " . $ex->getMessage();
379
		error_log($errMsg);
380
		error_log($ex->getTraceAsString());
381
382
		throw new Exception($errMsg);
383
	}
384
385
	/**
386
	 * Function will obtain stream from the message, For email messages it will open email as
387
	 * inet object and get the stream content as eml format.
388
	 *
389
	 * @param array $messageProps properties of this particular message
390
	 *
391
	 * @return Stream $stream the eml stream obtained from message
0 ignored issues
show
Bug introduced by
The type Stream 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...
392
	 */
393
	public function getEmlStream($messageProps) {
394
		$addrBook = $GLOBALS['mapisession']->getAddressbook();
395
396
		return mapi_inetmapi_imtoinet($GLOBALS['mapisession']->getSession(), $addrBook, $this->mapiMessage, []);
397
	}
398
399
	/**
400
	 * Function to get binary content of an attachment
401
	 * PR_ATTACH_DATA_BIN.
402
	 *
403
	 * @param string $tempFile         Path and file of temporary attachment file
404
	 * @param int    $attachmentNumber Number of attachment
405
	 *
406
	 * @return string
407
	 */
408
	public function saveAttachmentToTempFile($tempFile, $attachmentNumber) {
409
		$attach = mapi_message_openattach($this->mapiMessage, $attachmentNumber);
410
		file_put_contents($tempFile, mapi_attach_openbin($attach, PR_ATTACH_DATA_BIN));
411
	}
412
413
	/**
414
	 * Function to sanitize user input values to prevent XSS attacks.
415
	 *
416
	 * @param mixed  $value   value that should be sanitized
417
	 * @param mixed  $default default value to return when value is not safe
418
	 * @param string $regex   regex to validate values based on type of value passed
419
	 */
420
	public function sanitizeValue($value, $default = '', $regex = false) {
421
		$result = addslashes((string) $value);
422
		if ($regex) {
423
			$match = preg_match_all($regex, $result);
424
			if (!$match) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $match of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
425
				$result = $default;
426
			}
427
		}
428
429
		return $result;
430
	}
431
}
432