Test Failed
Push — master ( b103a7...4c2e75 )
by
unknown
17:01 queued 05:50
created
plugins/smime/php/plugin.smime.php 2 patches
Indentation   +1087 added lines, -1087 removed lines patch added patch discarded remove patch
@@ -34,520 +34,520 @@  discard block
 block discarded – undo
34 34
 define('OPENSSL_RECIPIENT_CERTIFICATE_MISMATCH', '21070073');
35 35
 
36 36
 class Pluginsmime extends Plugin {
37
-	/**
38
-	 * decrypted/verified message.
39
-	 */
40
-	private $message = [];
41
-
42
-	/**
43
-	 * Default MAPI Message Store.
44
-	 */
45
-	private $store;
46
-
47
-	/**
48
-	 * Last openssl error string.
49
-	 */
50
-	private $openssl_error = "";
51
-
52
-	/**
53
-	 * Called to initialize the plugin and register for hooks.
54
-	 */
55
-	public function init() {
56
-		$this->registerHook('server.core.settings.init.before');
57
-		$this->registerHook('server.util.parse_smime.signed');
58
-		$this->registerHook('server.util.parse_smime.encrypted');
59
-		$this->registerHook('server.module.itemmodule.open.after');
60
-		$this->registerHook('server.core.operations.submitmessage');
61
-		$this->registerHook('server.upload_attachment.upload');
62
-		$this->registerHook('server.module.createmailitemmodule.beforesend');
63
-		$this->registerHook('server.index.load.custom');
64
-
65
-		if (version_compare(phpversion(), '5.4', '<')) {
66
-			$this->cipher = OPENSSL_CIPHER_3DES;
67
-		}
68
-		else {
69
-			$this->cipher = PLUGIN_SMIME_CIPHER;
70
-		}
71
-	}
72
-
73
-	/**
74
-	 * Default message store.
75
-	 *
76
-	 * @return object MAPI Message store
77
-	 */
78
-	public function getStore() {
79
-		if (!$this->store) {
80
-			$this->store = $GLOBALS['mapisession']->getDefaultMessageStore();
81
-		}
82
-
83
-		return $this->store;
84
-	}
85
-
86
-	/**
87
-	 * Process the incoming events that where fired by the client.
88
-	 *
89
-	 * @param string $eventID Identifier of the hook
90
-	 * @param array  $data    Reference to the data of the triggered hook
91
-	 */
92
-	public function execute($eventID, &$data) {
93
-		switch ($eventID) {
94
-			// Register plugin
95
-			case 'server.core.settings.init.before':
96
-				$this->onBeforeSettingsInit($data);
97
-				break;
98
-			// Verify a signed or encrypted message when an email is opened
99
-			case 'server.util.parse_smime.signed':
100
-				$this->onSignedMessage($data);
101
-				break;
102
-
103
-			case 'server.util.parse_smime.encrypted':
104
-				$this->onEncrypted($data);
105
-				break;
106
-			// Add S/MIME property, which is send to the client
107
-			case 'server.module.itemmodule.open.after':
108
-				$this->onAfterOpen($data);
109
-				break;
110
-			// Catch uploaded certificate
111
-			case 'server.upload_attachment.upload':
112
-				$this->onUploadCertificate($data);
113
-				break;
114
-			// Sign email before sending
115
-			case 'server.core.operations.submitmessage':
116
-				$this->onBeforeSend($data);
117
-				break;
118
-			// Verify that we have public certificates for all recipients
119
-			case 'server.module.createmailitemmodule.beforesend':
120
-				$this->onCertificateCheck($data);
121
-				break;
122
-
123
-			case 'server.index.load.custom':
124
-				if ($data['name'] === 'smime_passphrase') {
125
-					include 'templates/passphrase.tpl.php';
126
-
127
-					exit();
128
-				}
129
-				if ($data['name'] === 'smime_passphrasecheck') {
130
-					// No need to do anything, this is just used to trigger
131
-					// the browser's autofill save password dialog.
132
-					exit();
133
-				}
134
-				break;
135
-		}
136
-	}
137
-
138
-	/**
139
-	 * Function checks if public certificate exists for all recipients and creates an error
140
-	 * message for the frontend which includes the email address of the missing public
141
-	 * certificates.
142
-	 *
143
-	 * If my own certificate is missing, a different error message is shown which informs the
144
-	 * user that his own public certificate is missing and required for reading encrypted emails
145
-	 * in the 'Sent items' folder.
146
-	 *
147
-	 * @param array $data Reference to the data of the triggered hook
148
-	 */
149
-	public function onCertificateCheck($data) {
150
-		$entryid = $data['entryid'];
151
-		// FIXME: unittests, save trigger will pass $entryid is 0 (which will open the root folder and not the message we want)
152
-		if ($entryid === false) {
153
-			return;
154
-		}
155
-
156
-		if (!isset($data['action']['props']['smime']) || empty($data['action']['props']['smime'])) {
157
-			return;
158
-		}
159
-
160
-		$message = mapi_msgstore_openentry($data['store'], $entryid);
161
-		$module = $data['moduleObject'];
162
-		$data['success'] = true;
163
-
164
-		$messageClass = mapi_getprops($message, [PR_MESSAGE_CLASS]);
165
-		$messageClass = $messageClass[PR_MESSAGE_CLASS];
166
-		if ($messageClass !== 'IPM.Note.SMIME' && $messageClass !== 'IPM.Note.SMIME.SignedEncrypt') {
167
-			return;
168
-		}
169
-
170
-		$recipients = $data['action']['props']['smime'];
171
-		$missingCerts = [];
172
-
173
-		foreach ($recipients as $recipient) {
174
-			$email = $recipient['email'];
175
-
176
-			if (!$this->pubcertExists($email, $recipient['internal'])) {
177
-				array_push($missingCerts, $email);
178
-			}
179
-		}
180
-
181
-		if (empty($missingCerts)) {
182
-			return;
183
-		}
184
-
185
-		function missingMyself($email) {
186
-			return $GLOBALS['mapisession']->getSMTPAddress() === $email;
187
-		}
188
-
189
-		if (array_filter($missingCerts, "missingMyself") === []) {
190
-			$errorMsg = _('Missing public certificates for the following recipients: ') . implode(', ', $missingCerts) . _('. Please contact your system administrator for details');
191
-		}
192
-		else {
193
-			$errorMsg = _("Your public certificate is not installed. Without this certificate, you will not be able to read encrypted messages you have sent to others.");
194
-		}
195
-
196
-		$module->sendFeedback(false, ["type" => ERROR_GENERAL, "info" => ['display_message' => $errorMsg]]);
197
-		$data['success'] = false;
198
-	}
199
-
200
-	/**
201
-	 * Function which verifies a message.
202
-	 *
203
-	 * TODO: Clean up flow
204
-	 *
205
-	 * @param mixed $message
206
-	 * @param mixed $eml
207
-	 */
208
-	public function verifyMessage($message, $eml) {
209
-		$userCert = '';
210
-		$tmpUserCert = tempnam(sys_get_temp_dir(), true);
211
-		$importMessageCert = true;
212
-		$fromGAB = false;
213
-
214
-		// TODO: worth to split fetching public certificate in a separate function?
215
-
216
-		// If user entry exists in GAB, try to retrieve public cert
217
-		// Public certificate from GAB in combination with LDAP saved in PR_EMS_AB_TAGGED_X509_CERT
218
-		$userProps = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_NAME]);
219
-		if (isset($userProps[PR_SENT_REPRESENTING_ENTRYID])) {
220
-			try {
221
-				$user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(), $userProps[PR_SENT_REPRESENTING_ENTRYID]);
222
-				$gabCert = $this->getGABCert($user);
223
-				if (!empty($gabCert)) {
224
-					$fromGAB = true;
225
-					// Put empty string into file? dafuq?
226
-					file_put_contents($tmpUserCert, $userCert);
227
-				}
228
-			}
229
-			catch (MAPIException $e) {
230
-				$msg = "[smime] Unable to open PR_SENT_REPRESENTING_ENTRYID. Maybe %s was does not exists or deleted from server.";
231
-				Log::write(LOGLEVEL_ERROR, sprintf($msg, $userProps[PR_SENT_REPRESENTING_NAME]));
232
-				error_log("[smime] Unable to open PR_SENT_REPRESENTING_NAME: " . print_r($userProps[PR_SENT_REPRESENTING_NAME], true));
233
-				$this->message['success'] = SMIME_NOPUB;
234
-				$this->message['info'] = SMIME_USER_DETECT_FAILURE;
235
-			}
236
-		}
237
-
238
-		// When downloading an email as eml, $GLOBALS['operations'] isn't set, so add a check so that downloading works
239
-		// If the certificate is already fetch from the GAB, skip checking the userStore.
240
-		if (!$fromGAB && isset($GLOBALS['operations'])) {
241
-			$senderAddressArray = $this->getSenderAddress($message);
242
-			$senderAddressArray = $senderAddressArray['props'];
243
-			if ($senderAddressArray['address_type'] === 'SMTP') {
244
-				$emailAddr = $senderAddressArray['email_address'];
245
-			}
246
-			else {
247
-				$emailAddr = $senderAddressArray['smtp_address'];
248
-			}
249
-
250
-			// User not in AB,
251
-			// so get email address from either PR_SENT_REPRESENTING_NAME, PR_SEARCH_KEY or PR_SENT_REPRESENTING_SEARCH_KEY
252
-			// of the message
253
-			if (!$emailAddr) {
254
-				if (!empty($userProps[PR_SENT_REPRESENTING_NAME])) {
255
-					$emailAddr = $userProps[PR_SENT_REPRESENTING_NAME];
256
-				}
257
-				else {
258
-					$searchKeys = mapi_getprops($message, [PR_SEARCH_KEY, PR_SENT_REPRESENTING_SEARCH_KEY]);
259
-					$searchKey = $searchKeys[PR_SEARCH_KEY] ?? $searchKeys[PR_SENT_REPRESENTING_SEARCH_KEY];
260
-					if ($searchKey) {
261
-						$emailAddr = $trim(strtolower(explode(':', $searchKey)[1]));
262
-					}
263
-				}
264
-			}
265
-
266
-			if ($emailAddr) {
267
-				// Get all public certificates of $emailAddr stored on the server
268
-				$userCerts = $this->getPublicKey($emailAddr, true);
269
-			}
270
-		}
271
-
272
-		// Save signed message in a random file
273
-		$tmpfname = tempnam(sys_get_temp_dir(), true);
274
-		file_put_contents($tmpfname, $eml);
275
-
276
-		// Create random file for saving the signed message
277
-		$outcert = tempnam(sys_get_temp_dir(), true);
278
-
279
-		// Verify signed message
280
-		// Returns True if verified, False if tampered or signing certificate invalid OR -1 on error
281
-		if (count($userCerts) > 0) {
282
-			// Try to verify a certificate in the MAPI store
283
-			foreach ($userCerts as $userCert) {
284
-				$userCert = base64_decode($userCert);
285
-				// Save signed message in a random file
286
-				$tmpfname = tempnam(sys_get_temp_dir(), true);
287
-				file_put_contents($tmpfname, $eml);
288
-
289
-				// Create random file for saving the signed message
290
-				$outcert = tempnam(sys_get_temp_dir(), true);
291
-
292
-				if (!empty($userCert)) { // Check MAPI UserStore
293
-					file_put_contents($tmpUserCert, $userCert);
294
-				}
295
-				$signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOINTERN, $outcert, explode(';', PLUGIN_SMIME_CACERTS), $tmpUserCert);
296
-				$openssl_error_code = $this->extract_openssl_error();
297
-				$this->validateSignedMessage($signed_ok, $openssl_error_code);
298
-				// Check if we need to import a newer certificate
299
-				$importCert = file_get_contents($outcert);
300
-				$parsedImportCert = openssl_x509_parse($importCert);
301
-				$parsedUserCert = openssl_x509_parse($userCert);
302
-				if ($signed_ok && $openssl_error_code !== OPENSSL_CA_VERIFY_FAIL) { // CA Checks out
303
-					$caCerts = $this->extractCAs($tmpfname);
304
-					// If validTo and validFrom are more in the future, emailAddress matches and OCSP check is valid, import newer certificate
305
-					if ($parsedImportCert['validTo'] > $parsedUserCert['validTo'] && $parsedImportCert['validFrom'] > $parsedUserCert['validFrom'] &&
306
-						getCertEmail($parsedImportCert) === getCertEmail($parsedUserCert) && verifyOCSP($importCert, $caCerts, $this->message) &&
307
-						$importMessageCert !== false) {
308
-						// Redundant
309
-						$importMessageCert = true;
310
-					}
311
-					else {
312
-						$importMessageCert = false;
313
-						verifyOCSP($userCert, $caCerts, $this->message);
314
-						break;
315
-					}
316
-				}
317
-			}
318
-		}
319
-		else {
320
-			// Works. Just leave it.
321
-			$signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOSIGS, $outcert, explode(';', PLUGIN_SMIME_CACERTS));
322
-			$openssl_error_code = $this->extract_openssl_error();
323
-			$this->validateSignedMessage($signed_ok, $openssl_error_code);
324
-
325
-			// OCSP check
326
-			if ($signed_ok && $openssl_error_code !== OPENSSL_CA_VERIFY_FAIL) { // CA Checks out
327
-				$userCert = file_get_contents($outcert);
328
-				$parsedImportCert = openssl_x509_parse($userCert);
329
-
330
-				$caCerts = $this->extractCAs($tmpfname);
331
-				if (!is_array($parsedImportCert) || !verifyOCSP($userCert, $caCerts, $this->message)) {
332
-					$importMessageCert = false;
333
-				}
334
-				// We don't have a certificate from the MAPI UserStore or LDAP, so we will set $userCert to $importCert
335
-				// so that we can verify the message according to the be imported certificate.
336
-			}
337
-			else { // No pubkey
338
-				$importMessageCert = false;
339
-				Log::write(LOGLEVEL_INFO, sprintf("[smime] Unable to verify message without public key, openssl error: '%s'", $this->openssl_error));
340
-				$this->message['success'] = SMIME_STATUS_FAIL;
341
-				$this->message['info'] = SMIME_CA;
342
-			}
343
-		}
344
-		// Certificate is newer or not yet imported to the user store and not revoked
345
-		// If certificate is from the GAB, then don't import it.
346
-		if ($importMessageCert && !$fromGAB) {
347
-			$signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOSIGS, $outcert, explode(';', PLUGIN_SMIME_CACERTS));
348
-			$openssl_error_code = $this->extract_openssl_error();
349
-			$this->validateSignedMessage($signed_ok, $openssl_error_code);
350
-			$userCert = file_get_contents($outcert);
351
-			$parsedImportCert = openssl_x509_parse($userCert);
352
-			// FIXME: doing this in importPublicKey too...
353
-			$certEmail = getCertEmail($parsedImportCert);
354
-			if (!empty($certEmail)) {
355
-				$this->importCertificate($userCert, $parsedImportCert, 'public', true);
356
-			}
357
-		}
358
-
359
-		// Remove extracted certificate from openssl_pkcs7_verify
360
-		unlink($outcert);
361
-
362
-		// remove the temporary file
363
-		unlink($tmpfname);
364
-
365
-		// Clean up temp cert
366
-		unlink($tmpUserCert);
367
-	}
368
-
369
-	/**
370
-	 * Function which decrypts an encrypted message.
371
-	 * The key should be unlocked and stored in the EncryptionStore for a successful decrypt
372
-	 * If the key isn't in the session, we give the user a message to unlock his certificate.
373
-	 *
374
-	 * @param {mixed} $data array of data from hook
375
-	 */
376
-	public function onEncrypted($data) {
377
-		// Cert unlocked, decode message
378
-		$this->message['success'] = SMIME_STATUS_INFO;
379
-		$this->message['info'] = SMIME_DECRYPT_FAILURE;
380
-
381
-		$this->message['type'] = 'encrypted';
382
-		$encryptionStore = EncryptionStore::getInstance();
383
-		$pass = $encryptionStore->get('smime');
384
-		if (isset($pass) && !empty($pass)) {
385
-			$certs = readPrivateCert($this->getStore(), $pass, false);
386
-			// create random file for saving the encrypted and body message
387
-			$tmpFile = tempnam(sys_get_temp_dir(), true);
388
-			$tmpDecrypted = tempnam(sys_get_temp_dir(), true);
389
-
390
-			// Write mime header. Because it's not provided in the attachment, otherwise openssl won't parse it
391
-			$fp = fopen($tmpFile, 'w');
392
-			fwrite($fp, "Content-Type: application/pkcs7-mime; name=\"smime.p7m\"; smime-type=enveloped-data\n");
393
-			fwrite($fp, "Content-Transfer-Encoding: base64\nContent-Disposition: attachment; filename=\"smime.p7m\"\n");
394
-			fwrite($fp, "Content-Description: S/MIME Encrypted Message\n\n");
395
-			fwrite($fp, chunk_split(base64_encode($data['data']), 72) . "\n");
396
-			fclose($fp);
397
-
398
-			$decryptStatus = false;
399
-			// If multiple private certs were decrypted with supplied password
400
-			if (!$certs['cert'] && count($certs) > 0) {
401
-				foreach ($certs as $cert) {
402
-					$decryptStatus = openssl_pkcs7_decrypt($tmpFile, $tmpDecrypted, $cert['cert'], [$cert['pkey'], $pass]);
403
-					if ($decryptStatus !== false) {
404
-						break;
405
-					}
406
-				}
407
-			}
408
-			else {
409
-				$decryptStatus = openssl_pkcs7_decrypt($tmpFile, $tmpDecrypted, $certs['cert'], [$certs['pkey'], $pass]);
410
-			}
411
-
412
-			$content = file_get_contents($tmpDecrypted);
413
-			// Handle OL empty body Outlook Signed & Encrypted mails.
414
-			// The S/MIME plugin has to extract the body from the signed message.
415
-			if (strpos($content, 'signed-data') !== false) {
416
-				$this->message['type'] = 'encryptsigned';
417
-				$olcert = tempnam(sys_get_temp_dir(), true);
418
-				$olmsg = tempnam(sys_get_temp_dir(), true);
419
-				openssl_pkcs7_verify($tmpDecrypted, PKCS7_NOVERIFY, $olcert);
420
-				openssl_pkcs7_verify($tmpDecrypted, PKCS7_NOVERIFY, $olcert, [], $olcert, $olmsg);
421
-				$content = file_get_contents($olmsg);
422
-				unlink($olmsg);
423
-				unlink($olcert);
424
-			}
425
-
426
-			$copyProps = mapi_getprops($data['message'], [PR_MESSAGE_DELIVERY_TIME, PR_SENDER_ENTRYID, PR_SENT_REPRESENTING_ENTRYID]);
427
-			mapi_inetmapi_imtomapi($GLOBALS['mapisession']->getSession(), $data['store'], $GLOBALS['mapisession']->getAddressbook(), $data['message'], $content, ['parse_smime_signed' => true]);
428
-			// Manually set time back to the received time, since mapi_inetmapi_imtomapi overwrites this
429
-			mapi_setprops($data['message'], $copyProps);
430
-
431
-			// remove temporary files
432
-			unlink($tmpFile);
433
-			unlink($tmpDecrypted);
434
-
435
-			// mapi_inetmapi_imtomapi removes the PR_MESSAGE_CLASS = 'IPM.Note.SMIME.MultipartSigned'
436
-			// So we need to check if the message was also signed by looking at the MIME_TAG in the eml
437
-			if (strpos($content, 'multipart/signed') !== false || strpos($content, 'signed-data') !== false) {
438
-				$this->message['type'] = 'encryptsigned';
439
-				$this->verifyMessage($data['message'], $content);
440
-			}
441
-			elseif ($decryptStatus) {
442
-				$this->message['info'] = SMIME_DECRYPT_SUCCESS;
443
-				$this->message['success'] = SMIME_STATUS_SUCCESS;
444
-			}
445
-			elseif ($this->extract_openssl_error() === OPENSSL_RECIPIENT_CERTIFICATE_MISMATCH) {
446
-				error_log("[smime] Error when decrypting email, openssl error: " . print_r($this->openssl_error, true));
447
-				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Error when decrypting email, openssl error: '%s'", $this->openssl_error));
448
-				$this->message['info'] = SMIME_DECRYPT_CERT_MISMATCH;
449
-				$this->message['success'] = SMIME_STATUS_FAIL;
450
-			}
451
-		}
452
-		else {
453
-			$this->message['info'] = SMIME_UNLOCK_CERT;
454
-		}
455
-
456
-		if (!encryptionStoreExpirationSupport()) {
457
-			withPHPSession(function () use ($encryptionStore) {
458
-				$encryptionStore->add('smime', '');
459
-			});
460
-		}
461
-	}
462
-
463
-	/**
464
-	 * Function which calls verifyMessage to verify if the message isn't malformed during transport.
465
-	 *
466
-	 * @param {mixed} $data array of data from hook
467
-	 */
468
-	public function onSignedMessage($data) {
469
-		$this->message['type'] = 'signed';
470
-		$this->verifyMessage($data['message'], $data['data']);
471
-	}
472
-
473
-	/**
474
-	 * General function which parses the openssl_pkcs7_verify return value and the errors generated by
475
-	 * openssl_error_string().
476
-	 *
477
-	 * @param mixed $openssl_return
478
-	 * @param mixed $openssl_errors
479
-	 */
480
-	public function validateSignedMessage($openssl_return, $openssl_errors) {
481
-		if ($openssl_return === -1) {
482
-			$this->message['info'] = SMIME_ERROR;
483
-			$this->message['success'] = SMIME_STATUS_FAIL;
484
-		// Verification was successful
485
-		}
486
-		elseif ($openssl_return) {
487
-			$this->message['info'] = SMIME_SUCCESS;
488
-			$this->message['success'] = SMIME_STATUS_SUCCESS;
489
-		// Verification was not successful, display extra information.
490
-		}
491
-		else {
492
-			$this->message['success'] = SMIME_STATUS_FAIL;
493
-			if ($openssl_errors === OPENSSL_CA_VERIFY_FAIL) {
494
-				$this->message['info'] = SMIME_CA;
495
-			}
496
-			else { // Catch general errors
497
-				$this->message['info'] = SMIME_ERROR;
498
-			}
499
-		}
500
-	}
501
-
502
-	/**
503
-	 * Set smime key in $data array, which is send back to client
504
-	 * Since we can't create this array key in the hooks:
505
-	 * 'server.util.parse_smime.signed'
506
-	 * 'server.util.parse_smime.encrypted'.
507
-	 *
508
-	 * TODO: investigate if we can move away from this hook
509
-	 *
510
-	 * @param {mixed} $data
511
-	 */
512
-	public function onAfterOpen($data) {
513
-		if (isset($this->message) && !empty($this->message)) {
514
-			$data['data']['item']['props']['smime'] = $this->message;
515
-		}
516
-	}
517
-
518
-	/**
519
-	 * Handles the uploaded certificate in the settingsmenu in grommunio Web
520
-	 * - Opens the certificate with provided passphrase
521
-	 * - Checks if it can be used for signing/decrypting
522
-	 * - Verifies that the email address is equal to the
523
-	 * - Verifies that the certificate isn't expired and inform user.
524
-	 *
525
-	 * @param {mixed} $data
526
-	 */
527
-	public function onUploadCertificate($data) {
528
-		if ($data['sourcetype'] === 'certificate') {
529
-			$passphrase = $_POST['passphrase'];
530
-			$saveCert = false;
531
-			$tmpname = $data['tmpname'];
532
-			$message = '';
533
-
534
-			$certificate = file_get_contents($tmpname);
535
-			$emailAddress = $GLOBALS['mapisession']->getSMTPAddress();
536
-			list($message, $publickey, $publickeyData) = validateUploadedPKCS($certificate, $passphrase, $emailAddress);
537
-
538
-			// All checks completed successful
539
-			// Store private cert in users associated store (check for duplicates)
540
-			if (empty($message)) {
541
-				$certMessage = getMAPICert($this->getStore());
542
-				// TODO: update to serialNumber check
543
-				if ($certMessage && $certMessage[0][PR_MESSAGE_DELIVERY_TIME] == $publickeyData['validTo_time_t']) {
544
-					$message = _('Certificate is already stored on the server');
545
-				}
546
-				else {
547
-					$saveCert = true;
548
-					$root = mapi_msgstore_openentry($this->getStore(), null);
549
-					// Remove old certificate
550
-					/*
37
+    /**
38
+     * decrypted/verified message.
39
+     */
40
+    private $message = [];
41
+
42
+    /**
43
+     * Default MAPI Message Store.
44
+     */
45
+    private $store;
46
+
47
+    /**
48
+     * Last openssl error string.
49
+     */
50
+    private $openssl_error = "";
51
+
52
+    /**
53
+     * Called to initialize the plugin and register for hooks.
54
+     */
55
+    public function init() {
56
+        $this->registerHook('server.core.settings.init.before');
57
+        $this->registerHook('server.util.parse_smime.signed');
58
+        $this->registerHook('server.util.parse_smime.encrypted');
59
+        $this->registerHook('server.module.itemmodule.open.after');
60
+        $this->registerHook('server.core.operations.submitmessage');
61
+        $this->registerHook('server.upload_attachment.upload');
62
+        $this->registerHook('server.module.createmailitemmodule.beforesend');
63
+        $this->registerHook('server.index.load.custom');
64
+
65
+        if (version_compare(phpversion(), '5.4', '<')) {
66
+            $this->cipher = OPENSSL_CIPHER_3DES;
67
+        }
68
+        else {
69
+            $this->cipher = PLUGIN_SMIME_CIPHER;
70
+        }
71
+    }
72
+
73
+    /**
74
+     * Default message store.
75
+     *
76
+     * @return object MAPI Message store
77
+     */
78
+    public function getStore() {
79
+        if (!$this->store) {
80
+            $this->store = $GLOBALS['mapisession']->getDefaultMessageStore();
81
+        }
82
+
83
+        return $this->store;
84
+    }
85
+
86
+    /**
87
+     * Process the incoming events that where fired by the client.
88
+     *
89
+     * @param string $eventID Identifier of the hook
90
+     * @param array  $data    Reference to the data of the triggered hook
91
+     */
92
+    public function execute($eventID, &$data) {
93
+        switch ($eventID) {
94
+            // Register plugin
95
+            case 'server.core.settings.init.before':
96
+                $this->onBeforeSettingsInit($data);
97
+                break;
98
+            // Verify a signed or encrypted message when an email is opened
99
+            case 'server.util.parse_smime.signed':
100
+                $this->onSignedMessage($data);
101
+                break;
102
+
103
+            case 'server.util.parse_smime.encrypted':
104
+                $this->onEncrypted($data);
105
+                break;
106
+            // Add S/MIME property, which is send to the client
107
+            case 'server.module.itemmodule.open.after':
108
+                $this->onAfterOpen($data);
109
+                break;
110
+            // Catch uploaded certificate
111
+            case 'server.upload_attachment.upload':
112
+                $this->onUploadCertificate($data);
113
+                break;
114
+            // Sign email before sending
115
+            case 'server.core.operations.submitmessage':
116
+                $this->onBeforeSend($data);
117
+                break;
118
+            // Verify that we have public certificates for all recipients
119
+            case 'server.module.createmailitemmodule.beforesend':
120
+                $this->onCertificateCheck($data);
121
+                break;
122
+
123
+            case 'server.index.load.custom':
124
+                if ($data['name'] === 'smime_passphrase') {
125
+                    include 'templates/passphrase.tpl.php';
126
+
127
+                    exit();
128
+                }
129
+                if ($data['name'] === 'smime_passphrasecheck') {
130
+                    // No need to do anything, this is just used to trigger
131
+                    // the browser's autofill save password dialog.
132
+                    exit();
133
+                }
134
+                break;
135
+        }
136
+    }
137
+
138
+    /**
139
+     * Function checks if public certificate exists for all recipients and creates an error
140
+     * message for the frontend which includes the email address of the missing public
141
+     * certificates.
142
+     *
143
+     * If my own certificate is missing, a different error message is shown which informs the
144
+     * user that his own public certificate is missing and required for reading encrypted emails
145
+     * in the 'Sent items' folder.
146
+     *
147
+     * @param array $data Reference to the data of the triggered hook
148
+     */
149
+    public function onCertificateCheck($data) {
150
+        $entryid = $data['entryid'];
151
+        // FIXME: unittests, save trigger will pass $entryid is 0 (which will open the root folder and not the message we want)
152
+        if ($entryid === false) {
153
+            return;
154
+        }
155
+
156
+        if (!isset($data['action']['props']['smime']) || empty($data['action']['props']['smime'])) {
157
+            return;
158
+        }
159
+
160
+        $message = mapi_msgstore_openentry($data['store'], $entryid);
161
+        $module = $data['moduleObject'];
162
+        $data['success'] = true;
163
+
164
+        $messageClass = mapi_getprops($message, [PR_MESSAGE_CLASS]);
165
+        $messageClass = $messageClass[PR_MESSAGE_CLASS];
166
+        if ($messageClass !== 'IPM.Note.SMIME' && $messageClass !== 'IPM.Note.SMIME.SignedEncrypt') {
167
+            return;
168
+        }
169
+
170
+        $recipients = $data['action']['props']['smime'];
171
+        $missingCerts = [];
172
+
173
+        foreach ($recipients as $recipient) {
174
+            $email = $recipient['email'];
175
+
176
+            if (!$this->pubcertExists($email, $recipient['internal'])) {
177
+                array_push($missingCerts, $email);
178
+            }
179
+        }
180
+
181
+        if (empty($missingCerts)) {
182
+            return;
183
+        }
184
+
185
+        function missingMyself($email) {
186
+            return $GLOBALS['mapisession']->getSMTPAddress() === $email;
187
+        }
188
+
189
+        if (array_filter($missingCerts, "missingMyself") === []) {
190
+            $errorMsg = _('Missing public certificates for the following recipients: ') . implode(', ', $missingCerts) . _('. Please contact your system administrator for details');
191
+        }
192
+        else {
193
+            $errorMsg = _("Your public certificate is not installed. Without this certificate, you will not be able to read encrypted messages you have sent to others.");
194
+        }
195
+
196
+        $module->sendFeedback(false, ["type" => ERROR_GENERAL, "info" => ['display_message' => $errorMsg]]);
197
+        $data['success'] = false;
198
+    }
199
+
200
+    /**
201
+     * Function which verifies a message.
202
+     *
203
+     * TODO: Clean up flow
204
+     *
205
+     * @param mixed $message
206
+     * @param mixed $eml
207
+     */
208
+    public function verifyMessage($message, $eml) {
209
+        $userCert = '';
210
+        $tmpUserCert = tempnam(sys_get_temp_dir(), true);
211
+        $importMessageCert = true;
212
+        $fromGAB = false;
213
+
214
+        // TODO: worth to split fetching public certificate in a separate function?
215
+
216
+        // If user entry exists in GAB, try to retrieve public cert
217
+        // Public certificate from GAB in combination with LDAP saved in PR_EMS_AB_TAGGED_X509_CERT
218
+        $userProps = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_NAME]);
219
+        if (isset($userProps[PR_SENT_REPRESENTING_ENTRYID])) {
220
+            try {
221
+                $user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(), $userProps[PR_SENT_REPRESENTING_ENTRYID]);
222
+                $gabCert = $this->getGABCert($user);
223
+                if (!empty($gabCert)) {
224
+                    $fromGAB = true;
225
+                    // Put empty string into file? dafuq?
226
+                    file_put_contents($tmpUserCert, $userCert);
227
+                }
228
+            }
229
+            catch (MAPIException $e) {
230
+                $msg = "[smime] Unable to open PR_SENT_REPRESENTING_ENTRYID. Maybe %s was does not exists or deleted from server.";
231
+                Log::write(LOGLEVEL_ERROR, sprintf($msg, $userProps[PR_SENT_REPRESENTING_NAME]));
232
+                error_log("[smime] Unable to open PR_SENT_REPRESENTING_NAME: " . print_r($userProps[PR_SENT_REPRESENTING_NAME], true));
233
+                $this->message['success'] = SMIME_NOPUB;
234
+                $this->message['info'] = SMIME_USER_DETECT_FAILURE;
235
+            }
236
+        }
237
+
238
+        // When downloading an email as eml, $GLOBALS['operations'] isn't set, so add a check so that downloading works
239
+        // If the certificate is already fetch from the GAB, skip checking the userStore.
240
+        if (!$fromGAB && isset($GLOBALS['operations'])) {
241
+            $senderAddressArray = $this->getSenderAddress($message);
242
+            $senderAddressArray = $senderAddressArray['props'];
243
+            if ($senderAddressArray['address_type'] === 'SMTP') {
244
+                $emailAddr = $senderAddressArray['email_address'];
245
+            }
246
+            else {
247
+                $emailAddr = $senderAddressArray['smtp_address'];
248
+            }
249
+
250
+            // User not in AB,
251
+            // so get email address from either PR_SENT_REPRESENTING_NAME, PR_SEARCH_KEY or PR_SENT_REPRESENTING_SEARCH_KEY
252
+            // of the message
253
+            if (!$emailAddr) {
254
+                if (!empty($userProps[PR_SENT_REPRESENTING_NAME])) {
255
+                    $emailAddr = $userProps[PR_SENT_REPRESENTING_NAME];
256
+                }
257
+                else {
258
+                    $searchKeys = mapi_getprops($message, [PR_SEARCH_KEY, PR_SENT_REPRESENTING_SEARCH_KEY]);
259
+                    $searchKey = $searchKeys[PR_SEARCH_KEY] ?? $searchKeys[PR_SENT_REPRESENTING_SEARCH_KEY];
260
+                    if ($searchKey) {
261
+                        $emailAddr = $trim(strtolower(explode(':', $searchKey)[1]));
262
+                    }
263
+                }
264
+            }
265
+
266
+            if ($emailAddr) {
267
+                // Get all public certificates of $emailAddr stored on the server
268
+                $userCerts = $this->getPublicKey($emailAddr, true);
269
+            }
270
+        }
271
+
272
+        // Save signed message in a random file
273
+        $tmpfname = tempnam(sys_get_temp_dir(), true);
274
+        file_put_contents($tmpfname, $eml);
275
+
276
+        // Create random file for saving the signed message
277
+        $outcert = tempnam(sys_get_temp_dir(), true);
278
+
279
+        // Verify signed message
280
+        // Returns True if verified, False if tampered or signing certificate invalid OR -1 on error
281
+        if (count($userCerts) > 0) {
282
+            // Try to verify a certificate in the MAPI store
283
+            foreach ($userCerts as $userCert) {
284
+                $userCert = base64_decode($userCert);
285
+                // Save signed message in a random file
286
+                $tmpfname = tempnam(sys_get_temp_dir(), true);
287
+                file_put_contents($tmpfname, $eml);
288
+
289
+                // Create random file for saving the signed message
290
+                $outcert = tempnam(sys_get_temp_dir(), true);
291
+
292
+                if (!empty($userCert)) { // Check MAPI UserStore
293
+                    file_put_contents($tmpUserCert, $userCert);
294
+                }
295
+                $signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOINTERN, $outcert, explode(';', PLUGIN_SMIME_CACERTS), $tmpUserCert);
296
+                $openssl_error_code = $this->extract_openssl_error();
297
+                $this->validateSignedMessage($signed_ok, $openssl_error_code);
298
+                // Check if we need to import a newer certificate
299
+                $importCert = file_get_contents($outcert);
300
+                $parsedImportCert = openssl_x509_parse($importCert);
301
+                $parsedUserCert = openssl_x509_parse($userCert);
302
+                if ($signed_ok && $openssl_error_code !== OPENSSL_CA_VERIFY_FAIL) { // CA Checks out
303
+                    $caCerts = $this->extractCAs($tmpfname);
304
+                    // If validTo and validFrom are more in the future, emailAddress matches and OCSP check is valid, import newer certificate
305
+                    if ($parsedImportCert['validTo'] > $parsedUserCert['validTo'] && $parsedImportCert['validFrom'] > $parsedUserCert['validFrom'] &&
306
+                        getCertEmail($parsedImportCert) === getCertEmail($parsedUserCert) && verifyOCSP($importCert, $caCerts, $this->message) &&
307
+                        $importMessageCert !== false) {
308
+                        // Redundant
309
+                        $importMessageCert = true;
310
+                    }
311
+                    else {
312
+                        $importMessageCert = false;
313
+                        verifyOCSP($userCert, $caCerts, $this->message);
314
+                        break;
315
+                    }
316
+                }
317
+            }
318
+        }
319
+        else {
320
+            // Works. Just leave it.
321
+            $signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOSIGS, $outcert, explode(';', PLUGIN_SMIME_CACERTS));
322
+            $openssl_error_code = $this->extract_openssl_error();
323
+            $this->validateSignedMessage($signed_ok, $openssl_error_code);
324
+
325
+            // OCSP check
326
+            if ($signed_ok && $openssl_error_code !== OPENSSL_CA_VERIFY_FAIL) { // CA Checks out
327
+                $userCert = file_get_contents($outcert);
328
+                $parsedImportCert = openssl_x509_parse($userCert);
329
+
330
+                $caCerts = $this->extractCAs($tmpfname);
331
+                if (!is_array($parsedImportCert) || !verifyOCSP($userCert, $caCerts, $this->message)) {
332
+                    $importMessageCert = false;
333
+                }
334
+                // We don't have a certificate from the MAPI UserStore or LDAP, so we will set $userCert to $importCert
335
+                // so that we can verify the message according to the be imported certificate.
336
+            }
337
+            else { // No pubkey
338
+                $importMessageCert = false;
339
+                Log::write(LOGLEVEL_INFO, sprintf("[smime] Unable to verify message without public key, openssl error: '%s'", $this->openssl_error));
340
+                $this->message['success'] = SMIME_STATUS_FAIL;
341
+                $this->message['info'] = SMIME_CA;
342
+            }
343
+        }
344
+        // Certificate is newer or not yet imported to the user store and not revoked
345
+        // If certificate is from the GAB, then don't import it.
346
+        if ($importMessageCert && !$fromGAB) {
347
+            $signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOSIGS, $outcert, explode(';', PLUGIN_SMIME_CACERTS));
348
+            $openssl_error_code = $this->extract_openssl_error();
349
+            $this->validateSignedMessage($signed_ok, $openssl_error_code);
350
+            $userCert = file_get_contents($outcert);
351
+            $parsedImportCert = openssl_x509_parse($userCert);
352
+            // FIXME: doing this in importPublicKey too...
353
+            $certEmail = getCertEmail($parsedImportCert);
354
+            if (!empty($certEmail)) {
355
+                $this->importCertificate($userCert, $parsedImportCert, 'public', true);
356
+            }
357
+        }
358
+
359
+        // Remove extracted certificate from openssl_pkcs7_verify
360
+        unlink($outcert);
361
+
362
+        // remove the temporary file
363
+        unlink($tmpfname);
364
+
365
+        // Clean up temp cert
366
+        unlink($tmpUserCert);
367
+    }
368
+
369
+    /**
370
+     * Function which decrypts an encrypted message.
371
+     * The key should be unlocked and stored in the EncryptionStore for a successful decrypt
372
+     * If the key isn't in the session, we give the user a message to unlock his certificate.
373
+     *
374
+     * @param {mixed} $data array of data from hook
375
+     */
376
+    public function onEncrypted($data) {
377
+        // Cert unlocked, decode message
378
+        $this->message['success'] = SMIME_STATUS_INFO;
379
+        $this->message['info'] = SMIME_DECRYPT_FAILURE;
380
+
381
+        $this->message['type'] = 'encrypted';
382
+        $encryptionStore = EncryptionStore::getInstance();
383
+        $pass = $encryptionStore->get('smime');
384
+        if (isset($pass) && !empty($pass)) {
385
+            $certs = readPrivateCert($this->getStore(), $pass, false);
386
+            // create random file for saving the encrypted and body message
387
+            $tmpFile = tempnam(sys_get_temp_dir(), true);
388
+            $tmpDecrypted = tempnam(sys_get_temp_dir(), true);
389
+
390
+            // Write mime header. Because it's not provided in the attachment, otherwise openssl won't parse it
391
+            $fp = fopen($tmpFile, 'w');
392
+            fwrite($fp, "Content-Type: application/pkcs7-mime; name=\"smime.p7m\"; smime-type=enveloped-data\n");
393
+            fwrite($fp, "Content-Transfer-Encoding: base64\nContent-Disposition: attachment; filename=\"smime.p7m\"\n");
394
+            fwrite($fp, "Content-Description: S/MIME Encrypted Message\n\n");
395
+            fwrite($fp, chunk_split(base64_encode($data['data']), 72) . "\n");
396
+            fclose($fp);
397
+
398
+            $decryptStatus = false;
399
+            // If multiple private certs were decrypted with supplied password
400
+            if (!$certs['cert'] && count($certs) > 0) {
401
+                foreach ($certs as $cert) {
402
+                    $decryptStatus = openssl_pkcs7_decrypt($tmpFile, $tmpDecrypted, $cert['cert'], [$cert['pkey'], $pass]);
403
+                    if ($decryptStatus !== false) {
404
+                        break;
405
+                    }
406
+                }
407
+            }
408
+            else {
409
+                $decryptStatus = openssl_pkcs7_decrypt($tmpFile, $tmpDecrypted, $certs['cert'], [$certs['pkey'], $pass]);
410
+            }
411
+
412
+            $content = file_get_contents($tmpDecrypted);
413
+            // Handle OL empty body Outlook Signed & Encrypted mails.
414
+            // The S/MIME plugin has to extract the body from the signed message.
415
+            if (strpos($content, 'signed-data') !== false) {
416
+                $this->message['type'] = 'encryptsigned';
417
+                $olcert = tempnam(sys_get_temp_dir(), true);
418
+                $olmsg = tempnam(sys_get_temp_dir(), true);
419
+                openssl_pkcs7_verify($tmpDecrypted, PKCS7_NOVERIFY, $olcert);
420
+                openssl_pkcs7_verify($tmpDecrypted, PKCS7_NOVERIFY, $olcert, [], $olcert, $olmsg);
421
+                $content = file_get_contents($olmsg);
422
+                unlink($olmsg);
423
+                unlink($olcert);
424
+            }
425
+
426
+            $copyProps = mapi_getprops($data['message'], [PR_MESSAGE_DELIVERY_TIME, PR_SENDER_ENTRYID, PR_SENT_REPRESENTING_ENTRYID]);
427
+            mapi_inetmapi_imtomapi($GLOBALS['mapisession']->getSession(), $data['store'], $GLOBALS['mapisession']->getAddressbook(), $data['message'], $content, ['parse_smime_signed' => true]);
428
+            // Manually set time back to the received time, since mapi_inetmapi_imtomapi overwrites this
429
+            mapi_setprops($data['message'], $copyProps);
430
+
431
+            // remove temporary files
432
+            unlink($tmpFile);
433
+            unlink($tmpDecrypted);
434
+
435
+            // mapi_inetmapi_imtomapi removes the PR_MESSAGE_CLASS = 'IPM.Note.SMIME.MultipartSigned'
436
+            // So we need to check if the message was also signed by looking at the MIME_TAG in the eml
437
+            if (strpos($content, 'multipart/signed') !== false || strpos($content, 'signed-data') !== false) {
438
+                $this->message['type'] = 'encryptsigned';
439
+                $this->verifyMessage($data['message'], $content);
440
+            }
441
+            elseif ($decryptStatus) {
442
+                $this->message['info'] = SMIME_DECRYPT_SUCCESS;
443
+                $this->message['success'] = SMIME_STATUS_SUCCESS;
444
+            }
445
+            elseif ($this->extract_openssl_error() === OPENSSL_RECIPIENT_CERTIFICATE_MISMATCH) {
446
+                error_log("[smime] Error when decrypting email, openssl error: " . print_r($this->openssl_error, true));
447
+                Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Error when decrypting email, openssl error: '%s'", $this->openssl_error));
448
+                $this->message['info'] = SMIME_DECRYPT_CERT_MISMATCH;
449
+                $this->message['success'] = SMIME_STATUS_FAIL;
450
+            }
451
+        }
452
+        else {
453
+            $this->message['info'] = SMIME_UNLOCK_CERT;
454
+        }
455
+
456
+        if (!encryptionStoreExpirationSupport()) {
457
+            withPHPSession(function () use ($encryptionStore) {
458
+                $encryptionStore->add('smime', '');
459
+            });
460
+        }
461
+    }
462
+
463
+    /**
464
+     * Function which calls verifyMessage to verify if the message isn't malformed during transport.
465
+     *
466
+     * @param {mixed} $data array of data from hook
467
+     */
468
+    public function onSignedMessage($data) {
469
+        $this->message['type'] = 'signed';
470
+        $this->verifyMessage($data['message'], $data['data']);
471
+    }
472
+
473
+    /**
474
+     * General function which parses the openssl_pkcs7_verify return value and the errors generated by
475
+     * openssl_error_string().
476
+     *
477
+     * @param mixed $openssl_return
478
+     * @param mixed $openssl_errors
479
+     */
480
+    public function validateSignedMessage($openssl_return, $openssl_errors) {
481
+        if ($openssl_return === -1) {
482
+            $this->message['info'] = SMIME_ERROR;
483
+            $this->message['success'] = SMIME_STATUS_FAIL;
484
+        // Verification was successful
485
+        }
486
+        elseif ($openssl_return) {
487
+            $this->message['info'] = SMIME_SUCCESS;
488
+            $this->message['success'] = SMIME_STATUS_SUCCESS;
489
+        // Verification was not successful, display extra information.
490
+        }
491
+        else {
492
+            $this->message['success'] = SMIME_STATUS_FAIL;
493
+            if ($openssl_errors === OPENSSL_CA_VERIFY_FAIL) {
494
+                $this->message['info'] = SMIME_CA;
495
+            }
496
+            else { // Catch general errors
497
+                $this->message['info'] = SMIME_ERROR;
498
+            }
499
+        }
500
+    }
501
+
502
+    /**
503
+     * Set smime key in $data array, which is send back to client
504
+     * Since we can't create this array key in the hooks:
505
+     * 'server.util.parse_smime.signed'
506
+     * 'server.util.parse_smime.encrypted'.
507
+     *
508
+     * TODO: investigate if we can move away from this hook
509
+     *
510
+     * @param {mixed} $data
511
+     */
512
+    public function onAfterOpen($data) {
513
+        if (isset($this->message) && !empty($this->message)) {
514
+            $data['data']['item']['props']['smime'] = $this->message;
515
+        }
516
+    }
517
+
518
+    /**
519
+     * Handles the uploaded certificate in the settingsmenu in grommunio Web
520
+     * - Opens the certificate with provided passphrase
521
+     * - Checks if it can be used for signing/decrypting
522
+     * - Verifies that the email address is equal to the
523
+     * - Verifies that the certificate isn't expired and inform user.
524
+     *
525
+     * @param {mixed} $data
526
+     */
527
+    public function onUploadCertificate($data) {
528
+        if ($data['sourcetype'] === 'certificate') {
529
+            $passphrase = $_POST['passphrase'];
530
+            $saveCert = false;
531
+            $tmpname = $data['tmpname'];
532
+            $message = '';
533
+
534
+            $certificate = file_get_contents($tmpname);
535
+            $emailAddress = $GLOBALS['mapisession']->getSMTPAddress();
536
+            list($message, $publickey, $publickeyData) = validateUploadedPKCS($certificate, $passphrase, $emailAddress);
537
+
538
+            // All checks completed successful
539
+            // Store private cert in users associated store (check for duplicates)
540
+            if (empty($message)) {
541
+                $certMessage = getMAPICert($this->getStore());
542
+                // TODO: update to serialNumber check
543
+                if ($certMessage && $certMessage[0][PR_MESSAGE_DELIVERY_TIME] == $publickeyData['validTo_time_t']) {
544
+                    $message = _('Certificate is already stored on the server');
545
+                }
546
+                else {
547
+                    $saveCert = true;
548
+                    $root = mapi_msgstore_openentry($this->getStore(), null);
549
+                    // Remove old certificate
550
+                    /*
551 551
 					if($certMessage) {
552 552
 						// Delete private key
553 553
 						mapi_folder_deletemessages($root, array($certMessage[PR_ENTRYID]));
@@ -562,578 +562,578 @@  discard block
 block discarded – undo
562 562
 						$message = _('Certificate uploaded');
563 563
 					}*/
564 564
 
565
-					$this->importCertificate($certificate, $publickeyData, 'private');
566
-
567
-					// Check if the user has a public key in the GAB.
568
-					$store_props = mapi_getprops($this->getStore(), [PR_USER_ENTRYID]);
569
-					$user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(), $store_props[PR_USER_ENTRYID]);
570
-
571
-					$this->importCertificate($publickey, $publickeyData, 'public', true);
572
-				}
573
-			}
574
-
575
-			$returnfiles = [];
576
-			$returnfiles[] = [
577
-				'props' => [
578
-					'attach_num' => -1,
579
-					'size' => $data['size'],
580
-					'name' => $data['name'],
581
-					'cert' => $saveCert,
582
-					'cert_warning' => $message,
583
-				],
584
-			];
585
-			$data['returnfiles'] = $returnfiles;
586
-		}
587
-	}
588
-
589
-	/**
590
-	 * This function handles the 'beforesend' hook which is triggered before sending the email.
591
-	 * If the PR_MESSAGE_CLASS is set to a signed email (IPM.Note.SMIME.Multipartsigned), this function
592
-	 * will convert the mapi message to RFC822, sign the eml and attach the signed email to the mapi message.
593
-	 *
594
-	 * @param {mixed} $data from php hook
595
-	 */
596
-	public function onBeforeSend(&$data) {
597
-		$store = $data['store'];
598
-		$message = $data['message'];
599
-
600
-		// Retrieve message class
601
-		$props = mapi_getprops($message, [PR_MESSAGE_CLASS, PR_EC_IMAP_EMAIL]);
602
-		$messageClass = $props[PR_MESSAGE_CLASS];
603
-
604
-		if (isset($messageClass) && (stripos($messageClass, 'IPM.Note.SMIME') !== false)) {
605
-			// FIXME: for now return when we are going to sign but we don't have the passphrase set
606
-			// This should never happen sign
607
-			$encryptionStore = \EncryptionStore::getInstance();
608
-			if (($messageClass === 'IPM.Note.SMIME.SignedEncrypt' || $messageClass === 'IPM.Note.SMIME.MultipartSigned') &&
609
-				!$encryptionStore->get('smime')) {
610
-				return;
611
-			}
612
-			// NOTE: setting message class to IPM.Note, so that mapi_inetmapi_imtoinet converts the message to plain email
613
-			// and doesn't fail when handling the attachments.
614
-			mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note']);
615
-			mapi_savechanges($message);
616
-
617
-			// If RFC822-formatted stream is already available in PR_EC_IMAP_EMAIL property
618
-			// than directly use it, generate otherwise.
619
-			if (isset($props[PR_EC_IMAP_EMAIL]) || propIsError(PR_EC_IMAP_EMAIL, $props) == MAPI_E_NOT_ENOUGH_MEMORY) {
620
-				// Stream the message to properly get the PR_EC_IMAP_EMAIL property
621
-				$emlMessageStream = mapi_openproperty($message, PR_EC_IMAP_EMAIL, IID_IStream, 0, 0);
622
-			}
623
-			else {
624
-				// Read the message as RFC822-formatted e-mail stream.
625
-				$emlMessageStream = mapi_inetmapi_imtoinet($GLOBALS['mapisession']->getSession(), $GLOBALS['mapisession']->getAddressbook(), $message, []);
626
-			}
627
-
628
-			// Remove all attachments, since they are stored in the attached signed message
629
-			$atable = mapi_message_getattachmenttable($message);
630
-			$rows = mapi_table_queryallrows($atable, [PR_ATTACH_MIME_TAG, PR_ATTACH_NUM]);
631
-			foreach ($rows as $row) {
632
-				$attnum = $row[PR_ATTACH_NUM];
633
-				mapi_message_deleteattach($message, $attnum);
634
-			}
635
-
636
-			// create temporary files
637
-			$tmpSendEmail = tempnam(sys_get_temp_dir(), true);
638
-			$tmpSendSmimeEmail = tempnam(sys_get_temp_dir(), true);
639
-
640
-			// Save message stream to a file
641
-			$stat = mapi_stream_stat($emlMessageStream);
642
-
643
-			$fhandle = fopen($tmpSendEmail, 'w');
644
-			$buffer = null;
645
-			for ($i = 0; $i < $stat["cb"]; $i += BLOCK_SIZE) {
646
-				// Write stream
647
-				$buffer = mapi_stream_read($emlMessageStream, BLOCK_SIZE);
648
-				fwrite($fhandle, $buffer, strlen($buffer));
649
-			}
650
-			fclose($fhandle);
651
-
652
-			// Create attachment for S/MIME message
653
-			$signedAttach = mapi_message_createattach($message);
654
-			$smimeProps = [
655
-				PR_ATTACH_LONG_FILENAME => 'smime.p7m',
656
-				PR_DISPLAY_NAME => 'smime.p7m',
657
-				PR_ATTACH_METHOD => ATTACH_BY_VALUE,
658
-				PR_ATTACH_MIME_TAG => 'multipart/signed',
659
-				PR_ATTACHMENT_HIDDEN => true,
660
-			];
661
-
662
-			// Sign then Encrypt email
663
-			switch ($messageClass) {
664
-				case 'IPM.Note.SMIME.SignedEncrypt':
665
-					$tmpFile = tempnam(sys_get_temp_dir(), true);
666
-					$this->sign($tmpSendEmail, $tmpFile, $message, $signedAttach, $smimeProps);
667
-					$this->encrypt($tmpFile, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
668
-					unlink($tmpFile);
669
-					break;
670
-
671
-				case 'IPM.Note.SMIME.MultipartSigned':
672
-					$this->sign($tmpSendEmail, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
673
-					break;
674
-
675
-				case 'IPM.Note.SMIME':
676
-					$this->encrypt($tmpSendEmail, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
677
-					break;
678
-			}
679
-
680
-			// Save the signed message as attachment of the send email
681
-			$stream = mapi_openproperty($signedAttach, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
682
-			$handle = fopen($tmpSendSmimeEmail, 'r');
683
-			while (!feof($handle)) {
684
-				$contents = fread($handle, BLOCK_SIZE);
685
-				mapi_stream_write($stream, $contents);
686
-			}
687
-			fclose($handle);
688
-
689
-			mapi_stream_commit($stream);
690
-
691
-			// remove tmp files
692
-			unlink($tmpSendSmimeEmail);
693
-			unlink($tmpSendEmail);
694
-
695
-			mapi_savechanges($signedAttach);
696
-			mapi_savechanges($message);
697
-		}
698
-	}
699
-
700
-	/**
701
-	 * Function to sign an email.
702
-	 *
703
-	 * @param object $infile       File eml to be encrypted
704
-	 * @param object $outfile      File
705
-	 * @param object $message      Mapi Message Object
706
-	 * @param object $signedAttach
707
-	 * @param array  $smimeProps
708
-	 */
709
-	public function sign(&$infile, &$outfile, &$message, &$signedAttach, $smimeProps) {
710
-		// Set mesageclass back to IPM.Note.SMIME.MultipartSigned
711
-		mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note.SMIME.MultipartSigned']);
712
-		mapi_setprops($signedAttach, $smimeProps);
713
-
714
-		// Obtain private certificate
715
-		$encryptionStore = EncryptionStore::getInstance();
716
-		// Only the newest one is returned
717
-		$certs = readPrivateCert($this->getStore(), $encryptionStore->get('smime'));
718
-
719
-		// Retrieve intermediate CA's for verification, if available
720
-		if (isset($certs['extracerts'])) {
721
-			$tmpFile = tempnam(sys_get_temp_dir(), true);
722
-			file_put_contents($tmpFile, implode('', $certs['extracerts']));
723
-			$ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED, $tmpFile);
724
-			if (!$ok) {
725
-				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message with intermediate certificates, openssl error: '%s'", @openssl_error_string()));
726
-			}
727
-			unlink($tmpFile);
728
-		}
729
-		else {
730
-			$ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED);
731
-			if (!$ok) {
732
-				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message, openssl error: '%s'", @openssl_error_string()));
733
-			}
734
-		}
735
-	}
736
-
737
-	/**
738
-	 * Function to encrypt an email.
739
-	 *
740
-	 * @param object $infile       File eml to be encrypted
741
-	 * @param object $outfile      File
742
-	 * @param object $message      Mapi Message Object
743
-	 * @param object $signedAttach
744
-	 * @param array  $smimeProps
745
-	 */
746
-	public function encrypt(&$infile, &$outfile, &$message, &$signedAttach, $smimeProps) {
747
-		mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note.SMIME']);
748
-		$smimeProps[PR_ATTACH_MIME_TAG] = "application/pkcs7-mime";
749
-		mapi_setprops($signedAttach, $smimeProps);
750
-
751
-		$publicCerts = $this->getPublicKeyForMessage($message);
752
-		// Always append our own certificate, so that the mail can be decrypted in 'Sent items'
753
-		// Prefer GAB public certificate above MAPI Store certificate.
754
-		$email = $GLOBALS['mapisession']->getSMTPAddress();
755
-		$user = $this->getGABUser($email);
756
-		$cert = $this->getGABCert($user);
757
-		if (empty($cert)) {
758
-			$cert = base64_decode($this->getPublicKey($email));
759
-		}
760
-
761
-		if (!empty($cert)) {
762
-			array_push($publicCerts, $cert);
763
-		}
764
-
765
-		$ok = openssl_pkcs7_encrypt($infile, $outfile, $publicCerts, [], 0, $this->cipher);
766
-		if (!$ok) {
767
-			error_log("[smime] unable to encrypt message, openssl error: " . print_r(@openssl_error_string(), true));
768
-			Log::Write(LOGLEVEL_ERROR, sprintf("[smime] unable to encrypt message, openssl error: '%s'", @openssl_error_string()));
769
-		}
770
-		$tmpEml = file_get_contents($outfile);
771
-
772
-		// Grab the base64 data, since MAPI requires it saved as decoded base64 string.
773
-		// FIXME: we can do better here
774
-		$matches = explode("\n\n", $tmpEml);
775
-		$base64 = str_replace("\n", "", $matches[1]);
776
-		file_put_contents($outfile, base64_decode($base64));
777
-
778
-		// Empty the body
779
-		mapi_setprops($message, [PR_BODY => ""]);
780
-	}
781
-
782
-	/**
783
-	 * Function which fetches the public certificates for all recipients (TO/CC/BCC) of a message
784
-	 * Always get the certificate of an address which expires last.
785
-	 *
786
-	 * @param object $message Mapi Message Object
787
-	 *
788
-	 * @return array of public certificates
789
-	 */
790
-	public function getPublicKeyForMessage($message) {
791
-		$recipientTable = mapi_message_getrecipienttable($message);
792
-		$recips = mapi_table_queryallrows($recipientTable, [PR_SMTP_ADDRESS, PR_RECIPIENT_TYPE, PR_ADDRTYPE], [RES_OR, [
793
-			[RES_PROPERTY,
794
-				[
795
-					RELOP => RELOP_EQ,
796
-					ULPROPTAG => PR_RECIPIENT_TYPE,
797
-					VALUE => MAPI_BCC,
798
-				],
799
-			],
800
-			[RES_PROPERTY,
801
-				[
802
-					RELOP => RELOP_EQ,
803
-					ULPROPTAG => PR_RECIPIENT_TYPE,
804
-					VALUE => MAPI_CC,
805
-				],
806
-			],
807
-			[RES_PROPERTY,
808
-				[
809
-					RELOP => RELOP_EQ,
810
-					ULPROPTAG => PR_RECIPIENT_TYPE,
811
-					VALUE => MAPI_TO,
812
-				],
813
-			],
814
-		]]);
815
-
816
-		$publicCerts = [];
817
-		$storeCert = '';
818
-		$gabCert = '';
819
-
820
-		foreach ($recips as $recip) {
821
-			$emailAddr = $recip[PR_SMTP_ADDRESS];
822
-			$addrType = $recip[PR_ADDRTYPE];
823
-
824
-			if ($addrType === "ZARAFA" || $addrType === "EX") {
825
-				$user = $this->getGABUser($emailAddr);
826
-				$gabCert = $this->getGABCert($user);
827
-			}
828
-
829
-			$storeCert = $this->getPublicKey($emailAddr);
830
-
831
-			if (!empty($gabCert)) {
832
-				array_push($publicCerts, $gabCert);
833
-			}
834
-			elseif (!empty($storeCert)) {
835
-				array_push($publicCerts, base64_decode($storeCert));
836
-			}
837
-		}
838
-
839
-		return $publicCerts;
840
-	}
841
-
842
-	/**
843
-	 * Retrieves the public certificates stored in the MAPI UserStore and belonging to the
844
-	 * emailAdddress, returns "" if there is no certificate for that user.
845
-	 *
846
-	 * @param {String} emailAddress
847
-	 * @param mixed $emailAddress
848
-	 * @param mixed $multiple
849
-	 *
850
-	 * @return {String} $certificate
851
-	 */
852
-	public function getPublicKey($emailAddress, $multiple = false) {
853
-		$certificates = [];
854
-
855
-		$certs = getMAPICert($this->getStore(), 'WebApp.Security.Public', $emailAddress);
856
-
857
-		if ($certs && count($certs) > 0) {
858
-			foreach ($certs as $cert) {
859
-				$pubkey = mapi_msgstore_openentry($this->getStore(), $cert[PR_ENTRYID]);
860
-				$certificate = "";
861
-				if ($pubkey != false) {
862
-					// retrieve pkcs#11 certificate from body
863
-					$stream = mapi_openproperty($pubkey, PR_BODY, IID_IStream, 0, 0);
864
-					$stat = mapi_stream_stat($stream);
865
-					mapi_stream_seek($stream, 0, STREAM_SEEK_SET);
866
-					for ($i = 0; $i < $stat['cb']; $i += 1024) {
867
-						$certificate .= mapi_stream_read($stream, 1024);
868
-					}
869
-					array_push($certificates, $certificate);
870
-				}
871
-			}
872
-		}
873
-
874
-		return $multiple ? $certificates : ($certificates[0] ?? '');
875
-	}
876
-
877
-	/**
878
-	 * Function which is used to check if there is a public certificate for the provided emailAddress.
879
-	 *
880
-	 * @param {String} emailAddress emailAddres of recipient
881
-	 * @param {Boolean} gabUser is the user of PR_ADDRTYPE == ZARAFA
882
-	 * @param mixed $emailAddress
883
-	 * @param mixed $gabUser
884
-	 *
885
-	 * @return {Boolean} true if public certificate exists
886
-	 */
887
-	public function pubcertExists($emailAddress, $gabUser = false) {
888
-		if ($gabUser) {
889
-			$user = $this->getGABUser($emailAddress);
890
-			$gabCert = $this->getGABCert($user);
891
-			if ($user && !empty($gabCert)) {
892
-				return true;
893
-			}
894
-		}
895
-
896
-		$root = mapi_msgstore_openentry($this->getStore(), null);
897
-		$table = mapi_folder_getcontentstable($root, MAPI_ASSOCIATED);
898
-
899
-		// Restriction for public certificates which are from the recipient of the email, are active and have the correct message_class
900
-		$restrict = [RES_AND, [
901
-			[RES_PROPERTY,
902
-				[
903
-					RELOP => RELOP_EQ,
904
-					ULPROPTAG => PR_MESSAGE_CLASS,
905
-					VALUE => [PR_MESSAGE_CLASS => "WebApp.Security.Public"],
906
-				],
907
-			],
908
-			[RES_PROPERTY,
909
-				[
910
-					RELOP => RELOP_EQ,
911
-					ULPROPTAG => PR_SUBJECT,
912
-					VALUE => [PR_SUBJECT => $emailAddress],
913
-				],
914
-			],
915
-		]];
916
-		mapi_table_restrict($table, $restrict, TBL_BATCH);
917
-		mapi_table_sort($table, [PR_MESSAGE_DELIVERY_TIME => TABLE_SORT_DESCEND], TBL_BATCH);
918
-
919
-		$rows = mapi_table_queryallrows($table, [PR_SUBJECT, PR_ENTRYID, PR_MESSAGE_DELIVERY_TIME, PR_CLIENT_SUBMIT_TIME], $restrict);
920
-
921
-		return !empty($rows);
922
-	}
923
-
924
-	/**
925
-	 * Helper functions which extracts the errors from openssl_error_string()
926
-	 * Example error from openssl_error_string(): error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error
927
-	 * Note that openssl_error_string() returns an error when verifying is successful, this is a bug in PHP https://bugs.php.net/bug.php?id=50713.
928
-	 *
929
-	 * @return {String}
930
-	 */
931
-	public function extract_openssl_error() {
932
-		// TODO: should catch more errors by using while($error = @openssl_error_string())
933
-		$this->openssl_error = @openssl_error_string();
934
-		$openssl_error_code = 0;
935
-		if ($this->openssl_error) {
936
-			$openssl_error_list = explode(":", $this->openssl_error);
937
-			$openssl_error_code = $openssl_error_list[1];
938
-		}
939
-
940
-		return $openssl_error_code;
941
-	}
942
-
943
-	/**
944
-	 * Extract the intermediate certificates from the signed email.
945
-	 * Uses openssl_pkcs7_verify to extract the PKCS#7 blob and then converts the PKCS#7 blob to
946
-	 * X509 certificates using openssl_pkcs7_read.
947
-	 *
948
-	 * @param string $emlfile - the s/mime message
949
-	 *
950
-	 * @return array a list of extracted intermediate certificates
951
-	 */
952
-	public function extractCAs($emlfile) {
953
-		$cas = [];
954
-		$certfile = tempnam(sys_get_temp_dir(), true);
955
-		$outfile = tempnam(sys_get_temp_dir(), true);
956
-		$p7bfile = tempnam(sys_get_temp_dir(), true);
957
-		openssl_pkcs7_verify($emlfile, PKCS7_NOVERIFY, $certfile);
958
-		openssl_pkcs7_verify($emlfile, PKCS7_NOVERIFY, $certfile, [], $certfile, $outfile, $p7bfile);
959
-
960
-		$p7b = file_get_contents($p7bfile);
565
+                    $this->importCertificate($certificate, $publickeyData, 'private');
566
+
567
+                    // Check if the user has a public key in the GAB.
568
+                    $store_props = mapi_getprops($this->getStore(), [PR_USER_ENTRYID]);
569
+                    $user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(), $store_props[PR_USER_ENTRYID]);
570
+
571
+                    $this->importCertificate($publickey, $publickeyData, 'public', true);
572
+                }
573
+            }
574
+
575
+            $returnfiles = [];
576
+            $returnfiles[] = [
577
+                'props' => [
578
+                    'attach_num' => -1,
579
+                    'size' => $data['size'],
580
+                    'name' => $data['name'],
581
+                    'cert' => $saveCert,
582
+                    'cert_warning' => $message,
583
+                ],
584
+            ];
585
+            $data['returnfiles'] = $returnfiles;
586
+        }
587
+    }
588
+
589
+    /**
590
+     * This function handles the 'beforesend' hook which is triggered before sending the email.
591
+     * If the PR_MESSAGE_CLASS is set to a signed email (IPM.Note.SMIME.Multipartsigned), this function
592
+     * will convert the mapi message to RFC822, sign the eml and attach the signed email to the mapi message.
593
+     *
594
+     * @param {mixed} $data from php hook
595
+     */
596
+    public function onBeforeSend(&$data) {
597
+        $store = $data['store'];
598
+        $message = $data['message'];
599
+
600
+        // Retrieve message class
601
+        $props = mapi_getprops($message, [PR_MESSAGE_CLASS, PR_EC_IMAP_EMAIL]);
602
+        $messageClass = $props[PR_MESSAGE_CLASS];
603
+
604
+        if (isset($messageClass) && (stripos($messageClass, 'IPM.Note.SMIME') !== false)) {
605
+            // FIXME: for now return when we are going to sign but we don't have the passphrase set
606
+            // This should never happen sign
607
+            $encryptionStore = \EncryptionStore::getInstance();
608
+            if (($messageClass === 'IPM.Note.SMIME.SignedEncrypt' || $messageClass === 'IPM.Note.SMIME.MultipartSigned') &&
609
+                !$encryptionStore->get('smime')) {
610
+                return;
611
+            }
612
+            // NOTE: setting message class to IPM.Note, so that mapi_inetmapi_imtoinet converts the message to plain email
613
+            // and doesn't fail when handling the attachments.
614
+            mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note']);
615
+            mapi_savechanges($message);
616
+
617
+            // If RFC822-formatted stream is already available in PR_EC_IMAP_EMAIL property
618
+            // than directly use it, generate otherwise.
619
+            if (isset($props[PR_EC_IMAP_EMAIL]) || propIsError(PR_EC_IMAP_EMAIL, $props) == MAPI_E_NOT_ENOUGH_MEMORY) {
620
+                // Stream the message to properly get the PR_EC_IMAP_EMAIL property
621
+                $emlMessageStream = mapi_openproperty($message, PR_EC_IMAP_EMAIL, IID_IStream, 0, 0);
622
+            }
623
+            else {
624
+                // Read the message as RFC822-formatted e-mail stream.
625
+                $emlMessageStream = mapi_inetmapi_imtoinet($GLOBALS['mapisession']->getSession(), $GLOBALS['mapisession']->getAddressbook(), $message, []);
626
+            }
627
+
628
+            // Remove all attachments, since they are stored in the attached signed message
629
+            $atable = mapi_message_getattachmenttable($message);
630
+            $rows = mapi_table_queryallrows($atable, [PR_ATTACH_MIME_TAG, PR_ATTACH_NUM]);
631
+            foreach ($rows as $row) {
632
+                $attnum = $row[PR_ATTACH_NUM];
633
+                mapi_message_deleteattach($message, $attnum);
634
+            }
635
+
636
+            // create temporary files
637
+            $tmpSendEmail = tempnam(sys_get_temp_dir(), true);
638
+            $tmpSendSmimeEmail = tempnam(sys_get_temp_dir(), true);
639
+
640
+            // Save message stream to a file
641
+            $stat = mapi_stream_stat($emlMessageStream);
642
+
643
+            $fhandle = fopen($tmpSendEmail, 'w');
644
+            $buffer = null;
645
+            for ($i = 0; $i < $stat["cb"]; $i += BLOCK_SIZE) {
646
+                // Write stream
647
+                $buffer = mapi_stream_read($emlMessageStream, BLOCK_SIZE);
648
+                fwrite($fhandle, $buffer, strlen($buffer));
649
+            }
650
+            fclose($fhandle);
651
+
652
+            // Create attachment for S/MIME message
653
+            $signedAttach = mapi_message_createattach($message);
654
+            $smimeProps = [
655
+                PR_ATTACH_LONG_FILENAME => 'smime.p7m',
656
+                PR_DISPLAY_NAME => 'smime.p7m',
657
+                PR_ATTACH_METHOD => ATTACH_BY_VALUE,
658
+                PR_ATTACH_MIME_TAG => 'multipart/signed',
659
+                PR_ATTACHMENT_HIDDEN => true,
660
+            ];
661
+
662
+            // Sign then Encrypt email
663
+            switch ($messageClass) {
664
+                case 'IPM.Note.SMIME.SignedEncrypt':
665
+                    $tmpFile = tempnam(sys_get_temp_dir(), true);
666
+                    $this->sign($tmpSendEmail, $tmpFile, $message, $signedAttach, $smimeProps);
667
+                    $this->encrypt($tmpFile, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
668
+                    unlink($tmpFile);
669
+                    break;
670
+
671
+                case 'IPM.Note.SMIME.MultipartSigned':
672
+                    $this->sign($tmpSendEmail, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
673
+                    break;
674
+
675
+                case 'IPM.Note.SMIME':
676
+                    $this->encrypt($tmpSendEmail, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
677
+                    break;
678
+            }
679
+
680
+            // Save the signed message as attachment of the send email
681
+            $stream = mapi_openproperty($signedAttach, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
682
+            $handle = fopen($tmpSendSmimeEmail, 'r');
683
+            while (!feof($handle)) {
684
+                $contents = fread($handle, BLOCK_SIZE);
685
+                mapi_stream_write($stream, $contents);
686
+            }
687
+            fclose($handle);
688
+
689
+            mapi_stream_commit($stream);
690
+
691
+            // remove tmp files
692
+            unlink($tmpSendSmimeEmail);
693
+            unlink($tmpSendEmail);
694
+
695
+            mapi_savechanges($signedAttach);
696
+            mapi_savechanges($message);
697
+        }
698
+    }
699
+
700
+    /**
701
+     * Function to sign an email.
702
+     *
703
+     * @param object $infile       File eml to be encrypted
704
+     * @param object $outfile      File
705
+     * @param object $message      Mapi Message Object
706
+     * @param object $signedAttach
707
+     * @param array  $smimeProps
708
+     */
709
+    public function sign(&$infile, &$outfile, &$message, &$signedAttach, $smimeProps) {
710
+        // Set mesageclass back to IPM.Note.SMIME.MultipartSigned
711
+        mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note.SMIME.MultipartSigned']);
712
+        mapi_setprops($signedAttach, $smimeProps);
713
+
714
+        // Obtain private certificate
715
+        $encryptionStore = EncryptionStore::getInstance();
716
+        // Only the newest one is returned
717
+        $certs = readPrivateCert($this->getStore(), $encryptionStore->get('smime'));
718
+
719
+        // Retrieve intermediate CA's for verification, if available
720
+        if (isset($certs['extracerts'])) {
721
+            $tmpFile = tempnam(sys_get_temp_dir(), true);
722
+            file_put_contents($tmpFile, implode('', $certs['extracerts']));
723
+            $ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED, $tmpFile);
724
+            if (!$ok) {
725
+                Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message with intermediate certificates, openssl error: '%s'", @openssl_error_string()));
726
+            }
727
+            unlink($tmpFile);
728
+        }
729
+        else {
730
+            $ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED);
731
+            if (!$ok) {
732
+                Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message, openssl error: '%s'", @openssl_error_string()));
733
+            }
734
+        }
735
+    }
736
+
737
+    /**
738
+     * Function to encrypt an email.
739
+     *
740
+     * @param object $infile       File eml to be encrypted
741
+     * @param object $outfile      File
742
+     * @param object $message      Mapi Message Object
743
+     * @param object $signedAttach
744
+     * @param array  $smimeProps
745
+     */
746
+    public function encrypt(&$infile, &$outfile, &$message, &$signedAttach, $smimeProps) {
747
+        mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note.SMIME']);
748
+        $smimeProps[PR_ATTACH_MIME_TAG] = "application/pkcs7-mime";
749
+        mapi_setprops($signedAttach, $smimeProps);
750
+
751
+        $publicCerts = $this->getPublicKeyForMessage($message);
752
+        // Always append our own certificate, so that the mail can be decrypted in 'Sent items'
753
+        // Prefer GAB public certificate above MAPI Store certificate.
754
+        $email = $GLOBALS['mapisession']->getSMTPAddress();
755
+        $user = $this->getGABUser($email);
756
+        $cert = $this->getGABCert($user);
757
+        if (empty($cert)) {
758
+            $cert = base64_decode($this->getPublicKey($email));
759
+        }
760
+
761
+        if (!empty($cert)) {
762
+            array_push($publicCerts, $cert);
763
+        }
764
+
765
+        $ok = openssl_pkcs7_encrypt($infile, $outfile, $publicCerts, [], 0, $this->cipher);
766
+        if (!$ok) {
767
+            error_log("[smime] unable to encrypt message, openssl error: " . print_r(@openssl_error_string(), true));
768
+            Log::Write(LOGLEVEL_ERROR, sprintf("[smime] unable to encrypt message, openssl error: '%s'", @openssl_error_string()));
769
+        }
770
+        $tmpEml = file_get_contents($outfile);
771
+
772
+        // Grab the base64 data, since MAPI requires it saved as decoded base64 string.
773
+        // FIXME: we can do better here
774
+        $matches = explode("\n\n", $tmpEml);
775
+        $base64 = str_replace("\n", "", $matches[1]);
776
+        file_put_contents($outfile, base64_decode($base64));
777
+
778
+        // Empty the body
779
+        mapi_setprops($message, [PR_BODY => ""]);
780
+    }
781
+
782
+    /**
783
+     * Function which fetches the public certificates for all recipients (TO/CC/BCC) of a message
784
+     * Always get the certificate of an address which expires last.
785
+     *
786
+     * @param object $message Mapi Message Object
787
+     *
788
+     * @return array of public certificates
789
+     */
790
+    public function getPublicKeyForMessage($message) {
791
+        $recipientTable = mapi_message_getrecipienttable($message);
792
+        $recips = mapi_table_queryallrows($recipientTable, [PR_SMTP_ADDRESS, PR_RECIPIENT_TYPE, PR_ADDRTYPE], [RES_OR, [
793
+            [RES_PROPERTY,
794
+                [
795
+                    RELOP => RELOP_EQ,
796
+                    ULPROPTAG => PR_RECIPIENT_TYPE,
797
+                    VALUE => MAPI_BCC,
798
+                ],
799
+            ],
800
+            [RES_PROPERTY,
801
+                [
802
+                    RELOP => RELOP_EQ,
803
+                    ULPROPTAG => PR_RECIPIENT_TYPE,
804
+                    VALUE => MAPI_CC,
805
+                ],
806
+            ],
807
+            [RES_PROPERTY,
808
+                [
809
+                    RELOP => RELOP_EQ,
810
+                    ULPROPTAG => PR_RECIPIENT_TYPE,
811
+                    VALUE => MAPI_TO,
812
+                ],
813
+            ],
814
+        ]]);
815
+
816
+        $publicCerts = [];
817
+        $storeCert = '';
818
+        $gabCert = '';
819
+
820
+        foreach ($recips as $recip) {
821
+            $emailAddr = $recip[PR_SMTP_ADDRESS];
822
+            $addrType = $recip[PR_ADDRTYPE];
823
+
824
+            if ($addrType === "ZARAFA" || $addrType === "EX") {
825
+                $user = $this->getGABUser($emailAddr);
826
+                $gabCert = $this->getGABCert($user);
827
+            }
828
+
829
+            $storeCert = $this->getPublicKey($emailAddr);
830
+
831
+            if (!empty($gabCert)) {
832
+                array_push($publicCerts, $gabCert);
833
+            }
834
+            elseif (!empty($storeCert)) {
835
+                array_push($publicCerts, base64_decode($storeCert));
836
+            }
837
+        }
838
+
839
+        return $publicCerts;
840
+    }
841
+
842
+    /**
843
+     * Retrieves the public certificates stored in the MAPI UserStore and belonging to the
844
+     * emailAdddress, returns "" if there is no certificate for that user.
845
+     *
846
+     * @param {String} emailAddress
847
+     * @param mixed $emailAddress
848
+     * @param mixed $multiple
849
+     *
850
+     * @return {String} $certificate
851
+     */
852
+    public function getPublicKey($emailAddress, $multiple = false) {
853
+        $certificates = [];
854
+
855
+        $certs = getMAPICert($this->getStore(), 'WebApp.Security.Public', $emailAddress);
856
+
857
+        if ($certs && count($certs) > 0) {
858
+            foreach ($certs as $cert) {
859
+                $pubkey = mapi_msgstore_openentry($this->getStore(), $cert[PR_ENTRYID]);
860
+                $certificate = "";
861
+                if ($pubkey != false) {
862
+                    // retrieve pkcs#11 certificate from body
863
+                    $stream = mapi_openproperty($pubkey, PR_BODY, IID_IStream, 0, 0);
864
+                    $stat = mapi_stream_stat($stream);
865
+                    mapi_stream_seek($stream, 0, STREAM_SEEK_SET);
866
+                    for ($i = 0; $i < $stat['cb']; $i += 1024) {
867
+                        $certificate .= mapi_stream_read($stream, 1024);
868
+                    }
869
+                    array_push($certificates, $certificate);
870
+                }
871
+            }
872
+        }
873
+
874
+        return $multiple ? $certificates : ($certificates[0] ?? '');
875
+    }
876
+
877
+    /**
878
+     * Function which is used to check if there is a public certificate for the provided emailAddress.
879
+     *
880
+     * @param {String} emailAddress emailAddres of recipient
881
+     * @param {Boolean} gabUser is the user of PR_ADDRTYPE == ZARAFA
882
+     * @param mixed $emailAddress
883
+     * @param mixed $gabUser
884
+     *
885
+     * @return {Boolean} true if public certificate exists
886
+     */
887
+    public function pubcertExists($emailAddress, $gabUser = false) {
888
+        if ($gabUser) {
889
+            $user = $this->getGABUser($emailAddress);
890
+            $gabCert = $this->getGABCert($user);
891
+            if ($user && !empty($gabCert)) {
892
+                return true;
893
+            }
894
+        }
895
+
896
+        $root = mapi_msgstore_openentry($this->getStore(), null);
897
+        $table = mapi_folder_getcontentstable($root, MAPI_ASSOCIATED);
898
+
899
+        // Restriction for public certificates which are from the recipient of the email, are active and have the correct message_class
900
+        $restrict = [RES_AND, [
901
+            [RES_PROPERTY,
902
+                [
903
+                    RELOP => RELOP_EQ,
904
+                    ULPROPTAG => PR_MESSAGE_CLASS,
905
+                    VALUE => [PR_MESSAGE_CLASS => "WebApp.Security.Public"],
906
+                ],
907
+            ],
908
+            [RES_PROPERTY,
909
+                [
910
+                    RELOP => RELOP_EQ,
911
+                    ULPROPTAG => PR_SUBJECT,
912
+                    VALUE => [PR_SUBJECT => $emailAddress],
913
+                ],
914
+            ],
915
+        ]];
916
+        mapi_table_restrict($table, $restrict, TBL_BATCH);
917
+        mapi_table_sort($table, [PR_MESSAGE_DELIVERY_TIME => TABLE_SORT_DESCEND], TBL_BATCH);
918
+
919
+        $rows = mapi_table_queryallrows($table, [PR_SUBJECT, PR_ENTRYID, PR_MESSAGE_DELIVERY_TIME, PR_CLIENT_SUBMIT_TIME], $restrict);
920
+
921
+        return !empty($rows);
922
+    }
923
+
924
+    /**
925
+     * Helper functions which extracts the errors from openssl_error_string()
926
+     * Example error from openssl_error_string(): error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error
927
+     * Note that openssl_error_string() returns an error when verifying is successful, this is a bug in PHP https://bugs.php.net/bug.php?id=50713.
928
+     *
929
+     * @return {String}
930
+     */
931
+    public function extract_openssl_error() {
932
+        // TODO: should catch more errors by using while($error = @openssl_error_string())
933
+        $this->openssl_error = @openssl_error_string();
934
+        $openssl_error_code = 0;
935
+        if ($this->openssl_error) {
936
+            $openssl_error_list = explode(":", $this->openssl_error);
937
+            $openssl_error_code = $openssl_error_list[1];
938
+        }
939
+
940
+        return $openssl_error_code;
941
+    }
942
+
943
+    /**
944
+     * Extract the intermediate certificates from the signed email.
945
+     * Uses openssl_pkcs7_verify to extract the PKCS#7 blob and then converts the PKCS#7 blob to
946
+     * X509 certificates using openssl_pkcs7_read.
947
+     *
948
+     * @param string $emlfile - the s/mime message
949
+     *
950
+     * @return array a list of extracted intermediate certificates
951
+     */
952
+    public function extractCAs($emlfile) {
953
+        $cas = [];
954
+        $certfile = tempnam(sys_get_temp_dir(), true);
955
+        $outfile = tempnam(sys_get_temp_dir(), true);
956
+        $p7bfile = tempnam(sys_get_temp_dir(), true);
957
+        openssl_pkcs7_verify($emlfile, PKCS7_NOVERIFY, $certfile);
958
+        openssl_pkcs7_verify($emlfile, PKCS7_NOVERIFY, $certfile, [], $certfile, $outfile, $p7bfile);
959
+
960
+        $p7b = file_get_contents($p7bfile);
961 961
 		
962
-		openssl_pkcs7_read($p7b, $cas);
963
-		unlink($certfile);
964
-		unlink($outfile);
965
-		unlink($p7bfile);
966
-
967
-		return $cas;
968
-	}
969
-
970
-	/**
971
-	 * Imports certificate in the MAPI Root Associated Folder.
972
-	 *
973
-	 * Private key, always insert certificate
974
-	 * Public key, check if we already have one stored
975
-	 *
976
-	 * @param string $cert     certificate body as a string
977
-	 * @param mixed  $certData an array with the parsed certificate data
978
-	 * @param string $type     certificate type, default 'public'
979
-	 * @param bool   $force    force import the certificate even though we have one already stored in the MAPI Store.
980
-	 *                         FIXME: remove $force in the future and move the check for newer certificate in this function.
981
-	 */
982
-	public function importCertificate($cert, $certData, $type = 'public', $force = false) {
983
-		$certEmail = getCertEmail($certData);
984
-		if (!$this->pubcertExists($certEmail) || $force || $type === 'private') {
985
-			$issued_by = "";
986
-			foreach (array_keys($certData['issuer']) as $key) {
987
-				$issued_by .= $key . '=' . $certData['issuer'][$key] . "\n";
988
-			}
989
-
990
-			$root = mapi_msgstore_openentry($this->getStore(), null);
991
-			$assocMessage = mapi_folder_createmessage($root, MAPI_ASSOCIATED);
992
-			// TODO: write these properties down.
993
-			mapi_setprops($assocMessage, [
994
-				PR_SUBJECT => $certEmail,
995
-				PR_MESSAGE_CLASS => $type == 'public' ? 'WebApp.Security.Public' : 'WebApp.Security.Private',
996
-				PR_MESSAGE_DELIVERY_TIME => $certData['validTo_time_t'],
997
-				PR_CLIENT_SUBMIT_TIME => $certData['validFrom_time_t'],
998
-				PR_SENDER_NAME => $certData['serialNumber'], // serial
999
-				PR_SENDER_EMAIL_ADDRESS => $issued_by, // Issuer To
1000
-				PR_SUBJECT_PREFIX => '',
1001
-				PR_RECEIVED_BY_NAME => $this->fingerprint_cert($cert, 'sha1'), // SHA1 Fingerprint
1002
-				PR_INTERNET_MESSAGE_ID => $this->fingerprint_cert($cert), // MD5 FingerPrint
1003
-			]);
1004
-			// Save attachment
1005
-			$msgBody = base64_encode($cert);
1006
-			$stream = mapi_openproperty($assocMessage, PR_BODY, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
1007
-			mapi_stream_setsize($stream, strlen($msgBody));
1008
-			mapi_stream_write($stream, $msgBody);
1009
-			mapi_stream_commit($stream);
1010
-			mapi_message_savechanges($assocMessage);
1011
-		}
1012
-	}
1013
-
1014
-	/**
1015
-	 * Function which returns the fingerprint (hash) of the certificate.
1016
-	 *
1017
-	 * @param {string} $cert certificate body as a string
1018
-	 * @param {string} $hash optional hash algorithm
1019
-	 * @param mixed $body
1020
-	 */
1021
-	public function fingerprint_cert($body, $hash = 'md5') {
1022
-		// TODO: Note for PHP > 5.6 we can use openssl_x509_fingerprint
1023
-		$body = str_replace('-----BEGIN CERTIFICATE-----', '', $body);
1024
-		$body = str_replace('-----END CERTIFICATE-----', '', $body);
1025
-		$body = base64_decode($body);
1026
-
1027
-		if ($hash === 'sha1') {
1028
-			$fingerprint = sha1($body);
1029
-		}
1030
-		else {
1031
-			$fingerprint = md5($body);
1032
-		}
1033
-
1034
-		// Format 1000AB as 10:00:AB
1035
-		return strtoupper(implode(':', str_split($fingerprint, 2)));
1036
-	}
1037
-
1038
-	/**
1039
-	 * Retrieve the GAB User.
1040
-	 *
1041
-	 * FIXME: ideally this would be a public function in grommunio Web.
1042
-	 *
1043
-	 * @param string $email the email address of the user
1044
-	 *
1045
-	 * @return mixed $user boolean if false else MAPIObject
1046
-	 */
1047
-	public function getGABUser($email) {
1048
-		$addrbook = $GLOBALS["mapisession"]->getAddressbook();
1049
-		$userArr = [[PR_DISPLAY_NAME => $email]];
1050
-		$user = false;
1051
-
1052
-		try {
1053
-			$user = mapi_ab_resolvename($addrbook, $userArr, EMS_AB_ADDRESS_LOOKUP);
1054
-			$user = mapi_ab_openentry($addrbook, $user[0][PR_ENTRYID]);
1055
-		}
1056
-		catch (MAPIException $e) {
1057
-			$e->setHandled();
1058
-		}
1059
-
1060
-		return $user;
1061
-	}
1062
-
1063
-	/**
1064
-	 * Retrieve the PR_EMS_AB_TAGGED_X509_CERT.
1065
-	 *
1066
-	 * @param MAPIObject $user the GAB user
1067
-	 *
1068
-	 * @return string $cert the certificate, empty if not found
1069
-	 */
1070
-	public function getGABCert($user) {
1071
-		$cert = '';
1072
-		$userCertArray = mapi_getprops($user, [PR_EMS_AB_TAGGED_X509_CERT]);
1073
-		if (isset($userCertArray[PR_EMS_AB_TAGGED_X509_CERT])) {
1074
-			$cert = der2pem($userCertArray[PR_EMS_AB_TAGGED_X509_CERT][0]);
1075
-		}
1076
-
1077
-		return $cert;
1078
-	}
1079
-
1080
-	/**
1081
-	 * Called when the core Settings class is initialized and ready to accept sysadmin default
1082
-	 * settings. Registers the sysadmin defaults for the example plugin.
1083
-	 *
1084
-	 * @param {mixed} $data Reference to the data of the triggered hook
1085
-	 */
1086
-	public function onBeforeSettingsInit(&$data) {
1087
-		$data['settingsObj']->addSysAdminDefaults([
1088
-			'zarafa' => [
1089
-				'v1' => [
1090
-					'plugins' => [
1091
-						'smime' => [
1092
-							'enable' => defined('PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME') && PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME,
1093
-							'passphrase_cache' => defined('PLUGIN_SMIME_PASSPHRASE_REMEMBER_BROWSER') && PLUGIN_SMIME_PASSPHRASE_REMEMBER_BROWSER,
1094
-						],
1095
-					],
1096
-				],
1097
-			],
1098
-		]);
1099
-	}
1100
-
1101
-	/**
1102
-	 * Get sender structure of the MAPI Message.
1103
-	 *
1104
-	 * @param mapimessage $mapiMessage MAPI Message resource from which we need to get the sender
1105
-	 *
1106
-	 * @return array with properties
1107
-	 */
1108
-	public function getSenderAddress($mapiMessage) {
1109
-		if (!method_exists($GLOBALS['operations'], 'getSenderAddress')) {
1110
-			$messageProps = mapi_getprops($mapiMessage, [PR_SENT_REPRESENTING_ENTRYID, PR_SENDER_ENTRYID]);
1111
-			$senderEntryID = isset($messageProps[PR_SENT_REPRESENTING_ENTRYID]) ? $messageProps[PR_SENT_REPRESENTING_ENTRYID] : $messageProps[PR_SENDER_ENTRYID];
1112
-
1113
-			try {
1114
-				$senderUser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $senderEntryID);
1115
-				if ($senderUser) {
1116
-					$userprops = mapi_getprops($senderUser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_OBJECT_TYPE, PR_RECIPIENT_TYPE, PR_DISPLAY_TYPE, PR_DISPLAY_TYPE_EX, PR_ENTRYID]);
1117
-
1118
-					$senderStructure = [];
1119
-					$senderStructure["props"]['entryid'] = bin2hex($userprops[PR_ENTRYID]);
1120
-					$senderStructure["props"]['display_name'] = isset($userprops[PR_DISPLAY_NAME]) ? $userprops[PR_DISPLAY_NAME] : '';
1121
-					$senderStructure["props"]['email_address'] = isset($userprops[PR_EMAIL_ADDRESS]) ? $userprops[PR_EMAIL_ADDRESS] : '';
1122
-					$senderStructure["props"]['smtp_address'] = isset($userprops[PR_SMTP_ADDRESS]) ? $userprops[PR_SMTP_ADDRESS] : '';
1123
-					$senderStructure["props"]['address_type'] = isset($userprops[PR_ADDRTYPE]) ? $userprops[PR_ADDRTYPE] : '';
1124
-					$senderStructure["props"]['object_type'] = $userprops[PR_OBJECT_TYPE];
1125
-					$senderStructure["props"]['recipient_type'] = MAPI_TO;
1126
-					$senderStructure["props"]['display_type'] = isset($userprops[PR_DISPLAY_TYPE]) ? $userprops[PR_DISPLAY_TYPE] : MAPI_MAILUSER;
1127
-					$senderStructure["props"]['display_type_ex'] = isset($userprops[PR_DISPLAY_TYPE_EX]) ? $userprops[PR_DISPLAY_TYPE_EX] : MAPI_MAILUSER;
1128
-				}
1129
-			}
1130
-			catch (MAPIException $e) {
1131
-				Log::write(LOGLEVEL_ERROR, sprintf("%s %s", $e, $userProps[PR_SENT_REPRESENTING_NAME]));
1132
-			}
1133
-
1134
-			return $senderStructure;
1135
-		}
1136
-
1137
-		return $GLOBALS["operations"]->getSenderAddress($mapiMessage);
1138
-	}
962
+        openssl_pkcs7_read($p7b, $cas);
963
+        unlink($certfile);
964
+        unlink($outfile);
965
+        unlink($p7bfile);
966
+
967
+        return $cas;
968
+    }
969
+
970
+    /**
971
+     * Imports certificate in the MAPI Root Associated Folder.
972
+     *
973
+     * Private key, always insert certificate
974
+     * Public key, check if we already have one stored
975
+     *
976
+     * @param string $cert     certificate body as a string
977
+     * @param mixed  $certData an array with the parsed certificate data
978
+     * @param string $type     certificate type, default 'public'
979
+     * @param bool   $force    force import the certificate even though we have one already stored in the MAPI Store.
980
+     *                         FIXME: remove $force in the future and move the check for newer certificate in this function.
981
+     */
982
+    public function importCertificate($cert, $certData, $type = 'public', $force = false) {
983
+        $certEmail = getCertEmail($certData);
984
+        if (!$this->pubcertExists($certEmail) || $force || $type === 'private') {
985
+            $issued_by = "";
986
+            foreach (array_keys($certData['issuer']) as $key) {
987
+                $issued_by .= $key . '=' . $certData['issuer'][$key] . "\n";
988
+            }
989
+
990
+            $root = mapi_msgstore_openentry($this->getStore(), null);
991
+            $assocMessage = mapi_folder_createmessage($root, MAPI_ASSOCIATED);
992
+            // TODO: write these properties down.
993
+            mapi_setprops($assocMessage, [
994
+                PR_SUBJECT => $certEmail,
995
+                PR_MESSAGE_CLASS => $type == 'public' ? 'WebApp.Security.Public' : 'WebApp.Security.Private',
996
+                PR_MESSAGE_DELIVERY_TIME => $certData['validTo_time_t'],
997
+                PR_CLIENT_SUBMIT_TIME => $certData['validFrom_time_t'],
998
+                PR_SENDER_NAME => $certData['serialNumber'], // serial
999
+                PR_SENDER_EMAIL_ADDRESS => $issued_by, // Issuer To
1000
+                PR_SUBJECT_PREFIX => '',
1001
+                PR_RECEIVED_BY_NAME => $this->fingerprint_cert($cert, 'sha1'), // SHA1 Fingerprint
1002
+                PR_INTERNET_MESSAGE_ID => $this->fingerprint_cert($cert), // MD5 FingerPrint
1003
+            ]);
1004
+            // Save attachment
1005
+            $msgBody = base64_encode($cert);
1006
+            $stream = mapi_openproperty($assocMessage, PR_BODY, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
1007
+            mapi_stream_setsize($stream, strlen($msgBody));
1008
+            mapi_stream_write($stream, $msgBody);
1009
+            mapi_stream_commit($stream);
1010
+            mapi_message_savechanges($assocMessage);
1011
+        }
1012
+    }
1013
+
1014
+    /**
1015
+     * Function which returns the fingerprint (hash) of the certificate.
1016
+     *
1017
+     * @param {string} $cert certificate body as a string
1018
+     * @param {string} $hash optional hash algorithm
1019
+     * @param mixed $body
1020
+     */
1021
+    public function fingerprint_cert($body, $hash = 'md5') {
1022
+        // TODO: Note for PHP > 5.6 we can use openssl_x509_fingerprint
1023
+        $body = str_replace('-----BEGIN CERTIFICATE-----', '', $body);
1024
+        $body = str_replace('-----END CERTIFICATE-----', '', $body);
1025
+        $body = base64_decode($body);
1026
+
1027
+        if ($hash === 'sha1') {
1028
+            $fingerprint = sha1($body);
1029
+        }
1030
+        else {
1031
+            $fingerprint = md5($body);
1032
+        }
1033
+
1034
+        // Format 1000AB as 10:00:AB
1035
+        return strtoupper(implode(':', str_split($fingerprint, 2)));
1036
+    }
1037
+
1038
+    /**
1039
+     * Retrieve the GAB User.
1040
+     *
1041
+     * FIXME: ideally this would be a public function in grommunio Web.
1042
+     *
1043
+     * @param string $email the email address of the user
1044
+     *
1045
+     * @return mixed $user boolean if false else MAPIObject
1046
+     */
1047
+    public function getGABUser($email) {
1048
+        $addrbook = $GLOBALS["mapisession"]->getAddressbook();
1049
+        $userArr = [[PR_DISPLAY_NAME => $email]];
1050
+        $user = false;
1051
+
1052
+        try {
1053
+            $user = mapi_ab_resolvename($addrbook, $userArr, EMS_AB_ADDRESS_LOOKUP);
1054
+            $user = mapi_ab_openentry($addrbook, $user[0][PR_ENTRYID]);
1055
+        }
1056
+        catch (MAPIException $e) {
1057
+            $e->setHandled();
1058
+        }
1059
+
1060
+        return $user;
1061
+    }
1062
+
1063
+    /**
1064
+     * Retrieve the PR_EMS_AB_TAGGED_X509_CERT.
1065
+     *
1066
+     * @param MAPIObject $user the GAB user
1067
+     *
1068
+     * @return string $cert the certificate, empty if not found
1069
+     */
1070
+    public function getGABCert($user) {
1071
+        $cert = '';
1072
+        $userCertArray = mapi_getprops($user, [PR_EMS_AB_TAGGED_X509_CERT]);
1073
+        if (isset($userCertArray[PR_EMS_AB_TAGGED_X509_CERT])) {
1074
+            $cert = der2pem($userCertArray[PR_EMS_AB_TAGGED_X509_CERT][0]);
1075
+        }
1076
+
1077
+        return $cert;
1078
+    }
1079
+
1080
+    /**
1081
+     * Called when the core Settings class is initialized and ready to accept sysadmin default
1082
+     * settings. Registers the sysadmin defaults for the example plugin.
1083
+     *
1084
+     * @param {mixed} $data Reference to the data of the triggered hook
1085
+     */
1086
+    public function onBeforeSettingsInit(&$data) {
1087
+        $data['settingsObj']->addSysAdminDefaults([
1088
+            'zarafa' => [
1089
+                'v1' => [
1090
+                    'plugins' => [
1091
+                        'smime' => [
1092
+                            'enable' => defined('PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME') && PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME,
1093
+                            'passphrase_cache' => defined('PLUGIN_SMIME_PASSPHRASE_REMEMBER_BROWSER') && PLUGIN_SMIME_PASSPHRASE_REMEMBER_BROWSER,
1094
+                        ],
1095
+                    ],
1096
+                ],
1097
+            ],
1098
+        ]);
1099
+    }
1100
+
1101
+    /**
1102
+     * Get sender structure of the MAPI Message.
1103
+     *
1104
+     * @param mapimessage $mapiMessage MAPI Message resource from which we need to get the sender
1105
+     *
1106
+     * @return array with properties
1107
+     */
1108
+    public function getSenderAddress($mapiMessage) {
1109
+        if (!method_exists($GLOBALS['operations'], 'getSenderAddress')) {
1110
+            $messageProps = mapi_getprops($mapiMessage, [PR_SENT_REPRESENTING_ENTRYID, PR_SENDER_ENTRYID]);
1111
+            $senderEntryID = isset($messageProps[PR_SENT_REPRESENTING_ENTRYID]) ? $messageProps[PR_SENT_REPRESENTING_ENTRYID] : $messageProps[PR_SENDER_ENTRYID];
1112
+
1113
+            try {
1114
+                $senderUser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $senderEntryID);
1115
+                if ($senderUser) {
1116
+                    $userprops = mapi_getprops($senderUser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_OBJECT_TYPE, PR_RECIPIENT_TYPE, PR_DISPLAY_TYPE, PR_DISPLAY_TYPE_EX, PR_ENTRYID]);
1117
+
1118
+                    $senderStructure = [];
1119
+                    $senderStructure["props"]['entryid'] = bin2hex($userprops[PR_ENTRYID]);
1120
+                    $senderStructure["props"]['display_name'] = isset($userprops[PR_DISPLAY_NAME]) ? $userprops[PR_DISPLAY_NAME] : '';
1121
+                    $senderStructure["props"]['email_address'] = isset($userprops[PR_EMAIL_ADDRESS]) ? $userprops[PR_EMAIL_ADDRESS] : '';
1122
+                    $senderStructure["props"]['smtp_address'] = isset($userprops[PR_SMTP_ADDRESS]) ? $userprops[PR_SMTP_ADDRESS] : '';
1123
+                    $senderStructure["props"]['address_type'] = isset($userprops[PR_ADDRTYPE]) ? $userprops[PR_ADDRTYPE] : '';
1124
+                    $senderStructure["props"]['object_type'] = $userprops[PR_OBJECT_TYPE];
1125
+                    $senderStructure["props"]['recipient_type'] = MAPI_TO;
1126
+                    $senderStructure["props"]['display_type'] = isset($userprops[PR_DISPLAY_TYPE]) ? $userprops[PR_DISPLAY_TYPE] : MAPI_MAILUSER;
1127
+                    $senderStructure["props"]['display_type_ex'] = isset($userprops[PR_DISPLAY_TYPE_EX]) ? $userprops[PR_DISPLAY_TYPE_EX] : MAPI_MAILUSER;
1128
+                }
1129
+            }
1130
+            catch (MAPIException $e) {
1131
+                Log::write(LOGLEVEL_ERROR, sprintf("%s %s", $e, $userProps[PR_SENT_REPRESENTING_NAME]));
1132
+            }
1133
+
1134
+            return $senderStructure;
1135
+        }
1136
+
1137
+        return $GLOBALS["operations"]->getSenderAddress($mapiMessage);
1138
+    }
1139 1139
 }
