Completed
Pull Request — master (#184)
by Joas
23:54
created

MailQueueHandler::sendEmailToUser()   B

Complexity

Conditions 7
Paths 17

Size

Total Lines 66
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 7.0629

Importance

Changes 0
Metric Value
dl 0
loc 66
ccs 41
cts 46
cp 0.8913
rs 7.0832
c 0
b 0
f 0
cc 7
eloc 46
nc 17
nop 5
crap 7.0629

How to fix   Long Method   

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
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Joas Schilling <[email protected]>
6
 * @author Lukas Reschke <[email protected]>
7
 *
8
 * @license AGPL-3.0
9
 *
10
 * This code is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License, version 3,
12
 * as published by the Free Software Foundation.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License, version 3,
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21
 *
22
 */
23
24
namespace OCA\Activity;
25
26
use OCA\Activity\Extension\LegacyParser;
27
use OCP\Activity\IEvent;
28
use OCP\Activity\IManager;
29
use OCP\DB\QueryBuilder\IQueryBuilder;
30
use OCP\Defaults;
31
use OCP\IConfig;
32
use OCP\IDateTimeFormatter;
33
use OCP\IDBConnection;
34
use OCP\ILogger;
35
use OCP\IURLGenerator;
36
use OCP\IUser;
37
use OCP\IUserManager;
38
use OCP\L10N\IFactory;
39
use OCP\Mail\IMailer;
40
use OCP\Util;
41
42
/**
43
 * Class MailQueueHandler
44
 * Gets the users from the database and
45
 *
46
 * @package OCA\Activity
47
 */
48
class MailQueueHandler {
49
50
	const CLI_EMAIL_BATCH_SIZE = 500;
51
52
	const WEB_EMAIL_BATCH_SIZE = 25;
53
54
	/** Number of entries we want to list in the email */
55
	const ENTRY_LIMIT = 200;
56
57
	/** @var array */
58
	protected $languages;
59
60
	/** @var string */
61
	protected $senderAddress;
62
63
	/** @var string */
64
	protected $senderName;
65
66
	/** @var IDateTimeFormatter */
67
	protected $dateFormatter;
68
69
	/** @var DataHelper */
70
	protected $dataHelper;
71
72
	/** @var IDBConnection */
73
	protected $connection;
74
75
	/** @var IMailer */
76
	protected $mailer;
77
78
	/** @var IURLGenerator */
79
	protected $urlGenerator;
80
81
	/** @var IUserManager */
82
	protected $userManager;
83
84
	/** @var IFactory */
85
	protected $lFactory;
86
87
	/** @var IManager */
88
	protected $activityManager;
89
90
	/** @var LegacyParser */
91
	protected $legacyParser;
92
93
	/** @var IConfig */
94
	protected $config;
95
96
	/** @var ILogger */
97
	protected $logger;
98
99
	/**
100
	 * Constructor
101
	 *
102
	 * @param IDateTimeFormatter $dateFormatter
103
	 * @param IDBConnection $connection
104
	 * @param DataHelper $dataHelper
105
	 * @param IMailer $mailer
106
	 * @param IURLGenerator $urlGenerator
107
	 * @param IUserManager $userManager
108
	 * @param IFactory $lFactory
109
	 * @param IManager $activityManager
110
	 * @param LegacyParser $legacyParser
111
	 * @param IConfig $config
112
	 * @param ILogger $logger
113
	 */
114 10
	public function __construct(IDateTimeFormatter $dateFormatter,
115
								IDBConnection $connection,
116
								DataHelper $dataHelper,
117
								IMailer $mailer,
118
								IURLGenerator $urlGenerator,
119
								IUserManager $userManager,
120
								IFactory $lFactory,
121
								IManager $activityManager,
122
								LegacyParser $legacyParser,
123
								IConfig $config,
124
								ILogger $logger) {
125 10
		$this->dateFormatter = $dateFormatter;
126 10
		$this->connection = $connection;
127 10
		$this->dataHelper = $dataHelper;
128 10
		$this->mailer = $mailer;
129 10
		$this->urlGenerator = $urlGenerator;
130 10
		$this->userManager = $userManager;
131 10
		$this->lFactory = $lFactory;
132 10
		$this->activityManager = $activityManager;
133 10
		$this->legacyParser = $legacyParser;
134 10
		$this->config = $config;
135 10
		$this->logger = $logger;
136 10
	}
137
138
	/**
139
	 * Send an email to {$limit} users
140
	 *
141
	 * @param int $limit Number of users we want to send an email to
142
	 * @param int $sendTime The latest send time
143
	 * @param bool $forceSending Ignores latest send and just sends all emails
144
	 * @param null|int $restrictEmails null or one of UserSettings::EMAIL_SEND_*
145
	 * @return int Number of users we sent an email to
146
	 */
147
	public function sendEmails($limit, $sendTime, $forceSending = false, $restrictEmails = null) {
148
		// Get all users which should receive an email
149
		$affectedUsers = $this->getAffectedUsers($limit, $sendTime, $forceSending, $restrictEmails);
150
		if (empty($affectedUsers)) {
151
			// No users found to notify, mission abort
152
			return 0;
153
		}
154
155
		$userLanguages = $this->config->getUserValueForUsers('core', 'lang', $affectedUsers);
156
		$userTimezones = $this->config->getUserValueForUsers('core', 'timezone', $affectedUsers);
157
		$userEmails = $this->config->getUserValueForUsers('settings', 'email', $affectedUsers);
158
		$userEnabled = $this->config->getUserValueForUsers('core', 'enabled', $affectedUsers);
159
160
		// Send Email
161
		$default_lang = $this->config->getSystemValue('default_language', 'en');
162
		$defaultTimeZone = date_default_timezone_get();
163
164
		$deleteItemsForUsers = [];
165
		$this->activityManager->setRequirePNG(true);
166
		foreach ($affectedUsers as $user) {
167
			if (isset($userEnabled[$user]) && $userEnabled[$user] === 'no') {
168
				$deleteItemsForUsers[] = $user;
169
				continue;
170
			}
171
172
			if (empty($userEmails[$user])) {
173
				// The user did not setup an email address
174
				// So we will not send an email :(
175
				$this->logger->debug("Couldn't send notification email to user '{user}' (email address isn't set for that user)", ['user' => $user, 'app' => 'activity']);
176
				$deleteItemsForUsers[] = $user;
177
				continue;
178
			}
179
180
			$language = (!empty($userLanguages[$user])) ? $userLanguages[$user] : $default_lang;
181
			$timezone = (!empty($userTimezones[$user])) ? $userTimezones[$user] : $defaultTimeZone;
182
			try {
183
				if ($this->sendEmailToUser($user, $userEmails[$user], $language, $timezone, $sendTime)) {
184
					$deleteItemsForUsers[] = $user;
185
				} else {
186
					$this->logger->debug("Failed sending activity email to user '{user}'.", ['user' => $user, 'app' => 'activity']);
187
				}
188
			} catch (\UnexpectedValueException $e) {
189
				// continue;
190
			}
191
		}
192
		$this->activityManager->setRequirePNG(false);
193
194
		// Delete all entries we dealt with
195
		$this->deleteSentItems($deleteItemsForUsers, $sendTime);
196
197
		return count($affectedUsers);
198
	}
199
200
	/**
201
	 * Get the users we want to send an email to
202
	 *
203
	 * @param int|null $limit
204
	 * @param int $latestSend
205
	 * @param bool $forceSending
206
	 * @param int|null $restrictEmails
207
	 * @return array
208
	 */
209 6
	protected function getAffectedUsers($limit, $latestSend, $forceSending, $restrictEmails) {
210 6
		$query = $this->connection->getQueryBuilder();
211 6
		$query->select('amq_affecteduser')
212 6
			->selectAlias($query->createFunction('MIN(' . $query->getColumnName('amq_latest_send') . ')'), 'amq_trigger_time')
213 6
			->from('activity_mq')
214 6
			->groupBy('amq_affecteduser')
215 6
			->orderBy('amq_trigger_time', 'ASC');
216
217 6
		if ($limit > 0) {
218 5
			$query->setMaxResults($limit);
219 5
		}
220
221 6
		if ($forceSending) {
222
			$query->where($query->expr()->lt('amq_timestamp', $query->createNamedParameter($latestSend)));
223
		} else {
224 6
			$query->where($query->expr()->lt('amq_latest_send', $query->createNamedParameter($latestSend)));
225
		}
226
227 6
		if ($restrictEmails !== null) {
228
			if ($restrictEmails === UserSettings::EMAIL_SEND_HOURLY) {
229
				$query->where($query->expr()->lte('amq_timestamp', $query->createFunction($query->getColumnName('amq_latest_send') . ' + ' . 3600)));
230
			} else if ($restrictEmails === UserSettings::EMAIL_SEND_DAILY) {
231
				$query->where($query->expr()->eq('amq_timestamp', $query->createFunction($query->getColumnName('amq_latest_send') . ' + ' . 3600 * 24)));
232
			} else if ($restrictEmails === UserSettings::EMAIL_SEND_WEEKLY) {
233
				$query->where($query->expr()->eq('amq_timestamp', $query->createFunction($query->getColumnName('amq_latest_send') . ' + ' . 3600 * 24 * 7)));
234
			}
235
		}
236
237 6
		$result = $query->execute();
238
239 6
		$affectedUsers = array();
240 6
		while ($row = $result->fetch()) {
241 6
			$affectedUsers[] = $row['amq_affecteduser'];
242 6
		}
243 6
		$result->closeCursor();
244
245 6
		return $affectedUsers;
246
	}
247
248
	/**
249
	 * Get all items for the user we want to send an email to
250
	 *
251
	 * @param string $affectedUser
252
	 * @param int $maxTime
253
	 * @param int $maxNumItems
254
	 * @return array [data of the first max. 200 entries, total number of entries]
255
	 */
256 7
	protected function getItemsForUser($affectedUser, $maxTime, $maxNumItems = self::ENTRY_LIMIT) {
257 7
		$query = $this->connection->prepare(
258
			'SELECT * '
259
			. ' FROM `*PREFIX*activity_mq` '
260 7
			. ' WHERE `amq_timestamp` <= ? '
261 7
			. ' AND `amq_affecteduser` = ? '
262 7
			. ' ORDER BY `amq_timestamp` ASC',
263
			$maxNumItems
264 7
		);
265 7
		$query->execute([(int) $maxTime, $affectedUser]);
266
267 7
		$activities = array();
268 7
		while ($row = $query->fetch()) {
269 7
			$activities[] = $row;
270 7
		}
271
272 7
		if (isset($activities[$maxNumItems - 1])) {
273
			// Reached the limit, run a query to get the actual count.
274 1
			$query = $this->connection->prepare(
275
				'SELECT COUNT(*) AS `actual_count`'
276
				. ' FROM `*PREFIX*activity_mq` '
277 1
				. ' WHERE `amq_timestamp` <= ? '
278 1
				. ' AND `amq_affecteduser` = ?'
279 1
			);
280 1
			$query->execute([(int) $maxTime, $affectedUser]);
281
282 1
			$row = $query->fetch();
283 1
			return [$activities, $row['actual_count'] - $maxNumItems];
284
		} else {
285 7
			return [$activities, 0];
286
		}
287
	}
288
289
	/**
290
	 * Get a language object for a specific language
291
	 *
292
	 * @param string $lang Language identifier
293
	 * @return \OCP\IL10N Language object of $lang
294
	 */
295 1
	protected function getLanguage($lang) {
296 1
		if (!isset($this->languages[$lang])) {
297 1
			$this->languages[$lang] = $this->lFactory->get('activity', $lang);
298 1
		}
299
300 1
		return $this->languages[$lang];
301
	}
302
303
	/**
304
	 * Get the sender data
305
	 * @param string $setting Either `email` or `name`
306
	 * @return string
307
	 */
308 1
	protected function getSenderData($setting) {
309 1
		if (empty($this->senderAddress)) {
310 1
			$this->senderAddress = Util::getDefaultEmailAddress('no-reply');
311 1
		}
312 1
		if (empty($this->senderName)) {
313 1
			$defaults = new Defaults();
314 1
			$this->senderName = $defaults->getName();
315 1
		}
316
317 1
		if ($setting === 'email') {
318 1
			return $this->senderAddress;
319
		}
320 1
		return $this->senderName;
321
	}
322
323
	/**
324
	 * Send a notification to one user
325
	 *
326
	 * @param string $userName Username of the recipient
327
	 * @param string $email Email address of the recipient
328
	 * @param string $lang Selected language of the recipient
329
	 * @param string $timezone Selected timezone of the recipient
330
	 * @param int $maxTime
331
	 * @return bool True if the entries should be removed, false otherwise
332
	 * @throws \UnexpectedValueException
333
	 */
334 1
	protected function sendEmailToUser($userName, $email, $lang, $timezone, $maxTime) {
335 1
		$user = $this->userManager->get($userName);
336 1
		if (!$user instanceof IUser) {
0 ignored issues
show
Bug introduced by
The class OCP\IUser does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
337 1
			return true;
338
		}
339
340 1
		list($mailData, $skippedCount) = $this->getItemsForUser($userName, $maxTime);
341
342 1
		$l = $this->getLanguage($lang);
343 1
		$this->dataHelper->setUser($userName);
344 1
		$this->dataHelper->setL10n($l);
345 1
		$this->activityManager->setCurrentUserId($userName);
346
347 1
		$template = $this->mailer->createEMailTemplate();
348 1
		$template->addHeader();
349 1
		$template->addHeading($l->t('Hello %s',[$user->getDisplayName()]), $l->t('Hello %s,',[$user->getDisplayName()]));
350 1
		$template->addBodyText($l->t('There was some activity at %s', [$this->urlGenerator->getAbsoluteURL('/')]));
351
352 1
		foreach ($mailData as $activity) {
353 1
			$event = $this->activityManager->generateEvent();
354
			try {
355 1
				$event->setApp($activity['amq_appid'])
356 1
					->setType($activity['amq_type'])
357 1
					->setTimestamp((int) $activity['amq_timestamp'])
358 1
					->setSubject($activity['amq_subject'], json_decode($activity['amq_subjectparams'], true));
359 1
			} catch (\InvalidArgumentException $e) {
360
				continue;
361
			}
362
363 1
			$relativeDateTime = $this->dateFormatter->formatDateTimeRelativeDay(
364 1
				$activity['amq_timestamp'],
365 1
				'long', 'short',
366 1
				new \DateTimeZone($timezone), $l
367 1
			);
368
369
			try {
370 1
				$event = $this->parseEvent($lang, $event);
371 1
			} catch (\InvalidArgumentException $e) {
372
				continue;
373
			}
374
375 1
			$template->addBodyListItem($event->getParsedSubject(), $relativeDateTime, $event->getIcon());
376 1
		}
377
378 1
		if ($skippedCount) {
379
			$template->addBodyListItem($l->n('and %n more ', 'and %n more ', $skippedCount));
380
		}
381
382 1
		$template->addFooter();
383
384 1
		$message = $this->mailer->createMessage();
385 1
		$message->setTo([$email => $user->getDisplayName()]);
386 1
		$message->setSubject((string) $l->t('Activity notification'));
387 1
		$message->setHtmlBody($template->renderHtml());
388 1
		$message->setPlainBody($template->renderText());
389 1
		$message->setFrom([$this->getSenderData('email') => $this->getSenderData('name')]);
390
391
		try {
392 1
			$this->mailer->send($message);
393 1
		} catch (\Exception $e) {
394
			return false;
395
		}
396
397 1
		$this->activityManager->setCurrentUserId(null);
398 1
		return true;
399
	}
400
401
	/**
402
	 * @param string $lang
403
	 * @param IEvent $event
404
	 * @return IEvent
405
	 * @throws \InvalidArgumentException when the event could not be parsed
406
	 */
407 1
	protected function parseEvent($lang, IEvent $event) {
408 1
		foreach ($this->activityManager->getProviders() as $provider) {
409
			try {
410
				$this->activityManager->setFormattingObject($event->getObjectType(), $event->getObjectId());
411
				$event = $provider->parse($lang, $event);
412
				$this->activityManager->setFormattingObject('', 0);
413
			} catch (\InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
414
			}
415 1
		}
416
417 1
		if (!$event->getParsedSubject()) {
418 1
			$this->activityManager->setFormattingObject($event->getObjectType(), $event->getObjectId());
419 1
			$event = $this->legacyParser->parse($lang, $event);
420 1
			$this->activityManager->setFormattingObject('', 0);
421 1
		}
422
423 1
		return $event;
424
	}
425
426
	/**
427
	 * Delete all entries we dealt with
428
	 *
429
	 * @param array $affectedUsers
430
	 * @param int $maxTime
431
	 */
432 5
	protected function deleteSentItems(array $affectedUsers, $maxTime) {
433 5
		if (empty($affectedUsers)) {
434
			return;
435
		}
436
437 5
		$query = $this->connection->getQueryBuilder();
438 5
		$query->delete('activity_mq')
439 5
			->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime, IQueryBuilder::PARAM_INT)))
440 5
			->andWhere($query->expr()->in('amq_affecteduser', $query->createNamedParameter($affectedUsers, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR));
441 5
		$query->execute();
442 5
	}
443
}
444