CreateMailItemModule::save()   C
last analyzed

Complexity

Conditions 15
Paths 105

Size

Total Lines 55
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 15
eloc 35
nc 105
nop 4
dl 0
loc 55
rs 5.875
c 2
b 1
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Create Mail ItemModule
5
 * Module which opens, creates, saves and deletes an item. It
6
 * extends the Module class.
7
 */
8
class CreateMailItemModule extends ItemModule {
9
	/**
10
	 * Constructor.
11
	 *
12
	 * @param int   $id   unique id
13
	 * @param array $data list of all actions
14
	 */
15
	public function __construct($id, $data) {
16
		parent::__construct($id, $data);
17
18
		$this->properties = $GLOBALS['properties']->getMailProperties();
19
		$useHtmlPreview = $GLOBALS['settings']->get('zarafa/v1/contexts/mail/use_html_email_preview', USE_HTML_EMAIL_PREVIEW);
20
		$this->plaintext = !$useHtmlPreview;
21
	}
22
23
	/**
24
	 * Function which saves and/or sends an item.
25
	 *
26
	 * @param object $store         MAPI Message Store Object
27
	 * @param string $parententryid parent entryid of the message
28
	 * @param string $entryid       entryid of the message
29
	 * @param array  $action        the action data, sent by the client
30
	 */
31
	#[Override]
32
	public function save($store, $parententryid, $entryid, $action) {
33
		$messageProps = [];
34
		$result = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
35
36
		$store = $this->resolveStore($store, $action);
37
		if (!$store) {
0 ignored issues
show
introduced by
$store is of type object, thus it always evaluated to true.
Loading history...
38
			return;
39
		}
40
41
		$parententryid = $this->resolveParentEntryId($store, $parententryid, $action);
42
43
		$attachments = !empty($action['attachments']) ? $action['attachments'] : [];
44
		$recipients = !empty($action['recipients']) ? $action['recipients'] : [];
45
46
		$result = $this->applyMessageFlags($store, $entryid, $action, $messageProps);
47
		$send = $this->shouldSendMessage($action);
48
		$saveChanges = $this->shouldPersistMessage($send, $action, $attachments, $recipients);
49
50
		if ($saveChanges) {
51
			$copyContext = $this->createCopyContext($store, $action, $send);
52
			$store = $copyContext['store'];
53
			$copyFromMessage = $copyContext['copyFromMessage'];
54
			$copyAttachments = $copyContext['copyAttachments'];
55
			$copyInlineAttachmentsOnly = $copyContext['copyInlineAttachmentsOnly'];
56
57
			if ($send) {
58
				$sendOutcome = $this->handleSend($store, $entryid, $parententryid, $action, $recipients, $messageProps, $copyFromMessage, $copyAttachments, $copyInlineAttachmentsOnly);
59
				if ($sendOutcome['aborted']) {
60
					return;
61
				}
62
				$result = $sendOutcome['result'];
63
			}
64
			else {
65
				$draftOutcome = $this->handleDraftSave($store, $entryid, $parententryid, $action, $messageProps, $copyFromMessage, $copyAttachments, $copyInlineAttachmentsOnly);
66
				if ($draftOutcome['aborted']) {
67
					return;
68
				}
69
				$result = $draftOutcome['result'];
70
			}
71
		}
72
73
		if ($result === false && isset($action['message_action']['soft_delete'])) {
74
			$result = true;
75
		}
76
77
		if ($result && !$send && isset($messageProps[PR_PARENT_ENTRYID])) {
78
			$GLOBALS['bus']->notify(bin2hex($messageProps[PR_PARENT_ENTRYID]), TABLE_SAVE, $messageProps);
79
		}
80
81
		if ($send) {
82
			$this->addActionData('update', ['item' => Conversion::mapMAPI2XML($this->properties, $messageProps)]);
83
		}
84
85
		$this->sendFeedback($result ? true : false, [], true);
86
	}
87
88
	/**
89
	 * Resolve the message store that should be used for the save operation.
90
	 *
91
	 * @param mixed $store
92
	 *
93
	 * @return mixed
94
	 */
95
	private function resolveStore($store, array $action) {
0 ignored issues
show
Unused Code introduced by
The parameter $action is not used and could be removed. ( Ignorable by Annotation )

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

95
	private function resolveStore($store, /** @scrutinizer ignore-unused */ array $action) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
96
		if ($store) {
97
			return $store;
98
		}
99
100
		return $GLOBALS['mapisession']->getDefaultMessageStore();
101
	}
102
103
	/**
104
	 * Resolve parent entry id based on provided data or defaults.
105
	 *
106
	 * @param mixed  $store
107
	 * @param string $parententryid
108
	 *
109
	 * @return string
110
	 */
111
	private function resolveParentEntryId($store, $parententryid, array $action) {
112
		if ($parententryid) {
113
			return $parententryid;
114
		}
115
116
		$messageClass = $action['props']['message_class'] ?? '';
117
118
		return $this->getDefaultFolderEntryID($store, $messageClass);
119
	}
120
121
	/**
122
	 * Apply message flag updates if requested by the client.
123
	 *
124
	 * @param mixed $store
125
	 * @param mixed $entryid
126
	 *
127
	 * @return bool
128
	 */
129
	private function applyMessageFlags($store, $entryid, array &$action, array &$messageProps) {
130
		if (!isset($action['props']['message_flags']) || !$entryid) {
131
			return false;
132
		}
133
134
		$msgAction = $action['message_action'] ?? false;
135
		$result = $GLOBALS['operations']->setMessageFlag($store, $entryid, $action['props']['message_flags'], $msgAction, $messageProps);
136
137
		unset($action['props']['message_flags']);
138
139
		return (bool) $result;
140
	}
141
142
	/**
143
	 * Determine whether the current action requests sending the message.
144
	 *
145
	 * @return bool
146
	 */
147
	private function shouldSendMessage(array $action) {
148
		return isset($action['message_action']['send']) ? (bool) $action['message_action']['send'] : false;
149
	}
150
151
	/**
152
	 * Decide if the message requires saving based on provided data.
153
	 *
154
	 * @param bool $send
155
	 *
156
	 * @return bool
157
	 */
158
	private function shouldPersistMessage($send, array $action, array $attachments, array $recipients) {
159
		if ($send) {
160
			return true;
161
		}
162
163
		if (!empty($action['props'])) {
164
			return true;
165
		}
166
167
		if ($this->hasAttachmentChanges($attachments)) {
168
			return true;
169
		}
170
171
		return !empty($recipients);
172
	}
173
174
	/**
175
	 * Check if attachment updates are pending in the attachment state.
176
	 *
177
	 * @return bool
178
	 */
179
	private function hasAttachmentChanges(array $attachments) {
180
		if (!isset($attachments['dialog_attachments'])) {
181
			return false;
182
		}
183
184
		$attachmentState = new AttachmentState();
185
		$attachmentState->open();
186
		$hasChanges = $attachmentState->isChangesPending($attachments['dialog_attachments']);
187
		$attachmentState->close();
188
189
		return $hasChanges;
190
	}
191
192
	/**
193
	 * Prepare context data when the new message should copy content from another message.
194
	 *
195
	 * @param mixed $store
196
	 * @param bool  $send
197
	 *
198
	 * @return array
199
	 */
200
	private function createCopyContext($store, array $action, $send) {
201
		$context = [
202
			'store' => $store,
203
			'copyAttachments' => false,
204
			'copyFromMessage' => false,
205
			'copyInlineAttachmentsOnly' => false,
206
		];
207
208
		if (isset($action['message_action']['action_type'])) {
209
			$actionType = $action['message_action']['action_type'];
210
			$requiresCopy = in_array($actionType, ['reply', 'replyall', 'forward', 'edit_as_new'], true);
211
212
			if ($requiresCopy) {
213
				$copyFromMessageId = (string) ($action['message_action']['source_entryid'] ?? '');
214
				$copyFromStoreId = (string) ($action['message_action']['source_store_entryid'] ?? '');
215
				$copyFromAttachNum = !empty($action['message_action']['source_attach_num']) ? $action['message_action']['source_attach_num'] : false;
216
217
				$copyStoreBinary = $copyFromStoreId !== '' ? $this->hexToBinOrFalse($copyFromStoreId) : false;
218
				$copyMessageBinary = $copyFromMessageId !== '' ? $this->hexToBinOrFalse($copyFromMessageId) : false;
219
				$copyFromStore = $copyStoreBinary !== false ? $GLOBALS['mapisession']->openMessageStore($copyStoreBinary) : false;
220
				$copyFromMessage = false;
221
222
				if ($copyFromStore && $copyMessageBinary !== false) {
223
					$copyFromMessage = $GLOBALS['operations']->openMessage($copyFromStore, $copyMessageBinary, $copyFromAttachNum);
224
				}
225
226
				if ($copyFromStore && $send) {
227
					$context['store'] = $copyFromStore;
228
				}
229
230
				if ($copyFromStore && $copyFromMessage) {
231
					parse_smime($copyFromStore, $copyFromMessage);
232
				}
233
234
				$context['copyAttachments'] = true;
235
				$context['copyFromMessage'] = $copyFromMessage;
236
				$context['copyInlineAttachmentsOnly'] = in_array($actionType, ['reply', 'replyall'], true);
237
			}
238
		}
239
		elseif (isset($action['props']['sent_representing_email_address'], $action['props']['sent_representing_address_type']) && strcasecmp($action['props']['sent_representing_address_type'], 'EX') === 0) {
240
			$otherStore = $GLOBALS['mapisession']->addUserStore($action['props']['sent_representing_email_address']);
241
			if ($otherStore && $send) {
242
				$context['store'] = $otherStore;
243
			}
244
		}
245
246
		return $context;
247
	}
248
249
	/**
250
	 * Handle the send flow including plugin hooks and recipient processing.
251
	 *
252
	 * @param mixed $store
253
	 * @param mixed $entryid
254
	 * @param mixed $parententryid
255
	 * @param mixed $copyFromMessage
256
	 * @param bool  $copyAttachments
257
	 * @param bool  $copyInlineAttachmentsOnly
258
	 *
259
	 * @return array
260
	 */
261
	private function handleSend($store, $entryid, $parententryid, array &$action, array $recipients, array &$messageProps, $copyFromMessage, $copyAttachments, $copyInlineAttachmentsOnly) {
262
		$success = true;
263
		$GLOBALS['PluginManager']->triggerHook('server.module.createmailitemmodule.beforesend', [
264
			'moduleObject' => $this,
265
			'store' => $store,
266
			'entryid' => $entryid,
267
			'action' => $action,
268
			'success' => &$success,
269
			'properties' => $this->properties,
270
			'messageProps' => $messageProps,
271
			'parententryid' => $parententryid,
272
		]);
273
274
		if (!$success) {
0 ignored issues
show
introduced by
The condition $success is always true.
Loading history...
275
			return ['result' => false, 'aborted' => true];
276
		}
277
278
		if (!isset($action['message_action']['action_type']) || $action['message_action']['action_type'] !== 'edit_as_new') {
279
			$this->setReplyForwardInfo($action);
280
		}
281
282
		$savedUnsavedRecipients = [];
283
		if ($entryid) {
284
			$message = $GLOBALS['operations']->openMessage($store, $entryid);
285
			$savedRecipients = $GLOBALS['operations']->getRecipientsInfo($message);
286
			foreach ($savedRecipients as $recipient) {
287
				$savedUnsavedRecipients['saved'][] = $recipient['props'];
288
			}
289
		}
290
291
		if (!empty($recipients['add'])) {
292
			foreach ($recipients['add'] as $recipient) {
293
				$savedUnsavedRecipients['unsaved'][] = $recipient;
294
			}
295
		}
296
297
		$remove = !empty($recipients['remove']) ? $recipients['remove'] : [];
298
		$members = $GLOBALS['operations']->convertLocalDistlistMembersToRecipients($savedUnsavedRecipients, $remove);
299
		$action['recipients'] = $action['recipients'] ?? [];
300
		$action['recipients']['add'] = $members['add'];
301
		$action['recipients']['remove'] = !empty($remove) ? array_merge($action['recipients']['remove'] ?? [], $members['remove']) : $members['remove'];
302
303
		$error = $GLOBALS['operations']->submitMessage(
304
			$store,
305
			$entryid,
306
			Conversion::mapXML2MAPI($this->properties, $action['props']),
307
			$messageProps,
308
			$action['recipients'] ?? [],
309
			$action['attachments'] ?? [],
310
			$copyFromMessage,
311
			$copyAttachments,
312
			false,
313
			$copyInlineAttachmentsOnly,
314
			isset($action['props']['isHTML']) ? !$action['props']['isHTML'] : false
315
		);
316
317
		if (!$error) {
318
			$GLOBALS['operations']->parseDistListAndAddToRecipientHistory($savedUnsavedRecipients, $remove);
319
			if ($entryid) {
320
				$props = [];
321
				$props[PR_ENTRYID] = $entryid;
322
				$props[PR_PARENT_ENTRYID] = $parententryid;
323
				$storeProps = mapi_getprops($store, [PR_ENTRYID]);
324
				$props[PR_STORE_ENTRYID] = $storeProps[PR_ENTRYID];
325
				$GLOBALS['bus']->addData($this->getResponseData());
326
				$GLOBALS['bus']->notify(bin2hex($parententryid), TABLE_DELETE, $props);
327
			}
328
			$this->sendFeedback(true, [], false);
329
330
			return ['result' => true, 'aborted' => false];
331
		}
332
333
		if ($error === 'MAPI_E_NO_ACCESS') {
334
			$data = [];
335
			$data['type'] = 1;
336
			$data['info'] = [];
337
			$data['info']['title'] = _('Insufficient permissions');
338
			$data['info']['display_message'] = _("You don't have the permission to complete this action");
339
			$this->addActionData('error', $data);
340
		}
341
		if ($error === 'ecQuotaExceeded') {
342
			$data = [];
343
			$data['type'] = 1;
344
			$data['info'] = [];
345
			$data['info']['title'] = _('Quota error');
346
			$data['info']['display_message'] = _('Send quota limit reached');
347
			$this->addActionData('error', $data);
348
		}
349
		if ($error === 'ecRpcFailed') {
350
			$data = [];
351
			$data['type'] = 1;
352
			$data['info'] = [];
353
			$data['info']['title'] = _('Operation failed');
354
			$data['info']['display_message'] = _('Email sending failed. Check the log files for more information.');
355
			$this->addActionData('error', $data);
356
		}
357
358
		return ['result' => false, 'aborted' => false];
359
	}
360
361
	/**
362
	 * Handle the draft save flow including attachment and plugin updates.
363
	 *
364
	 * @param mixed $store
365
	 * @param mixed $entryid
366
	 * @param mixed $parententryid
367
	 * @param mixed $copyFromMessage
368
	 * @param bool  $copyAttachments
369
	 * @param bool  $copyInlineAttachmentsOnly
370
	 *
371
	 * @return array
372
	 */
373
	private function handleDraftSave($store, $entryid, $parententryid, array $action, array &$messageProps, $copyFromMessage, $copyAttachments, $copyInlineAttachmentsOnly) {
374
		$propertiesToDelete = [];
375
		$mapiProps = Conversion::mapXML2MAPI($this->properties, $action['props']);
376
		if (isset($action['props']['sent_representing_entryid']) && empty($action['props']['sent_representing_entryid'])) {
377
			$propertiesToDelete[] = PR_SENT_REPRESENTING_ENTRYID;
378
			$propertiesToDelete[] = PR_SENT_REPRESENTING_SEARCH_KEY;
379
		}
380
381
		$result = $GLOBALS['operations']->saveMessage(
382
			$store,
383
			$entryid,
384
			$parententryid,
385
			$mapiProps,
386
			$messageProps,
387
			$action['recipients'] ?? [],
388
			$action['attachments'] ?? [],
389
			$propertiesToDelete,
390
			$copyFromMessage,
391
			$copyAttachments,
392
			false,
393
			$copyInlineAttachmentsOnly
394
		);
395
396
		if (!$result) {
397
			return ['result' => false, 'aborted' => false];
398
		}
399
400
		$props = mapi_getprops($result, [PR_ENTRYID]);
401
		$savedMsg = $GLOBALS['operations']->openMessage($store, $props[PR_ENTRYID]);
402
		$attachNum = !empty($action['attach_num']) ? $action['attach_num'] : false;
403
		$data = [];
404
405
		if ($attachNum) {
406
			$message = $GLOBALS['operations']->openMessage($store, $props[PR_ENTRYID], $attachNum);
407
			if (empty($message)) {
408
				return ['result' => false, 'aborted' => true];
409
			}
410
			$data['item'] = $GLOBALS['operations']->getEmbeddedMessageProps($store, $message, $this->properties, $savedMsg, $attachNum);
411
		}
412
		else {
413
			$data = $GLOBALS['operations']->getMessageProps($store, $savedMsg, $this->properties, $this->plaintext, true);
414
		}
415
416
		unset($data['props']['body'], $data['props']['html_body'], $data['props']['isHTML']);
417
418
		$GLOBALS['PluginManager']->triggerHook('server.module.createmailitemmodule.aftersave', [
419
			'data' => &$data,
420
			'entryid' => $props[PR_ENTRYID],
421
			'action' => $action,
422
			'properties' => $this->properties,
423
			'messageProps' => $messageProps,
424
			'parententryid' => $parententryid,
425
		]);
426
427
		$this->addActionData('update', ['item' => $data]);
428
429
		return ['result' => $result, 'aborted' => false];
430
	}
431
432
	/**
433
	 * Convert a hexadecimal string to binary data, returning false when invalid.
434
	 *
435
	 * @param string $value
436
	 *
437
	 * @return false|string
438
	 */
439
	private function hexToBinOrFalse($value) {
440
		if ($value === '') {
441
			return '';
442
		}
443
444
		if ((strlen($value) % 2) !== 0 || !ctype_xdigit($value)) {
445
			return false;
446
		}
447
448
		return hex2bin($value);
449
	}
450
451
	/**
452
	 * Function which is used to get the source message information, which contains the information of
453
	 * reply/forward and entry id of original mail, where we have to set the reply/forward arrow when
454
	 * draft(saved mail) is send.
455
	 *
456
	 * @param array $action the action data, sent by the client
457
	 *
458
	 * @return array|bool false when entryid and source message entryid is missing otherwise array with
459
	 *                    source store entryid and source message entryid if message has
460
	 */
461
	public function getSourceMsgInfo($action) {
462
		$metaData = [];
463
		if (isset($action["props"]["source_message_info"]) && !empty($action["props"]["source_message_info"])) {
464
			if (isset($action["props"]['sent_representing_entryid']) && !empty($action["props"]['sent_representing_entryid'])) {
465
				$storeEntryid = hex2bin((string) $action['message_action']['source_store_entryid']);
466
			}
467
			else {
468
				$storeEntryid = hex2bin((string) $action['store_entryid']);
469
			}
470
			$metaData['source_message_info'] = $action["props"]["source_message_info"];
471
			$metaData['storeEntryid'] = $storeEntryid;
472
473
			return $metaData;
474
		}
475
		if (isset($action["entryid"]) && !empty($action["entryid"])) {
476
			$storeEntryid = hex2bin((string) $action['store_entryid']);
477
			$store = $GLOBALS['mapisession']->openMessageStore($storeEntryid);
478
479
			$entryid = hex2bin((string) $action['entryid']);
480
			$message = $GLOBALS['operations']->openMessage($store, $entryid);
481
			$messageProps = mapi_getprops($message);
482
483
			$props = Conversion::mapMAPI2XML($this->properties, $messageProps);
484
485
			$sourceMsgInfo = !empty($props['props']['source_message_info']) ? $props['props']['source_message_info'] : false;
486
487
			if (isset($props["props"]['sent_representing_entryid']) && !empty($props["props"]['sent_representing_entryid'])) {
488
				$storeEntryid = $this->getSourceStoreEntryId($props);
489
			}
490
491
			$metaData['source_message_info'] = $sourceMsgInfo;
492
			$metaData['storeEntryid'] = $storeEntryid;
493
494
			return $metaData;
495
		}
496
497
		return false;
498
	}
499
500
	/**
501
	 * Function is used to get the shared or delegate store entryid where
502
	 * source message was stored on which we have to set replay/forward arrow
503
	 * when draft(saved mail) is send.
504
	 *
505
	 * @param array $props the $props data, which get from saved mail
506
	 *
507
	 * @return string source store entryid
508
	 */
509
	public function getSourceStoreEntryId($props) {
510
		$sentRepresentingEntryid = $props['props']['sent_representing_entryid'];
511
		$user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(), hex2bin((string) $sentRepresentingEntryid));
512
		$userProps = mapi_getprops($user, [PR_EMAIL_ADDRESS]);
513
514
		return $GLOBALS['mapisession']->getStoreEntryIdOfUser(strtolower((string) $userProps[PR_EMAIL_ADDRESS]));
515
	}