Please login to merge, or discard this patch.
Braces   +22 added lines, -44 removed lines patch added patch discarded remove patch
@@ -64,8 +64,7 @@  discard block
 block discarded – undo
64 64
 
65 65
 		if (version_compare(phpversion(), '5.4', '<')) {
66 66
 			$this->cipher = OPENSSL_CIPHER_3DES;
67
-		}
68
-		else {
67
+		} else {
69 68
 			$this->cipher = PLUGIN_SMIME_CIPHER;
70 69
 		}
71 70
 	}
@@ -188,8 +187,7 @@  discard block
 block discarded – undo
188 187
 
189 188
 		if (array_filter($missingCerts, "missingMyself") === []) {
190 189
 			$errorMsg = _('Missing public certificates for the following recipients: ') . implode(', ', $missingCerts) . _('. Please contact your system administrator for details');
191
-		}
192
-		else {
190
+		} else {
193 191
 			$errorMsg = _("Your public certificate is not installed. Without this certificate, you will not be able to read encrypted messages you have sent to others.");
194 192
 		}
195 193
 
@@ -225,8 +223,7 @@  discard block
 block discarded – undo
225 223
 					// Put empty string into file? dafuq?
226 224
 					file_put_contents($tmpUserCert, $userCert);
227 225
 				}
