Completed
Pull Request — master (#153)
by Maxence
02:45
created

MailQueueHandler::sendEmailToUser()   B

Complexity

Conditions 7
Paths 17

Size

Total Lines 66
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 7.2114

Importance

Changes 0
Metric Value
dl 0
loc 66
ccs 36
cts 43
cp 0.8372
rs 7.0832
c 0
b 0
f 0
cc 7
eloc 46
nc 17
nop 5
crap 7.2114

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
159
		// Send Email
160
		$default_lang = $this->config->getSystemValue('default_language', 'en');
161
		$defaultTimeZone = date_default_timezone_get();
162
163
		$deleteItemsForUsers = [];
164
		foreach ($affectedUsers as $user) {
165
			if (empty($userEmails[$user])) {
166
				// The user did not setup an email address
167
				// So we will not send an email :(
168
				$this->logger->debug("Couldn't send notification email to user '{user}' (email address isn't set for that user)", ['user' => $user, 'app' => 'activity']);
169
				continue;
170
			}
171
172
			$language = (!empty($userLanguages[$user])) ? $userLanguages[$user] : $default_lang;
173
			$timezone = (!empty($userTimezones[$user])) ? $userTimezones[$user] : $defaultTimeZone;
174
			try {
175
				if ($this->sendEmailToUser($user, $userEmails[$user], $language, $timezone, $sendTime)) {
176
					$deleteItemsForUsers[] = $user;
177
				} else {
178
					$this->logger->debug("Failed sending activity email to user '{user}'.", ['user' => $user, 'app' => 'activity']);
179
				}
180
			} catch (\UnexpectedValueException $e) {
181
				// continue;
182
			}
183
		}
184
185
		// Delete all entries we dealt with
186
		$this->deleteSentItems($deleteItemsForUsers, $sendTime);
187
188
		return count($affectedUsers);
189
	}
190
191
	/**
192
	 * Get the users we want to send an email to
193
	 *
194
	 * @param int|null $limit
195
	 * @param int $latestSend
196
	 * @param bool $forceSending
197
	 * @param int|null $restrictEmails
198
	 * @return array
199
	 */
200 6
	protected function getAffectedUsers($limit, $latestSend, $forceSending, $restrictEmails) {
201 6
		$query = $this->connection->getQueryBuilder();
202 6
		$query->select('amq_affecteduser')
203 6
			->selectAlias($query->createFunction('MIN(' . $query->getColumnName('amq_latest_send') . ')'), 'amq_trigger_time')
204 6
			->from('activity_mq')
205 6
			->groupBy('amq_affecteduser')
206 6
			->orderBy('amq_trigger_time', 'ASC');
207
208 6
		if ($limit > 0) {
209 5
			$query->setMaxResults($limit);
210
		}
211
212 6
		if ($forceSending) {
213
			$query->where($query->expr()->lt('amq_timestamp', $query->createNamedParameter($latestSend)));
214
		} else {
215 6
			$query->where($query->expr()->lt('amq_latest_send', $query->createNamedParameter($latestSend)));
216
		}
217
218 6
		if ($restrictEmails !== null) {
219
			if ($restrictEmails === UserSettings::EMAIL_SEND_HOURLY) {
220
				$query->where($query->expr()->lte('amq_timestamp', $query->createFunction($query->getColumnName('amq_latest_send') . ' + ' . 3600)));
221
			} else if ($restrictEmails === UserSettings::EMAIL_SEND_DAILY) {
222
				$query->where($query->expr()->eq('amq_timestamp', $query->createFunction($query->getColumnName('amq_latest_send') . ' + ' . 3600 * 24)));
223
			} else if ($restrictEmails === UserSettings::EMAIL_SEND_WEEKLY) {
224
				$query->where($query->expr()->eq('amq_timestamp', $query->createFunction($query->getColumnName('amq_latest_send') . ' + ' . 3600 * 24 * 7)));
225
			}
226
		}
227
228 6
		$result = $query->execute();
229
230 6
		$affectedUsers = array();
231 6
		while ($row = $result->fetch()) {
232 6
			$affectedUsers[] = $row['amq_affecteduser'];
233
		}
234 6
		$result->closeCursor();
235
236 6
		return $affectedUsers;
237
	}
