Passed
Push — master ( 6646c2...1e1793 )
by
unknown
05:17
created

CreateMailItemModule::handleDraftSave()   B

Complexity

Conditions 7
Paths 14

Size

Total Lines 57
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 41
c 0
b 0
f 0
nc 14
nop 8
dl 0
loc 57
rs 8.3306

How to fix   Long Method    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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