228
-			}
229
-			catch (MAPIException $e) {
226
+			} catch (MAPIException $e) {
230 227
 				$msg = "[smime] Unable to open PR_SENT_REPRESENTING_ENTRYID. Maybe %s was does not exists or deleted from server.";
231 228
 				Log::write(LOGLEVEL_ERROR, sprintf($msg, $userProps[PR_SENT_REPRESENTING_NAME]));
232 229
 				error_log("[smime] Unable to open PR_SENT_REPRESENTING_NAME: " . print_r($userProps[PR_SENT_REPRESENTING_NAME], true));
@@ -242,8 +239,7 @@  discard block
 block discarded – undo
242 239
 			$senderAddressArray = $senderAddressArray['props'];
243 240
 			if ($senderAddressArray['address_type'] === 'SMTP') {
244 241
 				$emailAddr = $senderAddressArray['email_address'];
245
-			}
246
-			else {
242
+			} else {
247 243
 				$emailAddr = $senderAddressArray['smtp_address'];
248 244
 			}
249 245
 
@@ -253,8 +249,7 @@  discard block
 block discarded – undo
253 249
 			if (!$emailAddr) {
254 250
 				if (!empty($userProps[PR_SENT_REPRESENTING_NAME])) {
255 251
 					$emailAddr = $userProps[PR_SENT_REPRESENTING_NAME];
256
-				}
257
-				else {
252
+				} else {
258 253
 					$searchKeys = mapi_getprops($message, [PR_SEARCH_KEY, PR_SENT_REPRESENTING_SEARCH_KEY]);
259 254
 					$searchKey = $searchKeys[PR_SEARCH_KEY] ?? $searchKeys[PR_SENT_REPRESENTING_SEARCH_KEY];
260 255
 					if ($searchKey) {
@@ -307,16 +302,14 @@  discard block
 block discarded – undo
307 302
 						$importMessageCert !== false) {
308 303
 						// Redundant
309 304
 						$importMessageCert = true;
310
-					}
311
-					else {
305
+					} else {
312 306
 						$importMessageCert = false;
313 307
 						verifyOCSP($userCert, $caCerts, $this->message);
314 308
 						break;
315 309
 					}
316 310
 				}
317 311
 			}
318
-		}
319
-		else {
312
+		} else {
320 313
 			// Works. Just leave it.
321 314
 			$signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOSIGS, $outcert, explode(';', PLUGIN_SMIME_CACERTS));