238
239
	/**
240
	 * Get all items for the user we want to send an email to
241
	 *
242
	 * @param string $affectedUser
243
	 * @param int $maxTime
244
	 * @param int $maxNumItems
245
	 * @return array [data of the first max. 200 entries, total number of entries]
246
	 */
247 7
	protected function getItemsForUser($affectedUser, $maxTime, $maxNumItems = self::ENTRY_LIMIT) {
248 7
		$query = $this->connection->prepare(
249
			'SELECT * '
250
			. ' FROM `*PREFIX*activity_mq` '
251
			. ' WHERE `amq_timestamp` <= ? '
252
			. ' AND `amq_affecteduser` = ? '
253 7
			. ' ORDER BY `amq_timestamp` ASC',
254 7
			$maxNumItems
255
		);
256 7
		$query->execute([(int) $maxTime, $affectedUser]);
257
258 7
		$activities = array();
259 7
		while ($row = $query->fetch()) {
260 7
			$activities[] = $row;
261
		}
262
263 7
		if (isset($activities[$maxNumItems - 1])) {
264
			// Reached the limit, run a query to get the actual count.
265 1
			$query = $this->connection->prepare(
266
				'SELECT COUNT(*) AS `actual_count`'
267
				. ' FROM `*PREFIX*activity_mq` '
268
				. ' WHERE `amq_timestamp` <= ? '
269 1
				. ' AND `amq_affecteduser` = ?'
270
			);
271 1
			$query->execute([(int) $maxTime, $affectedUser]);
272
273 1
			$row = $query->fetch();
274 1
			return [$activities, $row['actual_count'] - $maxNumItems];
275
		} else {
276 7
			return [$activities, 0];
277
		}
278
	}
279
280
	/**
281
	 * Get a language object for a specific language
282
	 *
283
	 * @param string $lang Language identifier
284
	 * @return \OCP\IL10N Language object of $lang
285
	 */
286 1
	protected function getLanguage($lang) {
287 1
		if (!isset($this->languages[$lang])) {
288 1
			$this->languages[$lang] = $this->lFactory->get('activity', $lang);
289
		}
290
291 1
		return $this->languages[$lang];
292
	}
293
294
	/**
295
	 * Get the sender data
296
	 * @param string $setting Either `email` or `name`
297
	 * @return string
298
	 */
299 1
	protected function getSenderData($setting) {
300 1
		if (empty($this->senderAddress)) {
301 1
			$this->senderAddress = Util::getDefaultEmailAddress('no-reply');
302
		}
303 1
		if (empty($this->senderName)) {
304 1
			$defaults = new Defaults();
305 1
			$this->senderName = $defaults->getName();
306
		}
307
308 1
		if ($setting === 'email') {
309 1
			return $this->senderAddress;
310
		}
311 1
		return $this->senderName;
312
	}
313
314
	/**
315
	 * Send a notification to one user
316
	 *
317
	 * @param string $userName Username of the recipient
318
	 * @param string $email Email address of the recipient
319
	 * @param string $lang Selected language of the recipient
320
	 * @param string $timezone Selected timezone of the recipient
321
	 * @param int $maxTime
322
	 * @return bool True if the entries should be removed, false otherwise
323
	 * @throws \UnexpectedValueException
324
	 */
325 1
	protected function sendEmailToUser($userName, $email, $lang, $timezone, $maxTime) {
326 1
		$user = $this->userManager->get($userName);
327 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...
328 1
			return true;
329
		}
330
331 1
		list($mailData, $skippedCount) = $this->getItemsForUser($userName, $maxTime);
332
333 1
		$l = $this->getLanguage($lang);
334 1
		$this->dataHelper->setUser($userName);
335 1
		$this->dataHelper->setL10n($l);