516
517
	/**
518
	 * Function is used to set the reply/forward arrow on original mail.
519
	 *
520
	 * @param array $action the action data, sent by the client
521
	 */
522
	public function setReplyForwardInfo($action) {
523
		$message = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $message is dead and can be removed.
Loading history...
524
		$sourceMsgInfo = $this->getSourceMsgInfo($action);
525
		if (isset($sourceMsgInfo['source_message_info']) && $sourceMsgInfo['source_message_info']) {
526
			/**
527
			 * $sourceMsgInfo['source_message_info'] contains the hex value, where first 24byte contains action type
528
			 * and next 48byte contains entryid of original mail. so we have to extract the action type
529
			 * from this hex value.
530
			 *
531
			 * Example : 01000E000C00000005010000660000000200000030000000 + record entryid
532
			 * Here 66 represents the REPLY action type. same way 67 and 68 is represent
533
			 * REPLY ALL and FORWARD respectively.
534
			 */
535
			$mailActionType = substr((string) $sourceMsgInfo['source_message_info'], 24, 2);
536
			// get the entry id of origanal mail's.
537
			$originalEntryid = substr((string) $sourceMsgInfo['source_message_info'], 48);
538
			$entryid = hex2bin($originalEntryid);
539
540
			$store = $GLOBALS['mapisession']->openMessageStore($sourceMsgInfo['storeEntryid']);
541
542
			try {
543
				// if original mail of reply/forward mail is deleted from inbox then,
544
				// it will throw an exception so to handle it we need to write this block in try catch.
545
				$message = $GLOBALS['operations']->openMessage($store, $entryid);
546
			}
547
			catch (MAPIException $e) {
548
				$e->setHandled();
549
			}
550
551
			if ($message) {
552
				$messageProps = mapi_getprops($message);
553
				$props = Conversion::mapMAPI2XML($this->properties, $messageProps);
554
555
				switch ($mailActionType) {
556
					case '66': // Reply
557
					case '67': // Reply All
558
						$props['icon_index'] = 261;
559
						break;
560
561
					case '68':// Forward
562
						$props['icon_index'] = 262;
563
						break;
564
				}
565
				$props['last_verb_executed'] = hexdec($mailActionType);
566
				$props['last_verb_execution_time'] = time();
567
				$mapiProps = Conversion::mapXML2MAPI($this->properties, $props);
568
				$messageActionProps = [];
569
				$messageActionResult = $GLOBALS['operations']->saveMessage($store, $mapiProps[PR_ENTRYID], $mapiProps[PR_PARENT_ENTRYID], $mapiProps, $messageActionProps);
570
				if ($messageActionResult) {
571
					if (isset($messageActionProps[PR_PARENT_ENTRYID])) {
572
						$GLOBALS['bus']->notify(bin2hex($messageActionProps[PR_PARENT_ENTRYID]), TABLE_SAVE, $messageActionProps);
573
					}
574
				}
575
			}
576
		}
577
	}
578
}
579