322 315
 			$openssl_error_code = $this->extract_openssl_error();
@@ -333,8 +326,7 @@  discard block
 block discarded – undo
333 326
 				}
334 327
 				// We don't have a certificate from the MAPI UserStore or LDAP, so we will set $userCert to $importCert
335 328
 				// so that we can verify the message according to the be imported certificate.
336
-			}
337
-			else { // No pubkey
329
+			} else { // No pubkey
338 330
 				$importMessageCert = false;
339 331
 				Log::write(LOGLEVEL_INFO, sprintf("[smime] Unable to verify message without public key, openssl error: '%s'", $this->openssl_error));
340 332
 				$this->message['success'] = SMIME_STATUS_FAIL;
@@ -404,8 +396,7 @@  discard block
 block discarded – undo
404 396
 						break;
405 397
 					}
406 398
 				}
407
-			}
408
-			else {
399
+			} else {
409 400
 				$decryptStatus = openssl_pkcs7_decrypt($tmpFile, $tmpDecrypted, $certs['cert'], [$certs['pkey'], $pass]);
410 401
 			}
411 402
 
@@ -437,19 +428,16 @@  discard block
 block discarded – undo
437 428
 			if (strpos($content, 'multipart/signed') !== false || strpos($content, 'signed-data') !== false) {
438 429
 				$this->message['type'] = 'encryptsigned';
439 430
 				$this->verifyMessage($data['message'], $content);
440
-			}
441
-			elseif ($decryptStatus) {
431
+			} elseif ($decryptStatus) {
442 432
 				$this->message['info'] = SMIME_DECRYPT_SUCCESS;
443 433
 				$this->message['success'] = SMIME_STATUS_SUCCESS;
444
-			}
445
-			elseif ($this->extract_openssl_error() === OPENSSL_RECIPIENT_CERTIFICATE_MISMATCH) {
434
+			} elseif ($this->extract_openssl_error() === OPENSSL_RECIPIENT_CERTIFICATE_MISMATCH) {
446 435
 				error_log("[smime] Error when decrypting email, openssl error: " . print_r($this->openssl_error, true));
447 436
 				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Error when decrypting email, openssl error: '%s'", $this->openssl_error));
448 437
 				$this->message['info'] = SMIME_DECRYPT_CERT_MISMATCH;
449 438
 				$this->message['success'] = SMIME_STATUS_FAIL;
450 439
 			}