336 1
		$this->activityManager->setCurrentUserId($userName);
337
338 1
		$template = $this->mailer->createEMailTemplate();
339 1
		$template->addHeader();
340 1
		$template->addHeading($l->t('Hello %s',[$user->getDisplayName()]), $l->t('Hello %s,',[$user->getDisplayName()]));
341 1
		$template->addBodyText($l->t('There was some activity at %s', [$this->urlGenerator->getAbsoluteURL('/')]));
342
343 1
		foreach ($mailData as $activity) {
344 1
			$event = $this->activityManager->generateEvent();
345
			try {
346 1
				$event->setApp($activity['amq_appid'])
347 1
					->setType($activity['amq_type'])
348 1
					->setTimestamp((int) $activity['amq_timestamp'])
349 1
					->setSubject($activity['amq_subject'], json_decode($activity['amq_subjectparams'], true));
350
			} catch (\InvalidArgumentException $e) {
351
				continue;
352
			}
353
354 1
			$relativeDateTime = $this->dateFormatter->formatDateTimeRelativeDay(
355 1
				$activity['amq_timestamp'],
356 1
				'long', 'short',
357 1
				new \DateTimeZone($timezone), $l
358
			);
359
360
			try {
361 1
				$event = $this->parseEvent($lang, $event);
362
			} catch (\InvalidArgumentException $e) {
363
				continue;
364
			}
365
366 1
			$template->addBodyListItem($event->getParsedSubject(), $relativeDateTime, $event->getIcon());
367
		}
368
369 1
		if ($skippedCount) {
370
			$template->addBodyListItem($l->n('and %n more ', 'and %n more ', $skippedCount));
371
		}
372
373 1
		$template->addFooter();
374
375 1
		$message = $this->mailer->createMessage();
376 1
		$message->setTo([$email => $user->getDisplayName()]);
377 1
		$message->setSubject((string) $l->t('Activity notification'));
378 1
		$message->setHtmlBody($template->renderHtml());
379 1
		$message->setPlainBody($template->renderText());
380 1
		$message->setFrom([$this->getSenderData('email') => $this->getSenderData('name')]);
381
382
		try {
383 1
			$this->mailer->send($message);
384
		} catch (\Exception $e) {
385
			return false;
386
		}
387
388 1
		$this->activityManager->setCurrentUserId(null);
389 1
		return true;
390
	}
391
392
	/**
393
	 * @param string $lang
394
	 * @param IEvent $event
395
	 * @return IEvent
396
	 * @throws \InvalidArgumentException when the event could not be parsed
397
	 */
398 1
	protected function parseEvent($lang, IEvent $event) {
399 1
		foreach ($this->activityManager->getProviders() as $provider) {
400
			try {
401
				$this->activityManager->setFormattingObject($event->getObjectType(), $event->getObjectId());
402
				$event = $provider->parse($lang, $event);
403
				$this->activityManager->setFormattingObject('', 0);
404
			} catch (\InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
405
			}
406
		}
407
408 1
		if (!$event->getParsedSubject()) {
409 1
			$this->activityManager->setFormattingObject($event->getObjectType(), $event->getObjectId());
410 1
			$event = $this->legacyParser->parse($lang, $event);
411 1
			$this->activityManager->setFormattingObject('', 0);
412
		}
413
414 1
		return $event;
415
	}
416
417
	/**
418
	 * Delete all entries we dealt with
419
	 *
420
	 * @param array $affectedUsers
421
	 * @param int $maxTime
422
	 */
423 5
	protected function deleteSentItems(array $affectedUsers, $maxTime) {
424 5
		if (empty($affectedUsers)) {
425
			return;
426
		}
427
428 5
		$query = $this->connection->getQueryBuilder();
429 5
		$query->delete('activity_mq')
430 5
			->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime, IQueryBuilder::PARAM_INT)))
431 5
			->andWhere($query->expr()->in('amq_affecteduser', $query->createNamedParameter($affectedUsers, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR));
432 5
		$query->execute();
433 5
	}
434
}
435