451
-		}
452
-		else {
440
+		} else {
453 441
 			$this->message['info'] = SMIME_UNLOCK_CERT;
454 442
 		}
455 443
 
@@ -482,18 +470,15 @@  discard block
 block discarded – undo
482 470
 			$this->message['info'] = SMIME_ERROR;
483 471
 			$this->message['success'] = SMIME_STATUS_FAIL;
484 472
 		// Verification was successful
485
-		}
486
-		elseif ($openssl_return) {
473
+		} elseif ($openssl_return) {
487 474
 			$this->message['info'] = SMIME_SUCCESS;
488 475
 			$this->message['success'] = SMIME_STATUS_SUCCESS;
489 476
 		// Verification was not successful, display extra information.
490
-		}
491
-		else {
477
+		} else {
492 478
 			$this->message['success'] = SMIME_STATUS_FAIL;
493 479
 			if ($openssl_errors === OPENSSL_CA_VERIFY_FAIL) {
494 480
 				$this->message['info'] = SMIME_CA;
495
-			}
496
-			else { // Catch general errors
481
+			} else { // Catch general errors
497 482
 				$this->message['info'] = SMIME_ERROR;
498 483
 			}
499 484
 		}
@@ -542,8 +527,7 @@  discard block
 block discarded – undo
542 527
 				// TODO: update to serialNumber check
543 528
 				if ($certMessage && $certMessage[0][PR_MESSAGE_DELIVERY_TIME] == $publickeyData['validTo_time_t']) {
544 529
 					$message = _('Certificate is already stored on the server');
545
-				}
546
-				else {
530
+				} else {
547 531
 					$saveCert = true;
548 532
 					$root = mapi_msgstore_openentry($this->getStore(), null);
549 533
 					// Remove old certificate
@@ -619,8 +603,7 @@  discard block
 block discarded – undo
619 603
 			if (isset($props[PR_EC_IMAP_EMAIL]) || propIsError(PR_EC_IMAP_EMAIL, $props) == MAPI_E_NOT_ENOUGH_MEMORY) {
620 604
 				// Stream the message to properly get the PR_EC_IMAP_EMAIL property
621 605
 				$emlMessageStream = mapi_openproperty($message, PR_EC_IMAP_EMAIL, IID_IStream, 0, 0);
622
-			}
623
-			else {
606
+			} else {
624 607
 				// Read the message as RFC822-formatted e-mail stream.
625 608
 				$emlMessageStream = mapi_inetmapi_imtoinet($GLOBALS['mapisession']->getSession(), $GLOBALS['mapisession']->getAddressbook(), $message, []);
626 609
 			}
@@ -725,8 +708,7 @@  discard block
 block discarded – undo
725 708
 				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message with intermediate certificates, openssl error: '%s'", @openssl_error_string()));
726 709
 			}
727 710
 			unlink($tmpFile);
728
-		}
729
-		else {
711
+		} else {
730 712
 			$ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED);
731 713
 			if (!$ok) {
732 714
 				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message, openssl error: '%s'", @openssl_error_string()));
@@ -830,8 +812,7 @@  discard block
 block discarded – undo
830 812
 
831 813
 			if (!empty($gabCert)) {
832 814
 				array_push($publicCerts, $gabCert);
833
-			}
834
-			elseif (!empty($storeCert)) {
815
+			} elseif (!empty($storeCert)) {
835 816
 				array_push($publicCerts, base64_decode($storeCert));
836 817
 			}
837 818
 		}
@@ -1026,8 +1007,7 @@  discard block
 block discarded – undo
1026 1007
 
1027 1008
 		if ($hash === 'sha1') {
1028 1009
 			$fingerprint = sha1($body);
1029
-		}
1030
-		else {
1010
+		} else {
1031 1011
 			$fingerprint = md5($body);
1032 1012
 		}
1033 1013
 
@@ -1052,8 +1032,7 @@  discard block
 block discarded – undo
1052 1032
 		try {
1053 1033
 			$user = mapi_ab_resolvename($addrbook, $userArr, EMS_AB_ADDRESS_LOOKUP);
1054 1034
 			$user = mapi_ab_openentry($addrbook, $user[0][PR_ENTRYID]);
1055
-		}
1056
-		catch (MAPIException $e) {
1035
+		} catch (MAPIException $e) {
1057 1036
 			$e->setHandled();
1058 1037
 		}
1059 1038
 
@@ -1126,8 +1105,7 @@  discard block
 block discarded – undo
1126 1105
 					$senderStructure["props"]['display_type'] = isset($userprops[PR_DISPLAY_TYPE]) ? $userprops[PR_DISPLAY_TYPE] : MAPI_MAILUSER;
1127 1106
 					$senderStructure["props"]['display_type_ex'] = isset($userprops[PR_DISPLAY_TYPE_EX]) ? $userprops[PR_DISPLAY_TYPE_EX] : MAPI_MAILUSER;
1128 1107
 				}
1129
-			}
1130
-			catch (MAPIException $e) {
1108
+			} catch (MAPIException $e) {
1131 1109
 				Log::write(LOGLEVEL_ERROR, sprintf("%s %s", $e, $userProps[PR_SENT_REPRESENTING_NAME]));
1132 1110
 			}
1133 1111
 
Please login to merge, or discard this patch.