1<?php
2# MantisBT - A PHP based bugtracking system
3
4# MantisBT is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 2 of the License, or
7# (at your option) any later version.
8#
9# MantisBT is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with MantisBT.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Email API
19 *
20 * @package CoreAPI
21 * @subpackage EmailAPI
22 * @copyright Copyright 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
23 * @copyright Copyright 2002  MantisBT Team - mantisbt-dev@lists.sourceforge.net
24 * @link http://www.mantisbt.org
25 *
26 * @uses access_api.php
27 * @uses authentication_api.php
28 * @uses bug_api.php
29 * @uses bugnote_api.php
30 * @uses category_api.php
31 * @uses config_api.php
32 * @uses constant_inc.php
33 * @uses current_user_api.php
34 * @uses custom_field_api.php
35 * @uses database_api.php
36 * @uses email_queue_api.php
37 * @uses event_api.php
38 * @uses helper_api.php
39 * @uses history_api.php
40 * @uses lang_api.php
41 * @uses logging_api.php
42 * @uses project_api.php
43 * @uses relationship_api.php
44 * @uses sponsorship_api.php
45 * @uses string_api.php
46 * @uses user_api.php
47 * @uses user_pref_api.php
48 * @uses utility_api.php
49 *
50 * @uses PHPMailerAutoload.php PHPMailer library
51 */
52
53require_api( 'access_api.php' );
54require_api( 'authentication_api.php' );
55require_api( 'bug_api.php' );
56require_api( 'bugnote_api.php' );
57require_api( 'category_api.php' );
58require_api( 'config_api.php' );
59require_api( 'constant_inc.php' );
60require_api( 'current_user_api.php' );
61require_api( 'custom_field_api.php' );
62require_api( 'database_api.php' );
63require_api( 'email_queue_api.php' );
64require_api( 'event_api.php' );
65require_api( 'helper_api.php' );
66require_api( 'history_api.php' );
67require_api( 'lang_api.php' );
68require_api( 'logging_api.php' );
69require_api( 'project_api.php' );
70require_api( 'relationship_api.php' );
71require_api( 'sponsorship_api.php' );
72require_api( 'string_api.php' );
73require_api( 'user_api.php' );
74require_api( 'user_pref_api.php' );
75require_api( 'utility_api.php' );
76
77use PHPMailer\PHPMailer\PHPMailer;
78use PHPMailer\PHPMailer\Exception as phpmailerException;
79use Mantis\Exceptions\ClientException;
80
81/** @global PHPMailer $g_phpMailer reusable PHPMailer object */
82$g_phpMailer = null;
83
84/**
85 * Indicates how generated emails will be processed by the shutdown function
86 * at the end of the current request's execution; this is a binary flag:
87 * - EMAIL_SHUTDOWN_SKIP       Initial state: do nothing (no generated emails)
88 * - EMAIL_SHUTDOWN_GENERATED  Emails will be sent, unless $g_email_send_using_cronjob is ON
89 * - EMAIL_SHUTDOWN_FORCE      All queued emails will be sent regardless of cronjob settings
90 * @see email_shutdown_function()
91 * @global $g_email_shutdown_processing
92 */
93$g_email_shutdown_processing = EMAIL_SHUTDOWN_SKIP;
94
95/**
96 * Regex for valid email addresses
97 * @see string_insert_hrefs()
98 * This pattern is consistent with email addresses validation logic
99 * @see $g_validate_email
100 * Uses the standard HTML5 pattern defined in
101 * {@link http://www.w3.org/TR/html5/forms.html#valid-e-mail-address}
102 * Note: the original regex from the spec has been modified to
103 * - escape the '/' in the first character class definition
104 * - remove the '^' and '$' anchors to allow matching anywhere in a string
105 * - add a limit of 64 chars on local part to avoid timeouts on very long texts with false matches.
106 *
107 * @return string
108 */
109function email_regex_simple() {
110	return "/[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]{1,64}@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/";
111}
112
113/**
114 * check to see that the format is valid and that the mx record exists
115 * @param string $p_email An email address.
116 * @return boolean
117 */
118function email_is_valid( $p_email ) {
119	$t_validate_email = config_get_global( 'validate_email' );
120
121	# if we don't validate then just accept
122	# If blank email is allowed or current user is admin, then accept blank emails which are useful for
123	# accounts that should never receive email notifications (e.g. anonymous account)
124	if( OFF == $t_validate_email ||
125		ON == config_get_global( 'use_ldap_email' ) ||
126		( is_blank( $p_email ) && ( ON == config_get( 'allow_blank_email' ) || current_user_is_administrator() ) )
127	) {
128		return true;
129	}
130
131	# E-mail validation method
132	# Note: PHPMailer offers alternative validation methods.
133	# It was decided in PR 172 (https://github.com/mantisbt/mantisbt/pull/172)
134	# to just default to HTML5 without over-complicating things for end users
135	# by offering a potentially confusing choice between the different methods.
136	# Refer to PHPMailer documentation for ValidateAddress method for details.
137	# @link https://github.com/PHPMailer/PHPMailer/blob/v5.2.9/class.phpmailer.php#L863
138	$t_method = 'html5';
139
140	# check email address is a valid format
141	log_event( LOG_EMAIL_VERBOSE, "Validating address '$p_email' with method '$t_method'" );
142	if( PHPMailer::validateAddress( $p_email, $t_method ) ) {
143		$t_domain = substr( $p_email, strpos( $p_email, '@' ) + 1 );
144
145		# see if we're limited to a set of known domains
146		$t_limit_email_domains = config_get( 'limit_email_domains' );
147		if( !empty( $t_limit_email_domains ) ) {
148			foreach( $t_limit_email_domains as $t_email_domain ) {
149				if( 0 == strcasecmp( $t_email_domain, $t_domain ) ) {
150					return true; # no need to check mx record details (below) if we've explicitly allowed the domain
151				}
152			}
153			log_event( LOG_EMAIL, "failed - not in limited domains list '$t_limit_email_domains'" );
154			return false;
155		}
156
157		if( ON == config_get( 'check_mx_record' ) ) {
158			$t_mx = array();
159
160			# Check for valid mx records
161			if( getmxrr( $t_domain, $t_mx ) ) {
162				return true;
163			} else {
164				$t_host = $t_domain . '.';
165
166				# for no mx record... try dns check
167				if( checkdnsrr( $t_host, 'ANY' ) ) {
168					return true;
169				}
170				log_event( LOG_EMAIL, "failed - mx/dns record check" );
171			}
172		} else {
173			# Email format was valid but didn't check for valid mx records
174			return true;
175		}
176	} else {
177		log_event( LOG_EMAIL, "failed - invalid address" );
178	}
179
180	# Everything failed.  The email is invalid
181	return false;
182}
183
184/**
185 * Check if the email address is valid trigger an ERROR if it isn't
186 * @param string $p_email An email address.
187 * @throws ClientException
188 * @return void
189 */
190function email_ensure_valid( $p_email ) {
191	if( !email_is_valid( $p_email ) ) {
192		throw new ClientException(
193			sprintf( "Email '%s' is invalid.", $p_email ),
194			ERROR_EMAIL_INVALID );
195	}
196}
197
198/**
199 * Check if the email address is disposable
200 * @param string $p_email An email address.
201 * @return boolean
202 */
203function email_is_disposable( $p_email ) {
204	return \VBoctor\Email\DisposableEmailChecker::is_disposable_email( $p_email );
205}
206
207/**
208 * Check if the email address is disposable
209 * trigger an ERROR if it isn't
210 * @param string $p_email An email address.
211 * @throws ClientException
212 * @return void
213 */
214function email_ensure_not_disposable( $p_email ) {
215	if( email_is_disposable( $p_email ) ) {
216		throw new ClientException(
217			sprintf( "Email '%s' is disposable.", $p_email ),
218			ERROR_EMAIL_DISPOSABLE
219		);
220	}
221}
222
223/**
224 * Get the value associated with the specific action and flag.
225 * For example, you can get the value associated with notifying "admin"
226 * on action "new", i.e. notify administrators on new bugs which can be
227 * ON or OFF.
228 * @param string $p_action Action.
229 * @param string $p_flag   Flag.
230 * @return integer 1 - enabled, 0 - disabled.
231 */
232function email_notify_flag( $p_action, $p_flag ) {
233	# If flag is specified for the specific event, use that.
234	$t_notify_flags = config_get( 'notify_flags' );
235	if( isset( $t_notify_flags[$p_action][$p_flag] ) ) {
236		return $t_notify_flags[$p_action][$p_flag];
237	}
238
239	# If not, then use the default if specified in database or global.
240	# Note that web UI may not support or specify all flags (e.g. explicit),
241	# hence, if config is retrieved from database it may not have the flag.
242	$t_default_notify_flags = config_get( 'default_notify_flags' );
243	if( isset( $t_default_notify_flags[$p_flag] ) ) {
244		return $t_default_notify_flags[$p_flag];
245	}
246
247	# If the flag is not specified so far, then force using global config which
248	# should have all flags specified.
249	$t_global_default_notify_flags = config_get_global( 'default_notify_flags' );
250	if( isset( $t_global_default_notify_flags[$p_flag] ) ) {
251		return $t_global_default_notify_flags[$p_flag];
252	}
253
254	return OFF;
255}
256
257/**
258 * Collect valid email recipients for email notification
259 * @todo yarick123: email_collect_recipients(...) will be completely rewritten to provide additional information such as language, user access,..
260 * @todo yarick123:sort recipients list by language to reduce switches between different languages
261 * @param integer $p_bug_id                  A bug identifier.
262 * @param string  $p_notify_type             Notification type.
263 * @param array   $p_extra_user_ids_to_email Array of additional email addresses to notify.
264 * @param integer $p_bugnote_id The bugnote id in case of bugnote, otherwise null.
265 * @return array
266 */
267function email_collect_recipients( $p_bug_id, $p_notify_type, array $p_extra_user_ids_to_email = array(), $p_bugnote_id = null ) {
268	$t_recipients = array();
269
270	# add explicitly specified users
271	$t_explicit_enabled = ( ON == email_notify_flag( $p_notify_type, 'explicit' ) );
272	foreach ( $p_extra_user_ids_to_email as $t_user_id ) {
273		if ( $t_explicit_enabled ) {
274			$t_recipients[$t_user_id] = true;
275			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, add @U%d (explicitly specified)', $p_bug_id, $t_user_id );
276		} else {
277			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, skip @U%d (explicit disabled)', $p_bug_id, $t_user_id );
278		}
279	}
280
281	# add Reporter
282	$t_reporter_id = bug_get_field( $p_bug_id, 'reporter_id' );
283	if( ON == email_notify_flag( $p_notify_type, 'reporter' ) ) {
284		$t_recipients[$t_reporter_id] = true;
285		log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, add @U%d (reporter)', $p_bug_id, $t_reporter_id );
286	} else {
287		log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, skip @U%d (reporter disabled)', $p_bug_id, $t_reporter_id );
288	}
289
290	# add Handler
291	$t_handler_id = bug_get_field( $p_bug_id, 'handler_id' );
292	if( $t_handler_id > 0 ) {
293		if( ON == email_notify_flag( $p_notify_type, 'handler' ) ) {
294			$t_recipients[$t_handler_id] = true;
295			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, add @U%d (handler)', $p_bug_id, $t_handler_id );
296		} else {
297			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, skip @U%d (handler disabled)', $p_bug_id, $t_handler_id );
298		}
299	}
300
301	$t_project_id = bug_get_field( $p_bug_id, 'project_id' );
302
303	# add users monitoring the bug
304	$t_monitoring_enabled = ON == email_notify_flag( $p_notify_type, 'monitor' );
305	db_param_push();
306	$t_query = 'SELECT DISTINCT user_id FROM {bug_monitor} WHERE bug_id=' . db_param();
307	$t_result = db_query( $t_query, array( $p_bug_id ) );
308
309	while( $t_row = db_fetch_array( $t_result ) ) {
310		$t_user_id = $t_row['user_id'];
311		if ( $t_monitoring_enabled ) {
312			$t_recipients[$t_user_id] = true;
313			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, add @U%d (monitoring)', $p_bug_id, $t_user_id );
314		} else {
315			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, skip @U%d (monitoring disabled)', $p_bug_id, $t_user_id );
316		}
317	}
318
319	# add Category Owner
320	if( ON == email_notify_flag( $p_notify_type, 'category' ) ) {
321		$t_category_id = bug_get_field( $p_bug_id, 'category_id' );
322
323		if( $t_category_id > 0 ) {
324			$t_category_assigned_to = category_get_field( $t_category_id, 'user_id' );
325
326			if( $t_category_assigned_to > 0 ) {
327				$t_recipients[$t_category_assigned_to] = true;
328				log_event( LOG_EMAIL_RECIPIENT, sprintf( 'Issue = #%d, add Category Owner = @U%d', $p_bug_id, $t_category_assigned_to ) );
329			}
330		}
331	}
332
333	# add users who contributed bugnotes
334	$t_notes_enabled = ( ON == email_notify_flag( $p_notify_type, 'bugnotes' ) );
335	db_param_push();
336	$t_query = 'SELECT DISTINCT reporter_id FROM {bugnote} WHERE bug_id = ' . db_param();
337	$t_result = db_query( $t_query, array( $p_bug_id ) );
338	while( $t_row = db_fetch_array( $t_result ) ) {
339		$t_user_id = $t_row['reporter_id'];
340		if ( $t_notes_enabled ) {
341			$t_recipients[$t_user_id] = true;
342			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, add @U%d (note author)', $p_bug_id, $t_user_id );
343		} else {
344			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, skip @U%d (note author disabled)', $p_bug_id, $t_user_id );
345		}
346	}
347
348	# add project users who meet the thresholds
349	$t_bug_is_private = bug_get_field( $p_bug_id, 'view_state' ) == VS_PRIVATE;
350	$t_threshold_min = email_notify_flag( $p_notify_type, 'threshold_min' );
351	$t_threshold_max = email_notify_flag( $p_notify_type, 'threshold_max' );
352	$t_threshold_users = project_get_all_user_rows( $t_project_id, $t_threshold_min );
353	foreach( $t_threshold_users as $t_user ) {
354		if( $t_user['access_level'] <= $t_threshold_max ) {
355			if( !$t_bug_is_private || access_compare_level( $t_user['access_level'], config_get( 'private_bug_threshold' ) ) ) {
356				$t_recipients[$t_user['id']] = true;
357				log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, add @U%d (based on access level)', $p_bug_id, $t_user['id'] );
358			}
359		}
360	}
361
362	# add users as specified by plugins
363	$t_recipients_include_data = event_signal( 'EVENT_NOTIFY_USER_INCLUDE', array( $p_bug_id, $p_notify_type ) );
364	foreach( $t_recipients_include_data as $t_plugin => $t_recipients_include_data2 ) {
365		foreach( $t_recipients_include_data2 as $t_callback => $t_recipients_included ) {
366			# only handle if we get an array from the callback
367			if( is_array( $t_recipients_included ) ) {
368				foreach( $t_recipients_included as $t_user_id ) {
369					$t_recipients[$t_user_id] = true;
370					log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, add @U%d (by %s plugin)', $p_bug_id, $t_user_id, $t_plugin );
371				}
372			}
373		}
374	}
375
376	# FIXME: the value of $p_notify_type could at this stage be either a status
377	# or a built-in actions such as 'owner and 'sponsor'. We have absolutely no
378	# idea whether 'new' is indicating a new bug has been filed, or if the
379	# status of an existing bug has been changed to 'new'. Therefore it is best
380	# to just assume built-in actions have precedence over status changes.
381	switch( $p_notify_type ) {
382		case 'new':
383		case 'feedback': # This isn't really a built-in action (delete me!)
384		case 'reopened':
385		case 'resolved':
386		case 'closed':
387		case 'bugnote':
388			$t_pref_field = 'email_on_' . $p_notify_type;
389			if( !$p_bugnote_id ) {
390				$p_bugnote_id = bugnote_get_latest_id( $p_bug_id );
391			}
392			break;
393		case 'owner':
394			# The email_on_assigned notification type is now effectively
395			# email_on_change_of_handler.
396			$t_pref_field = 'email_on_assigned';
397			break;
398		case 'deleted':
399		case 'updated':
400		case 'sponsor':
401		case 'relation':
402		case 'monitor':
403		case 'priority': # This is never used, but exists in the database!
404			# Issue #19459 these notification actions are not actually implemented
405			# in the database and therefore aren't adjustable on a per-user
406			# basis! The exception is 'monitor' that makes no sense being a
407			# customisable per-user preference.
408		default:
409			# Anything not built-in is probably going to be a status
410			$t_pref_field = 'email_on_status';
411			break;
412	}
413
414	# @@@ we could optimize by modifiying user_cache() to take an array
415	#  of user ids so we could pull them all in.  We'll see if it's necessary
416	$t_final_recipients = array();
417
418	$t_bug = bug_get( $p_bug_id );
419	$t_user_ids = array_keys( $t_recipients );
420	user_cache_array_rows( $t_user_ids );
421	user_pref_cache_array_rows( $t_user_ids );
422	user_pref_cache_array_rows( $t_user_ids, $t_bug->project_id );
423
424	# Check whether users should receive the emails
425	# and put email address to $t_recipients[user_id]
426	foreach( $t_recipients as $t_id => $t_ignore ) {
427		# Possibly eliminate the current user
428		if( ( auth_get_current_user_id() == $t_id ) && ( OFF == config_get( 'email_receive_own' ) ) ) {
429			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, drop @U%d (own action)', $p_bug_id, $t_id );
430			continue;
431		}
432
433		# Eliminate users who don't exist anymore or who are disabled
434		if( !user_exists( $t_id ) || !user_is_enabled( $t_id ) ) {
435			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, drop @U%d (user disabled)', $p_bug_id, $t_id );
436			continue;
437		}
438
439		# Exclude users who have this notification type turned off
440		if( $t_pref_field ) {
441			$t_notify = user_pref_get_pref( $t_id, $t_pref_field );
442			if( OFF == $t_notify ) {
443				log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, drop @U%d (pref %s off)', $p_bug_id, $t_id, $t_pref_field );
444				continue;
445			} else {
446				# Users can define the severity of an issue before they are emailed for
447				# each type of notification
448				$t_min_sev_pref_field = $t_pref_field . '_min_severity';
449				$t_min_sev_notify = user_pref_get_pref( $t_id, $t_min_sev_pref_field );
450				$t_bug_severity = bug_get_field( $p_bug_id, 'severity' );
451
452				if( $t_bug_severity < $t_min_sev_notify ) {
453					log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, drop @U%d (pref threshold)', $p_bug_id, $t_id );
454					continue;
455				}
456			}
457		}
458
459		# exclude users who don't have at least viewer access to the bug,
460		# or who can't see bugnotes if the last update included a bugnote
461		$t_view_bug_threshold = config_get( 'view_bug_threshold', null, $t_id, $t_bug->project_id );
462		if(   !access_has_bug_level( $t_view_bug_threshold, $p_bug_id, $t_id )
463		   || (   $p_bugnote_id
464			   && !access_has_bugnote_level( $t_view_bug_threshold, $p_bugnote_id, $t_id )
465			  )
466		) {
467			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, drop @U%d (access level)', $p_bug_id, $t_id );
468			continue;
469		}
470
471		# check to exclude users as specified by plugins
472		$t_recipient_exclude_data = event_signal( 'EVENT_NOTIFY_USER_EXCLUDE', array( $p_bug_id, $p_notify_type, $t_id ) );
473		$t_exclude = false;
474		foreach( $t_recipient_exclude_data as $t_plugin => $t_recipient_exclude_data2 ) {
475			foreach( $t_recipient_exclude_data2 as $t_callback => $t_recipient_excluded ) {
476				# exclude if any plugin returns true (excludes the user)
477				if( $t_recipient_excluded ) {
478					$t_exclude = true;
479					log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, drop @U%d (by %s plugin)', $p_bug_id, $t_id, $t_plugin );
480				}
481			}
482		}
483
484		# user was excluded by a plugin
485		if( $t_exclude ) {
486			continue;
487		}
488
489		# Finally, let's get their emails, if they've set one
490		$t_email = user_get_email( $t_id );
491		if( is_blank( $t_email ) ) {
492			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, drop @U%d (no email address)', $p_bug_id, $t_id );
493		} else {
494			# @@@ we could check the emails for validity again but I think
495			#   it would be too slow
496			$t_final_recipients[$t_id] = $t_email;
497		}
498	}
499
500	return $t_final_recipients;
501}
502
503/**
504 * Send password to user
505 * @param integer $p_user_id      A valid user identifier.
506 * @param string  $p_confirm_hash Confirmation hash.
507 * @param string  $p_admin_name   Administrator name.
508 * @return void
509 */
510function email_signup( $p_user_id, $p_confirm_hash, $p_admin_name = '' ) {
511	if( ( OFF == config_get( 'send_reset_password' ) ) || ( OFF == config_get( 'enable_email_notification' ) ) ) {
512		return;
513	}
514
515	#	@@@ thraxisp - removed to address #6084 - user won't have any settings yet,
516	#  use same language as display for the email
517	#  lang_push( user_pref_get_language( $p_user_id ) );
518	# retrieve the username and email
519	$t_username = user_get_username( $p_user_id );
520	$t_email = user_get_email( $p_user_id );
521
522	# Build Welcome Message
523	$t_subject = '[' . config_get( 'window_title' ) . '] ' . lang_get( 'new_account_subject' );
524
525	if( !empty( $p_admin_name ) ) {
526		$t_intro_text = sprintf( lang_get( 'new_account_greeting_admincreated' ), $p_admin_name, $t_username );
527	} else {
528		$t_intro_text = sprintf( lang_get( 'new_account_greeting' ), $t_username );
529	}
530
531	$t_message = $t_intro_text . "\n\n" . string_get_confirm_hash_url( $p_user_id, $p_confirm_hash ) . "\n\n" . lang_get( 'new_account_message' ) . "\n\n" . lang_get( 'new_account_do_not_reply' );
532
533	# Send signup email regardless of mail notification pref
534	# or else users won't be able to sign up
535	if( !is_blank( $t_email ) ) {
536		email_store( $t_email, $t_subject, $t_message, null, true );
537		log_event( LOG_EMAIL, 'Signup Email = %s, Hash = %s, User = @U%d', $t_email, $p_confirm_hash, $p_user_id );
538	}
539
540	# lang_pop(); # see above
541}
542
543/**
544 * Send confirm_hash URL to let user reset their password.
545 *
546 * @param integer $p_user_id        A valid user identifier.
547 * @param string  $p_confirm_hash   Confirmation hash.
548 * @param bool    $p_reset_by_admin True if password was reset by admin,
549 *                                  False (default) for user request (lost password)
550 *
551 * @return void
552 */
553function email_send_confirm_hash_url( $p_user_id, $p_confirm_hash, $p_reset_by_admin = false ) {
554	if( OFF == config_get( 'send_reset_password' ) ) {
555		log_event( LOG_EMAIL_VERBOSE, 'Password reset email notifications disabled.' );
556		return;
557	}
558	if( OFF == config_get( 'enable_email_notification' ) ) {
559		log_event( LOG_EMAIL_VERBOSE, 'email notifications disabled.' );
560		return;
561	}
562	if( !user_is_enabled( $p_user_id ) ) {
563		log_event( LOG_EMAIL, 'Password reset for user @U%d not sent, user is disabled', $p_user_id );
564		return;
565	}
566	lang_push( user_pref_get_language( $p_user_id ) );
567
568	# retrieve the username and email
569	$t_username = user_get_username( $p_user_id );
570	$t_email = user_get_email( $p_user_id );
571
572	$t_subject = '[' . config_get( 'window_title' ) . '] ' . lang_get( 'lost_password_subject' );
573
574	if( $p_reset_by_admin ) {
575		$t_message = lang_get( 'reset_request_admin_msg' );
576	} else {
577		$t_message = lang_get( 'reset_request_msg' );
578	}
579	$t_message .= "\n\n"
580		. string_get_confirm_hash_url( $p_user_id, $p_confirm_hash ) . "\n\n"
581		. lang_get( 'new_account_username' ) . ' ' . $t_username . "\n"
582		. lang_get( 'new_account_IP' ) . ' ' . $_SERVER['REMOTE_ADDR'] . "\n\n"
583		. lang_get( 'new_account_do_not_reply' );
584
585	# Send password reset regardless of mail notification preferences
586	# or else users won't be able to receive their reset passwords
587	if( !is_blank( $t_email ) ) {
588		email_store( $t_email, $t_subject, $t_message, null, true );
589		log_event( LOG_EMAIL, 'Password reset for user @U%d sent to %s', $p_user_id, $t_email );
590	} else {
591		log_event( LOG_EMAIL, 'Password reset for user @U%d not sent, email is empty', $p_user_id );
592	}
593
594	lang_pop();
595}
596
597/**
598 * notify the selected group a new user has signup
599 * @param string $p_username Username of new user.
600 * @param string $p_email    Email address of new user.
601 * @return void
602 */
603function email_notify_new_account( $p_username, $p_email ) {
604	log_event( LOG_EMAIL, 'New account for user %s', $p_username );
605
606	$t_threshold_min = config_get( 'notify_new_user_created_threshold_min' );
607	$t_threshold_users = project_get_all_user_rows( ALL_PROJECTS, $t_threshold_min );
608	$t_user_ids = array_keys( $t_threshold_users );
609	user_cache_array_rows( $t_user_ids );
610	user_pref_cache_array_rows( $t_user_ids );
611
612	foreach( $t_threshold_users as $t_user ) {
613		lang_push( user_pref_get_language( $t_user['id'] ) );
614
615		$t_recipient_email = user_get_email( $t_user['id'] );
616		$t_subject = '[' . config_get( 'window_title' ) . '] ' . lang_get( 'new_account_subject' );
617
618		$t_message = lang_get( 'new_account_signup_msg' ) . "\n\n" . lang_get( 'new_account_username' ) . ' ' . $p_username . "\n" . lang_get( 'new_account_email' ) . ' ' . $p_email . "\n" . lang_get( 'new_account_IP' ) . ' ' . $_SERVER['REMOTE_ADDR'] . "\n" . config_get_global( 'path' ) . "\n\n" . lang_get( 'new_account_do_not_reply' );
619
620		if( !is_blank( $t_recipient_email ) ) {
621			email_store( $t_recipient_email, $t_subject, $t_message );
622			log_event( LOG_EMAIL, 'New Account Notify for email = \'%s\'', $t_recipient_email );
623		}
624
625		lang_pop();
626	}
627}
628
629
630/**
631 * send a generic email
632 * $p_notify_type: use check who she get notified of such event.
633 * $p_message_id: message id to be translated and included at the top of the email message.
634 * Return false if it were problems sending email
635 * @param integer $p_bug_id                  A bug identifier.
636 * @param string  $p_notify_type             Notification type.
637 * @param integer $p_message_id              Message identifier.
638 * @param array   $p_header_optional_params  Optional Parameters (default null).
639 * @param array   $p_extra_user_ids_to_email Array of additional users to email.
640 * @return void
641 */
642function email_generic( $p_bug_id, $p_notify_type, $p_message_id = null, array $p_header_optional_params = null, array $p_extra_user_ids_to_email = array() ) {
643	# @todo yarick123: email_collect_recipients(...) will be completely rewritten to provide additional information such as language, user access,..
644	# @todo yarick123:sort recipients list by language to reduce switches between different languages
645	$t_recipients = email_collect_recipients( $p_bug_id, $p_notify_type, $p_extra_user_ids_to_email );
646	email_generic_to_recipients( $p_bug_id, $p_notify_type, $t_recipients, $p_message_id, $p_header_optional_params );
647}
648
649/**
650 * Sends a generic email to the specific set of recipients.
651 *
652 * @param integer $p_bug_id                  A bug identifier
653 * @param string  $p_notify_type             Notification type
654 * @param array   $p_recipients              Array of recipients (key: user id, value: email address)
655 * @param integer $p_message_id              Message identifier
656 * @param array   $p_header_optional_params  Optional Parameters (default null)
657 * @return void
658 */
659function email_generic_to_recipients( $p_bug_id, $p_notify_type, array $p_recipients, $p_message_id = null, array $p_header_optional_params = null ) {
660	if( empty( $p_recipients ) ) {
661		return;
662	}
663
664	if( OFF == config_get( 'enable_email_notification' ) ) {
665		return;
666	}
667
668	ignore_user_abort( true );
669
670	bugnote_get_all_bugnotes( $p_bug_id );
671
672	$t_project_id = bug_get_field( $p_bug_id, 'project_id' );
673
674	if( is_array( $p_recipients ) ) {
675		# send email to every recipient
676		foreach( $p_recipients as $t_user_id => $t_user_email ) {
677			log_event( LOG_EMAIL_VERBOSE, 'Issue = #%d, Type = %s, Msg = \'%s\', User = @U%d, Email = \'%s\'.', $p_bug_id, $p_notify_type, $p_message_id, $t_user_id, $t_user_email );
678
679			# load (push) user language here as build_visible_bug_data assumes current language
680			lang_push( user_pref_get_language( $t_user_id, $t_project_id ) );
681
682			$t_visible_bug_data = email_build_visible_bug_data( $t_user_id, $p_bug_id, $p_message_id );
683			email_bug_info_to_one_user( $t_visible_bug_data, $p_message_id, $t_user_id, $p_header_optional_params );
684
685			lang_pop();
686		}
687	}
688}
689
690/**
691 * Send notices that a user is now monitoring the bug.  Typically this will only be sent when the added
692 * user is not the logged in user.  This is assuming that receive own notifications is OFF (default).
693 * @param integer $p_bug_id  A valid bug identifier.
694 * @param integer $p_user_id A valid user identifier.
695 * @return void
696 */
697function email_monitor_added( $p_bug_id, $p_user_id ) {
698	log_event( LOG_EMAIL, 'Issue #%d monitored by user @U%d', $p_bug_id, $p_user_id );
699
700	$t_opt = array();
701	$t_opt[] = bug_format_id( $p_bug_id );
702	$t_opt[] = user_get_name( $p_user_id );
703
704	email_generic( $p_bug_id, 'monitor', 'email_notification_title_for_action_monitor', $t_opt, array( $p_user_id ) );
705}
706
707/**
708 * send notices when a relationship is ADDED
709 * @param integer $p_bug_id         A bug identifier.
710 * @param integer $p_related_bug_id Related bug identifier.
711 * @param integer $p_rel_type       Relationship type.
712 * @param bool $p_email_for_source     Should an email be triggered for source issue?
713 * @return void
714 */
715function email_relationship_added( $p_bug_id, $p_related_bug_id, $p_rel_type, $p_email_for_source ) {
716	global $g_relationships;
717
718	if( !isset( $g_relationships[$p_rel_type] ) ) {
719		trigger_error( ERROR_RELATIONSHIP_NOT_FOUND, ERROR );
720	}
721
722	$t_rev_rel_type = relationship_get_complementary_type( $p_rel_type );
723	if( !isset( $g_relationships[$t_rev_rel_type] ) ) {
724		trigger_error( ERROR_RELATIONSHIP_NOT_FOUND, ERROR );
725	}
726
727	log_event(
728		LOG_EMAIL,
729		'Issue #%d relationship added to issue #%d (relationship type %s)',
730		$p_bug_id,
731		$p_related_bug_id,
732		$g_relationships[$p_rel_type]['#description'] );
733
734	# Source issue email notification
735	if( $p_email_for_source ) {
736		$t_recipients = email_collect_recipients( $p_bug_id, 'relation' );
737
738		# Recipient has to have access to both bugs to get the notification.
739		$t_recipients = email_filter_recipients_for_bug( $p_bug_id, $t_recipients );
740		$t_recipients = email_filter_recipients_for_bug( $p_related_bug_id, $t_recipients );
741
742		$t_opt = array();
743		$t_opt[] = bug_format_id( $p_related_bug_id );
744
745		email_generic_to_recipients(
746			$p_bug_id, 'relation', $t_recipients, $g_relationships[$p_rel_type]['#notify_added'], $t_opt );
747	}
748
749	# Destination issue email notification
750	$t_recipients = email_collect_recipients( $p_related_bug_id, 'relation' );
751
752	# Recipient has to have access to both bugs to get the notification.
753	$t_recipients = email_filter_recipients_for_bug( $p_bug_id, $t_recipients );
754	$t_recipients = email_filter_recipients_for_bug( $p_related_bug_id, $t_recipients );
755
756	$t_opt = array();
757	$t_opt[] = bug_format_id( $p_bug_id );
758	email_generic_to_recipients(
759		$p_related_bug_id, 'relation', $t_recipients, $g_relationships[$t_rev_rel_type]['#notify_added'], $t_opt );
760}
761
762/**
763 * Filter recipients to remove ones that don't have access to the specified bug.
764 *
765 * @param integer $p_bug_id       The bug id
766 * @param array   $p_recipients   The recipients array (key: id, value: email)
767 * @return array The filtered list of recipients in same format
768 * @access private
769 */
770function email_filter_recipients_for_bug( $p_bug_id, array $p_recipients ) {
771    $t_view_bug_threshold = config_get( 'view_bug_threshold' );
772
773    $t_authorized_recipients = array();
774
775    foreach( $p_recipients as $t_recipient_id => $t_recipient_email ) {
776        if( access_has_bug_level( $t_view_bug_threshold, $p_bug_id, $t_recipient_id ) ) {
777            $t_authorized_recipients[$t_recipient_id] = $t_recipient_email;
778        }
779    }
780
781    return $t_authorized_recipients;
782}
783
784/**
785 * send notices when a relationship is DELETED
786 * @param integer $p_bug_id         A bug identifier.
787 * @param integer $p_related_bug_id Related bug identifier.
788 * @param integer $p_rel_type       Relationship type.
789 * @param integer $p_skip_email_for_issue_id Skip email for specified issue, otherwise 0.
790 * @return void
791 */
792function email_relationship_deleted( $p_bug_id, $p_related_bug_id, $p_rel_type, $p_skip_email_for_issue_id = 0 ) {
793	global $g_relationships;
794	if( !isset( $g_relationships[$p_rel_type] ) ) {
795		trigger_error( ERROR_RELATIONSHIP_NOT_FOUND, ERROR );
796	}
797
798	$t_rev_rel_type = relationship_get_complementary_type( $p_rel_type );
799	if( !isset( $g_relationships[$t_rev_rel_type] ) ) {
800		trigger_error( ERROR_RELATIONSHIP_NOT_FOUND, ERROR );
801	}
802
803	log_event(
804		LOG_EMAIL,
805		'Issue #%d relationship to issue #%d (relationship type %s) deleted.',
806		$p_bug_id,
807		$p_related_bug_id,
808		$g_relationships[$p_rel_type]['#description'] );
809
810	if( $p_bug_id != $p_skip_email_for_issue_id ) {
811		$t_recipients = email_collect_recipients( $p_bug_id, 'relation' );
812
813		# Recipient has to have access to both bugs to get the notification.
814		$t_recipients = email_filter_recipients_for_bug( $p_bug_id, $t_recipients );
815		$t_recipients = email_filter_recipients_for_bug( $p_related_bug_id, $t_recipients );
816
817		$t_opt = array();
818		$t_opt[] = bug_format_id( $p_related_bug_id );
819		email_generic_to_recipients(
820			$p_bug_id,
821			'relation',
822			$t_recipients,
823			$g_relationships[$p_rel_type]['#notify_deleted'],
824			$t_opt );
825	}
826
827	if( $p_bug_id != $p_related_bug_id && bug_exists( $p_related_bug_id) ) {
828		$t_recipients = email_collect_recipients( $p_related_bug_id, 'relation' );
829
830		# Recipient has to have access to both bugs to get the notification.
831		$t_recipients = email_filter_recipients_for_bug( $p_bug_id, $t_recipients );
832		$t_recipients = email_filter_recipients_for_bug( $p_related_bug_id, $t_recipients );
833
834		$t_opt = array();
835		$t_opt[] = bug_format_id( $p_bug_id );
836		email_generic_to_recipients(
837			$p_related_bug_id,
838			'relation',
839			$t_recipients,
840			$g_relationships[$t_rev_rel_type]['#notify_deleted'],
841			$t_opt );
842	}
843}
844
845/**
846 * Email related issues when a bug is deleted.  This should be deleted before the bug is deleted.
847 *
848 * @param integer $p_bug_id The id of the bug to be deleted.
849 * @return void
850 */
851function email_relationship_bug_deleted( $p_bug_id ) {
852	$t_ignore = false;
853	$t_relationships = relationship_get_all( $p_bug_id, $t_ignore );
854	if( empty( $t_relationships ) ) {
855		return;
856	}
857
858	log_event( LOG_EMAIL, sprintf( 'Issue #%d has been deleted, sending notifications to related issues', $p_bug_id ) );
859
860	foreach( $t_relationships as $t_relationship ) {
861		$t_related_bug_id = $p_bug_id == $t_relationship->src_bug_id ?
862			$t_relationship->dest_bug_id : $t_relationship->src_bug_id;
863
864		$t_opt = array();
865		$t_opt[] = bug_format_id( $p_bug_id );
866		email_generic( $t_related_bug_id, 'handler', 'email_notification_title_for_action_related_issue_deleted', $t_opt );
867	}
868}
869
870/**
871 * send notices to all the handlers of the parent bugs when a child bug is RESOLVED
872 * @param integer $p_bug_id A bug identifier.
873 * @return void
874 */
875function email_relationship_child_resolved( $p_bug_id ) {
876	email_relationship_child_resolved_closed( $p_bug_id, 'email_notification_title_for_action_relationship_child_resolved' );
877}
878
879/**
880 * send notices to all the handlers of the parent bugs when a child bug is CLOSED
881 * @param integer $p_bug_id A bug identifier.
882 * @return void
883 */
884function email_relationship_child_closed( $p_bug_id ) {
885	email_relationship_child_resolved_closed( $p_bug_id, 'email_notification_title_for_action_relationship_child_closed' );
886}
887
888/**
889 * send notices to all the handlers of the parent bugs still open when a child bug is resolved/closed
890 *
891 * @param integer $p_bug_id     A bug identifier.
892 * @param integer $p_message_id A message identifier.
893 * @return void
894 */
895function email_relationship_child_resolved_closed( $p_bug_id, $p_message_id ) {
896	# retrieve all the relationships in which the bug is the destination bug
897	$t_relationship = relationship_get_all_dest( $p_bug_id );
898	$t_relationship_count = count( $t_relationship );
899	if( $t_relationship_count == 0 ) {
900		# no parent bug found
901		return;
902	}
903
904	if( $p_message_id == 'email_notification_title_for_action_relationship_child_closed' ) {
905		log_event( LOG_EMAIL, sprintf( 'Issue #%d child issue closed', $p_bug_id ) );
906	} else {
907		log_event( LOG_EMAIL, sprintf( 'Issue #%d child issue resolved', $p_bug_id ) );
908	}
909
910	for( $i = 0;$i < $t_relationship_count;$i++ ) {
911		if( $t_relationship[$i]->type == BUG_DEPENDANT ) {
912			$t_src_bug_id = $t_relationship[$i]->src_bug_id;
913			$t_status = bug_get_field( $t_src_bug_id, 'status' );
914			if( $t_status < config_get( 'bug_resolved_status_threshold' ) ) {
915
916				# sent the notification just for parent bugs not resolved/closed
917				$t_opt = array();
918				$t_opt[] = bug_format_id( $p_bug_id );
919				email_generic( $t_src_bug_id, 'handler', $p_message_id, $t_opt );
920			}
921		}
922	}
923}
924
925/**
926 * send notices when a bug is sponsored
927 * @param int $p_bug_id
928 * @return void
929 */
930function email_sponsorship_added( $p_bug_id ) {
931	log_event( LOG_EMAIL, sprintf( 'Issue #%d sponsorship added', $p_bug_id ) );
932	email_generic( $p_bug_id, 'sponsor', 'email_notification_title_for_action_sponsorship_added' );
933}
934
935/**
936 * send notices when a sponsorship is modified
937 * @param int $p_bug_id
938 * @return void
939 */
940function email_sponsorship_updated( $p_bug_id ) {
941	log_event( LOG_EMAIL, sprintf( 'Issue #%d sponsorship updated', $p_bug_id ) );
942	email_generic( $p_bug_id, 'sponsor', 'email_notification_title_for_action_sponsorship_updated' );
943}
944
945/**
946 * send notices when a sponsorship is deleted
947 * @param int $p_bug_id
948 * @return void
949 */
950function email_sponsorship_deleted( $p_bug_id ) {
951	log_event( LOG_EMAIL, sprintf( 'Issue #%d sponsorship removed', $p_bug_id ) );
952	email_generic( $p_bug_id, 'sponsor', 'email_notification_title_for_action_sponsorship_deleted' );
953}
954
955/**
956 * send notices when a new bug is added
957 * @param int $p_bug_id
958 * @return void
959 */
960function email_bug_added( $p_bug_id ) {
961	log_event( LOG_EMAIL, sprintf( 'Issue #%d reported', $p_bug_id ) );
962	email_generic( $p_bug_id, 'new', 'email_notification_title_for_action_bug_submitted' );
963}
964
965/**
966 * Send notifications for bug update.
967 * @param int $p_bug_id  The bug id.
968 * @return void
969 */
970function email_bug_updated( $p_bug_id ) {
971	log_event( LOG_EMAIL, sprintf( 'Issue #%d updated', $p_bug_id ) );
972	email_generic( $p_bug_id, 'updated', 'email_notification_title_for_action_bug_updated' );
973}
974
975/**
976 * send notices when a new bugnote
977 * @param int $p_bugnote_id  The bugnote id.
978 * @param array $p_files The array of file information (keys: name, size)
979 * @param array $p_exclude_user_ids The id of users to exclude.
980 * @return void
981 */
982function email_bugnote_add( $p_bugnote_id, $p_files = array(), $p_exclude_user_ids = array() ) {
983	if( OFF == config_get( 'enable_email_notification' ) ) {
984		log_event( LOG_EMAIL_VERBOSE, 'email notifications disabled.' );
985		return;
986	}
987
988	ignore_user_abort( true );
989
990	$t_bugnote = bugnote_get( $p_bugnote_id );
991
992	log_event( LOG_EMAIL, sprintf( 'Note ~%d added to issue #%d', $p_bugnote_id, $t_bugnote->bug_id ) );
993
994	$t_project_id = bug_get_field( $t_bugnote->bug_id, 'project_id' );
995	$t_separator = config_get( 'email_separator2' );
996	$t_time_tracking_access_threshold = config_get( 'time_tracking_view_threshold' );
997	$t_view_attachments_threshold = config_get( 'view_attachments_threshold' );
998	$t_message_id = 'email_notification_title_for_action_bugnote_submitted';
999
1000	$t_subject = email_build_subject( $t_bugnote->bug_id );
1001
1002	$t_recipients = email_collect_recipients( $t_bugnote->bug_id, 'bugnote', /* extra_user_ids */ array(), $p_bugnote_id );
1003	$t_recipients_verbose = array();
1004
1005	# send email to every recipient
1006	foreach( $t_recipients as $t_user_id => $t_user_email ) {
1007		if( in_array( $t_user_id, $p_exclude_user_ids ) ) {
1008			log_event( LOG_EMAIL_RECIPIENT, 'Issue = #%d, Note = ~%d, Type = %s, Msg = \'%s\', User = @U%d excluded, Email = \'%s\'.',
1009				$t_bugnote->bug_id, $p_bugnote_id, 'bugnote', 'email_notification_title_for_action_bugnote_submitted', $t_user_id, $t_user_email );
1010			continue;
1011		}
1012
1013		# Load this here per user to allow overriding this per user, or even per user per project
1014		if( config_get( 'email_notifications_verbose', /* default */ null, $t_user_id, $t_project_id ) == ON ) {
1015			$t_recipients_verbose[$t_user_id] = $t_user_email;
1016			continue;
1017		}
1018
1019		log_event( LOG_EMAIL_VERBOSE, 'Issue = #%d, Note = ~%d, Type = %s, Msg = \'%s\', User = @U%d, Email = \'%s\'.',
1020			$t_bugnote->bug_id, $p_bugnote_id, 'bugnote', $t_message_id, $t_user_id, $t_user_email );
1021
1022		# load (push) user language
1023		lang_push( user_pref_get_language( $t_user_id, $t_project_id ) );
1024
1025		$t_message = lang_get( 'email_notification_title_for_action_bugnote_submitted' ) . "\n\n";
1026
1027		$t_show_time_tracking = access_has_bug_level( $t_time_tracking_access_threshold, $t_bugnote->bug_id, $t_user_id );
1028		$t_formatted_note = email_format_bugnote( $t_bugnote, $t_project_id, $t_show_time_tracking, $t_separator );
1029		$t_message .= trim( $t_formatted_note ) . "\n";
1030		$t_message .= $t_separator . "\n";
1031
1032		# Files attached
1033		if( count( $p_files ) > 0 &&
1034			access_has_bug_level( $t_view_attachments_threshold, $t_bugnote->bug_id, $t_user_id ) ) {
1035			$t_message .= lang_get( 'bugnote_attached_files' ) . "\n";
1036
1037			foreach( $p_files as $t_file ) {
1038				$t_message .= '- ' . $t_file['name'] . ' (' . number_format( $t_file['size'] ) .
1039					' ' . lang_get( 'bytes' ) . ")\n";
1040			}
1041
1042			$t_message .= $t_separator . "\n";
1043		}
1044
1045		$t_contents = $t_message . "\n";
1046
1047		email_store( $t_user_email, $t_subject, $t_contents );
1048
1049		log_event( LOG_EMAIL_VERBOSE, 'queued bugnote email for note ~' . $p_bugnote_id .
1050			' issue #' . $t_bugnote->bug_id . ' by U' . $t_user_id );
1051
1052		lang_pop();
1053	}
1054
1055	# Send emails out for users that select verbose notifications
1056	email_generic_to_recipients(
1057		$t_bugnote->bug_id,
1058		'bugnote',
1059		$t_recipients_verbose,
1060		$t_message_id );
1061}
1062
1063/**
1064 * send notices when a bug is RESOLVED
1065 * @param int $p_bug_id
1066 * @return void
1067 */
1068function email_resolved( $p_bug_id ) {
1069	log_event( LOG_EMAIL, sprintf( 'Issue #%d resolved', $p_bug_id ) );
1070	email_generic( $p_bug_id, 'resolved', 'email_notification_title_for_status_bug_resolved' );
1071}
1072
1073/**
1074 * send notices when a bug is CLOSED
1075 * @param int $p_bug_id
1076 * @return void
1077 */
1078function email_close( $p_bug_id ) {
1079	log_event( LOG_EMAIL, sprintf( 'Issue #%d closed', $p_bug_id ) );
1080	email_generic( $p_bug_id, 'closed', 'email_notification_title_for_status_bug_closed' );
1081}
1082
1083/**
1084 * send notices when a bug is REOPENED
1085 * @param int $p_bug_id
1086 * @return void
1087 */
1088function email_bug_reopened( $p_bug_id ) {
1089	log_event( LOG_EMAIL, sprintf( 'Issue #%d reopened', $p_bug_id ) );
1090	email_generic( $p_bug_id, 'reopened', 'email_notification_title_for_action_bug_reopened' );
1091}
1092
1093/**
1094 * Send notices when a bug handler is changed.
1095 * @param int $p_bug_id
1096 * @param int $p_prev_handler_id
1097 * @param int $p_new_handler_id
1098 * @return void
1099 */
1100function email_owner_changed($p_bug_id, $p_prev_handler_id, $p_new_handler_id ) {
1101	if ( $p_prev_handler_id == 0 && $p_new_handler_id != 0 ) {
1102		log_event( LOG_EMAIL, sprintf( 'Issue #%d assigned to user @U%d.', $p_bug_id, $p_new_handler_id ) );
1103	} else if ( $p_prev_handler_id != 0 && $p_new_handler_id == 0 ) {
1104		log_event( LOG_EMAIL, sprintf( 'Issue #%d is no longer assigned to @U%d.', $p_bug_id, $p_prev_handler_id ) );
1105	} else {
1106		log_event(
1107			LOG_EMAIL,
1108			sprintf(
1109				'Issue #%d is assigned to @U%d instead of @U%d.',
1110				$p_bug_id,
1111				$p_new_handler_id,
1112				$p_prev_handler_id )
1113		);
1114	}
1115
1116	$t_message_id = $p_new_handler_id == NO_USER ?
1117			'email_notification_title_for_action_bug_unassigned' :
1118			'email_notification_title_for_action_bug_assigned';
1119
1120	$t_extra_user_ids_to_email = array();
1121	if ( $p_prev_handler_id !== NO_USER && $p_prev_handler_id != $p_new_handler_id ) {
1122		if ( email_notify_flag( 'owner', 'handler' ) == ON ) {
1123			$t_extra_user_ids_to_email[] = $p_prev_handler_id;
1124		}
1125	}
1126
1127	email_generic( $p_bug_id, 'owner', $t_message_id, /* headers */ null, $t_extra_user_ids_to_email );
1128}
1129
1130/**
1131 * Send notifications when bug status is changed.
1132 * @param int $p_bug_id The bug id
1133 * @param string $p_new_status_label The new status label.
1134 * @return void
1135 */
1136function email_bug_status_changed( $p_bug_id, $p_new_status_label ) {
1137	log_event( LOG_EMAIL, sprintf( 'Issue #%d status changed', $p_bug_id ) );
1138	email_generic( $p_bug_id, $p_new_status_label, 'email_notification_title_for_status_bug_' . $p_new_status_label );
1139}
1140
1141/**
1142 * send notices when a bug is DELETED
1143 * @param int $p_bug_id
1144 * @return void
1145 */
1146function email_bug_deleted( $p_bug_id ) {
1147	log_event( LOG_EMAIL, sprintf( 'Issue #%d deleted', $p_bug_id ) );
1148	email_generic( $p_bug_id, 'deleted', 'email_notification_title_for_action_bug_deleted' );
1149}
1150
1151/**
1152 * Store email in queue for sending
1153 *
1154 * @param string  $p_recipient Email recipient address.
1155 * @param string  $p_subject   Subject of email message.
1156 * @param string  $p_message   Body text of email message.
1157 * @param array   $p_headers   Array of additional headers to send with the email.
1158 * @param boolean $p_force     True to force sending of emails in shutdown function,
1159 *                             even when using cronjob
1160 * @return integer|null
1161 */
1162function email_store( $p_recipient, $p_subject, $p_message, array $p_headers = null, $p_force = false ) {
1163	global $g_email_shutdown_processing;
1164
1165	$t_recipient = trim( $p_recipient );
1166	$t_subject = string_email( trim( $p_subject ) );
1167	$t_message = string_email_links( trim( $p_message ) );
1168
1169	# short-circuit if no recipient is defined, or email disabled
1170	# note that this may cause signup messages not to be sent
1171	if( is_blank( $p_recipient ) || ( OFF == config_get( 'enable_email_notification' ) ) ) {
1172		return null;
1173	}
1174
1175	$t_email_data = new EmailData;
1176
1177	$t_email_data->email = $t_recipient;
1178	$t_email_data->subject = $t_subject;
1179	$t_email_data->body = $t_message;
1180	$t_email_data->metadata = array();
1181	$t_email_data->metadata['headers'] = $p_headers === null ? array() : $p_headers;
1182
1183	# Urgent = 1, Not Urgent = 5, Disable = 0
1184	$t_email_data->metadata['charset'] = 'utf-8';
1185
1186	$t_hostname = '';
1187	if( isset( $_SERVER['SERVER_NAME'] ) ) {
1188		$t_hostname = $_SERVER['SERVER_NAME'];
1189	} else {
1190		$t_address = explode( '@', config_get( 'from_email' ) );
1191		if( isset( $t_address[1] ) ) {
1192			$t_hostname = $t_address[1];
1193		}
1194	}
1195	$t_email_data->metadata['hostname'] = $t_hostname;
1196
1197	$t_email_id = email_queue_add( $t_email_data );
1198
1199	# Set the email processing flag for the shutdown function
1200	$g_email_shutdown_processing |= EMAIL_SHUTDOWN_GENERATED;
1201	if( $p_force ) {
1202		$g_email_shutdown_processing |= EMAIL_SHUTDOWN_FORCE;
1203	}
1204
1205	return $t_email_id;
1206}
1207
1208/**
1209 * This function sends all the emails that are stored in the queue.
1210 * It will be called
1211 * - immediately after queueing messages in case of synchronous emails
1212 * - from a cronjob in case of asynchronous emails
1213 * If a failure occurs, then the function exits.
1214 * @todo In case of synchronous email sending, we may get a race condition where two requests send the same email.
1215 * @param boolean $p_delete_on_failure Indicates whether to remove email from queue on failure (default false).
1216 * @return void
1217 */
1218function email_send_all( $p_delete_on_failure = false ) {
1219	$t_ids = email_queue_get_ids();
1220
1221	log_event( LOG_EMAIL_VERBOSE, 'Processing e-mail queue (' . count( $t_ids ) . ' messages)' );
1222
1223	foreach( $t_ids as $t_id ) {
1224		$t_email_data = email_queue_get( $t_id );
1225		$t_start = microtime( true );
1226
1227		# check if email was not found.  This can happen if another request picks up the email first and sends it.
1228		if( $t_email_data === false ) {
1229			$t_email_sent = true;
1230			log_event( LOG_EMAIL_VERBOSE, 'Message $t_id has already been sent' );
1231		} else {
1232			log_event( LOG_EMAIL_VERBOSE, 'Sending message ' . $t_id );
1233			$t_email_sent = email_send( $t_email_data );
1234		}
1235
1236		if( !$t_email_sent ) {
1237			# Delete emails that were submitted more than N days ago
1238			$t_submitted = (int)$t_email_data->submitted;
1239			$t_delete_after_in_days = (int)config_get_global( 'email_retry_in_days' );
1240			$t_retry_cutoff = time() - ( $t_delete_after_in_days * 24 * 60 * 60 );
1241			if( $p_delete_on_failure || $t_submitted < $t_retry_cutoff ) {
1242				$t_reason = $p_delete_on_failure ? 'delete on failure' : 'retry expired';
1243				email_queue_delete( $t_email_data->email_id, $t_reason );
1244			}
1245
1246			# If unable to place the email in the email server queue and more
1247			# than 5 seconds have elapsed, then we assume that the server
1248			# connection is down, hence no point to continue trying with the
1249			# rest of the emails.
1250			if( microtime( true ) - $t_start > 5 ) {
1251				log_event( LOG_EMAIL, 'Server not responding for 5 seconds, aborting' );
1252				break;
1253			}
1254		}
1255	}
1256}
1257
1258/**
1259 * This function sends an email message based on the supplied email data.
1260 *
1261 * @param EmailData $p_email_data Email Data object representing the email to send.
1262 * @return boolean
1263 */
1264function email_send( EmailData $p_email_data ) {
1265	global $g_phpMailer;
1266
1267	$t_email_data = $p_email_data;
1268
1269	$t_recipient = trim( $t_email_data->email );
1270	$t_subject = string_email( trim( $t_email_data->subject ) );
1271	$t_message = string_email_links( trim( $t_email_data->body ) );
1272
1273	$t_debug_email = config_get_global( 'debug_email' );
1274	$t_mailer_method = config_get( 'phpMailer_method' );
1275
1276	$t_log_msg = 'ERROR: Message could not be sent - ';
1277
1278	if( is_null( $g_phpMailer ) ) {
1279		if( $t_mailer_method == PHPMAILER_METHOD_SMTP ) {
1280			register_shutdown_function( 'email_smtp_close' );
1281		}
1282		$t_mail = new PHPMailer( true );
1283
1284		// Set e-mail addresses validation pattern. The 'html5' setting is
1285		// consistent with the regex defined in email_regex_simple().
1286		PHPMailer::$validator  = 'html5';
1287
1288	} else {
1289		$t_mail = $g_phpMailer;
1290	}
1291
1292	if( isset( $t_email_data->metadata['hostname'] ) ) {
1293		$t_mail->Hostname = $t_email_data->metadata['hostname'];
1294	}
1295
1296	# @@@ should this be the current language (for the recipient) or the default one (for the user running the command) (thraxisp)
1297	$t_lang = config_get_global( 'default_language' );
1298	if( 'auto' == $t_lang ) {
1299		$t_lang = config_get_global( 'fallback_language' );
1300	}
1301	$t_mail->setLanguage( lang_get( 'phpmailer_language', $t_lang ) );
1302
1303	# Select the method to send mail
1304	switch( config_get( 'phpMailer_method' ) ) {
1305		case PHPMAILER_METHOD_MAIL:
1306			$t_mail->isMail();
1307			break;
1308
1309		case PHPMAILER_METHOD_SENDMAIL:
1310			$t_mail->isSendmail();
1311			break;
1312
1313		case PHPMAILER_METHOD_SMTP:
1314			$t_mail->isSMTP();
1315
1316			# SMTP collection is always kept alive
1317			$t_mail->SMTPKeepAlive = true;
1318
1319			if( !is_blank( config_get( 'smtp_username' ) ) ) {
1320				# Use SMTP Authentication
1321				$t_mail->SMTPAuth = true;
1322				$t_mail->Username = config_get( 'smtp_username' );
1323				$t_mail->Password = config_get( 'smtp_password' );
1324			}
1325
1326			if( is_blank( config_get( 'smtp_connection_mode' ) ) ) {
1327				$t_mail->SMTPAutoTLS = false;
1328			}
1329			else {
1330				$t_mail->SMTPSecure = config_get( 'smtp_connection_mode' );
1331			}
1332
1333			$t_mail->Port = config_get( 'smtp_port' );
1334
1335			break;
1336	}
1337
1338	# S/MIME signature
1339	if( ON == config_get_global( 'email_smime_enable' ) ) {
1340		$t_mail->sign(
1341			config_get_global( 'email_smime_cert_file' ),
1342			config_get_global( 'email_smime_key_file' ),
1343			config_get_global( 'email_smime_key_password' ),
1344			config_get_global( 'email_smime_extracerts_file' )
1345		);
1346	}
1347
1348	#apply DKIM settings
1349	if( config_get_global( 'email_dkim_enable' ) ) {
1350		$t_mail->DKIM_domain = config_get_global( 'email_dkim_domain' );
1351		$t_mail->DKIM_private = config_get_global( 'email_dkim_private_key_file_path' );
1352		$t_mail->DKIM_private_string = config_get_global( 'email_dkim_private_key_string' );
1353		$t_mail->DKIM_selector = config_get_global( 'email_dkim_selector' );
1354		$t_mail->DKIM_passphrase = config_get_global( 'email_dkim_passphrase' );
1355		$t_mail->DKIM_identity = config_get_global( 'email_dkim_identity' );
1356	}
1357
1358	$t_mail->isHTML( false );              # set email format to plain text
1359	$t_mail->WordWrap = 80;              # set word wrap to 80 characters
1360	$t_mail->CharSet = $t_email_data->metadata['charset'];
1361	$t_mail->Host = config_get( 'smtp_host' );
1362	$t_mail->From = config_get( 'from_email' );
1363	$t_mail->Sender = config_get( 'return_path_email' );
1364	$t_mail->FromName = config_get( 'from_name' );
1365	$t_mail->AddCustomHeader( 'Auto-Submitted:auto-generated' );
1366	$t_mail->AddCustomHeader( 'X-Auto-Response-Suppress: All' );
1367
1368	$t_mail->Encoding   = 'quoted-printable';
1369
1370	if( isset( $t_email_data->metadata['priority'] ) ) {
1371		$t_mail->Priority = $t_email_data->metadata['priority'];  # Urgent = 1, Not Urgent = 5, Disable = 0
1372	}
1373
1374	if( !empty( $t_debug_email ) ) {
1375		$t_message = 'To: ' . $t_recipient . "\n\n" . $t_message;
1376		$t_recipient = $t_debug_email;
1377		log_event(LOG_EMAIL_VERBOSE, "Using debug email '$t_debug_email'");
1378	}
1379
1380	try {
1381		$t_mail->addAddress( $t_recipient, '' );
1382	}
1383	catch ( phpmailerException $e ) {
1384		log_event( LOG_EMAIL, $t_log_msg . $t_mail->ErrorInfo );
1385		$t_success = false;
1386		$t_mail->clearAllRecipients();
1387		$t_mail->clearAttachments();
1388		$t_mail->clearReplyTos();
1389		$t_mail->clearCustomHeaders();
1390		return $t_success;
1391	}
1392
1393	$t_mail->Subject = $t_subject;
1394	$t_mail->Body = make_lf_crlf( $t_message );
1395
1396	if( isset( $t_email_data->metadata['headers'] ) && is_array( $t_email_data->metadata['headers'] ) ) {
1397		foreach( $t_email_data->metadata['headers'] as $t_key => $t_value ) {
1398			switch( $t_key ) {
1399				case 'Message-ID':
1400					# Note: hostname can never be blank here as we set metadata['hostname']
1401					# in email_store() where mail gets queued.
1402					if( !strchr( $t_value, '@' ) && !is_blank( $t_mail->Hostname ) ) {
1403						$t_value = $t_value . '@' . $t_mail->Hostname;
1404					}
1405					$t_mail->set( 'MessageID', '<' . $t_value . '>' );
1406					break;
1407				case 'In-Reply-To':
1408					$t_mail->addCustomHeader( $t_key . ': <' . $t_value . '@' . $t_mail->Hostname . '>' );
1409					break;
1410				default:
1411					$t_mail->addCustomHeader( $t_key . ': ' . $t_value );
1412					break;
1413			}
1414		}
1415	}
1416
1417	try {
1418		$t_success = $t_mail->send();
1419		if( $t_success ) {
1420			$t_success = true;
1421
1422			if( $t_email_data->email_id > 0 ) {
1423				email_queue_delete( $t_email_data->email_id );
1424			}
1425		} else {
1426			# We should never get here, as an exception is thrown after failures
1427			log_event( LOG_EMAIL, $t_log_msg . $t_mail->ErrorInfo );
1428			$t_success = false;
1429		}
1430	}
1431	catch ( phpmailerException $e ) {
1432		log_event( LOG_EMAIL, $t_log_msg . $t_mail->ErrorInfo );
1433		$t_success = false;
1434	}
1435
1436	$t_mail->clearAllRecipients();
1437	$t_mail->clearAttachments();
1438	$t_mail->clearReplyTos();
1439	$t_mail->clearCustomHeaders();
1440
1441	return $t_success;
1442}
1443
1444/**
1445 * closes opened kept alive SMTP connection (if it was opened)
1446 *
1447 * @return void
1448 */
1449function email_smtp_close() {
1450	global $g_phpMailer;
1451
1452	if( !is_null( $g_phpMailer ) ) {
1453		$t_smtp = $g_phpMailer->getSMTPInstance();
1454		if( $t_smtp->connected() ) {
1455			$t_smtp->quit();
1456			$t_smtp->close();
1457		}
1458		$g_phpMailer = null;
1459	}
1460}
1461
1462/**
1463 * formats the subject correctly
1464 * we include the project name, bug id, and summary.
1465 *
1466 * @param integer $p_bug_id A bug identifier.
1467 * @return string
1468 */
1469function email_build_subject( $p_bug_id ) {
1470	# grab the project name
1471	$p_project_name = project_get_field( bug_get_field( $p_bug_id, 'project_id' ), 'name' );
1472
1473	# grab the subject (summary)
1474	$p_subject = bug_get_field( $p_bug_id, 'summary' );
1475
1476	# pad the bug id with zeros
1477	$t_bug_id = bug_format_id( $p_bug_id );
1478
1479	# build standard subject string
1480	$t_email_subject = '[' . $p_project_name . ' ' . $t_bug_id . ']: ' . $p_subject;
1481
1482	# update subject as defined by plugins
1483	$t_email_subject = event_signal( 'EVENT_DISPLAY_EMAIL_BUILD_SUBJECT', $t_email_subject, array( 'bug_id' => $p_bug_id ) );
1484
1485	return $t_email_subject;
1486}
1487
1488/**
1489 * clean up LF to CRLF
1490 *
1491 * @param string $p_string String to convert linefeeds on.
1492 * @return string
1493 */
1494function make_lf_crlf( $p_string ) {
1495	$t_string = str_replace( "\n", "\r\n", $p_string );
1496	return str_replace( "\r\r\n", "\r\n", $t_string );
1497}
1498
1499/**
1500 * Send a bug reminder to the given user(s), or to each user if the first parameter is an array
1501 *
1502 * @param integer|array $p_recipients User id or list of user ids array to send reminder to.
1503 * @param integer       $p_bug_id     Issue for which the reminder is sent.
1504 * @param string        $p_message    Optional message to add to the e-mail.
1505 * @return array List of users ids to whom the reminder e-mail was actually sent
1506 */
1507function email_bug_reminder( $p_recipients, $p_bug_id, $p_message ) {
1508	if( OFF == config_get( 'enable_email_notification' ) ) {
1509		return array();
1510	}
1511
1512	if( !is_array( $p_recipients ) ) {
1513		$p_recipients = array(
1514			$p_recipients,
1515		);
1516	}
1517
1518	$t_project_id = bug_get_field( $p_bug_id, 'project_id' );
1519	$t_sender_id = auth_get_current_user_id();
1520	$t_sender = user_get_name( $t_sender_id );
1521
1522	$t_subject = email_build_subject( $p_bug_id );
1523	$t_date = date( config_get( 'normal_date_format' ) );
1524
1525	$t_result = array();
1526	foreach( $p_recipients as $t_recipient ) {
1527		lang_push( user_pref_get_language( $t_recipient, $t_project_id ) );
1528
1529		$t_email = user_get_email( $t_recipient );
1530
1531		if( access_has_project_level( config_get( 'show_user_email_threshold' ), $t_project_id, $t_recipient ) ) {
1532			$t_sender_email = ' <' . user_get_email( $t_sender_id ) . '>';
1533		} else {
1534			$t_sender_email = '';
1535		}
1536		$t_header = "\n" . lang_get( 'on_date' ) . ' ' . $t_date . ', ' . $t_sender . ' ' . $t_sender_email . lang_get( 'sent_you_this_reminder_about' ) . ': ' . "\n\n";
1537		$t_contents = $t_header . string_get_bug_view_url_with_fqdn( $p_bug_id ) . " \n\n" . $p_message;
1538
1539		$t_id = email_store( $t_email, $t_subject, $t_contents );
1540		if( $t_id !== null ) {
1541			$t_result[] = $t_recipient;
1542		}
1543		log_event( LOG_EMAIL_VERBOSE, 'queued reminder email ' . $t_id . ' for U' . $t_recipient );
1544
1545		lang_pop();
1546	}
1547
1548	return $t_result;
1549}
1550
1551/**
1552 * Send a notification to user or set of users that were mentioned in an issue
1553 * or an issue note.
1554 *
1555 * @param integer       $p_bug_id     Issue for which the reminder is sent.
1556 * @param array         $p_mention_user_ids User id or list of user ids array.
1557 * @param string        $p_message    Optional message to add to the e-mail.
1558 * @param array         $p_removed_mention_user_ids  The users that were removed due to lack of access.
1559 * @return array        List of users ids to whom the mentioned e-mail were actually sent
1560 */
1561function email_user_mention( $p_bug_id, $p_mention_user_ids, $p_message, $p_removed_mention_user_ids = array() ) {
1562	if( OFF == config_get( 'enable_email_notification' ) ) {
1563		log_event( LOG_EMAIL_VERBOSE, 'email notifications disabled.' );
1564		return array();
1565	}
1566
1567	$t_project_id = bug_get_field( $p_bug_id, 'project_id' );
1568	$t_sender_id = auth_get_current_user_id();
1569	$t_sender = user_get_name( $t_sender_id );
1570
1571	$t_subject = email_build_subject( $p_bug_id );
1572	$t_date = date( config_get( 'normal_date_format' ) );
1573	$t_user_id = auth_get_current_user_id();
1574	$t_users_processed = array();
1575
1576	foreach( $p_removed_mention_user_ids as $t_removed_mention_user_id ) {
1577		log_event( LOG_EMAIL_VERBOSE, 'skipped mention email for U' . $t_removed_mention_user_id . ' (no access to issue or note).' );
1578	}
1579
1580	$t_result = array();
1581	foreach( $p_mention_user_ids as $t_mention_user_id ) {
1582		# Don't trigger mention emails for self mentions
1583		if( $t_mention_user_id == $t_user_id ) {
1584			log_event( LOG_EMAIL_VERBOSE, 'skipped mention email for U' . $t_mention_user_id . ' (self-mention).' );
1585			continue;
1586		}
1587
1588		# Don't process a user more than once
1589		if( isset( $t_users_processed[$t_mention_user_id] ) ) {
1590			continue;
1591		}
1592
1593		$t_users_processed[$t_mention_user_id] = true;
1594
1595		# Don't email mention notifications to disabled users.
1596		if( !user_is_enabled( $t_mention_user_id ) ) {
1597			continue;
1598		}
1599
1600		lang_push( user_pref_get_language( $t_mention_user_id, $t_project_id ) );
1601
1602		$t_email = user_get_email( $t_mention_user_id );
1603
1604		if( access_has_project_level( config_get( 'show_user_email_threshold' ), $t_project_id, $t_mention_user_id ) ) {
1605			$t_sender_email = ' <' . user_get_email( $t_sender_id ) . '> ';
1606		} else {
1607			$t_sender_email = '';
1608		}
1609
1610		$t_complete_subject = sprintf( lang_get( 'mentioned_in' ), $t_subject );
1611		$t_header = "\n" . lang_get( 'on_date' ) . ' ' . $t_date . ', ' . $t_sender . ' ' . $t_sender_email . lang_get( 'mentioned_you' ) . "\n\n";
1612		$t_contents = $t_header . string_get_bug_view_url_with_fqdn( $p_bug_id ) . " \n\n" . $p_message;
1613
1614		$t_id = email_store( $t_email, $t_complete_subject, $t_contents );
1615		if( $t_id !== null ) {
1616			$t_result[] = $t_mention_user_id;
1617		}
1618
1619		log_event( LOG_EMAIL_VERBOSE, 'queued mention email ' . $t_id . ' for U' . $t_mention_user_id );
1620
1621		lang_pop();
1622	}
1623
1624	return $t_result;
1625}
1626
1627/**
1628 * Send bug info to given user
1629 * return true on success
1630 * @param array   $p_visible_bug_data       Array of bug data information.
1631 * @param string  $p_message_id             A message identifier.
1632 * @param integer $p_user_id                A valid user identifier.
1633 * @param array   $p_header_optional_params Array of additional email headers.
1634 * @return void
1635 */
1636function email_bug_info_to_one_user( array $p_visible_bug_data, $p_message_id, $p_user_id, array $p_header_optional_params = null ) {
1637	$t_user_email = user_get_email( $p_user_id );
1638
1639	# check whether email should be sent
1640	# @@@ can be email field empty? if yes - then it should be handled here
1641	if( ON !== config_get( 'enable_email_notification' ) || is_blank( $t_user_email ) ) {
1642		return;
1643	}
1644
1645	# build subject
1646	$t_subject = email_build_subject( $p_visible_bug_data['email_bug'] );
1647
1648	# build message
1649	$t_message = lang_get_defaulted( $p_message_id, null );
1650
1651	if( is_array( $p_header_optional_params ) ) {
1652		$t_message = vsprintf( $t_message, $p_header_optional_params );
1653	}
1654
1655	if( ( $t_message !== null ) && ( !is_blank( $t_message ) ) ) {
1656		$t_message .= " \n";
1657	}
1658
1659	$t_message .= email_format_bug_message( $p_visible_bug_data );
1660
1661	# build headers
1662	$t_bug_id = $p_visible_bug_data['email_bug'];
1663	$t_message_md5 = md5( $t_bug_id . $p_visible_bug_data['email_date_submitted'] );
1664	$t_mail_headers = array(
1665		'keywords' => $p_visible_bug_data['set_category'],
1666	);
1667	if( $p_message_id == 'email_notification_title_for_action_bug_submitted' ) {
1668		$t_mail_headers['Message-ID'] = $t_message_md5;
1669	} else {
1670		$t_mail_headers['In-Reply-To'] = $t_message_md5;
1671	}
1672
1673	# send mail
1674	email_store( $t_user_email, $t_subject, $t_message, $t_mail_headers );
1675
1676	return;
1677}
1678
1679/**
1680 * Generates a formatted note to be used in email notifications.
1681 *
1682 * @param BugnoteData $p_bugnote The bugnote object.
1683 * @param integer $p_project_id  The project id
1684 * @param boolean $p_show_time_tracking true: show time tracking, false otherwise.
1685 * @param string $p_horizontal_separator The horizontal line separator to use.
1686 * @param string $p_date_format The date format to use.
1687 * @return string The formatted note.
1688 */
1689function email_format_bugnote( $p_bugnote, $p_project_id, $p_show_time_tracking, $p_horizontal_separator, $p_date_format = null ) {
1690	$t_date_format = ( $p_date_format === null ) ? config_get( 'normal_date_format' ) : $p_date_format;
1691
1692	$t_last_modified = date( $t_date_format, $p_bugnote->last_modified );
1693
1694	$t_formatted_bugnote_id = bugnote_format_id( $p_bugnote->id );
1695	$t_bugnote_link = string_process_bugnote_link( config_get( 'bugnote_link_tag' ) . $p_bugnote->id, false, false, true );
1696
1697	if( $p_show_time_tracking && $p_bugnote->time_tracking > 0 ) {
1698		$t_time_tracking = ' ' . lang_get( 'time_tracking' ) . ' ' . db_minutes_to_hhmm( $p_bugnote->time_tracking ) . "\n";
1699	} else {
1700		$t_time_tracking = '';
1701	}
1702
1703	if( user_exists( $p_bugnote->reporter_id ) ) {
1704		$t_access_level = access_get_project_level( $p_project_id, $p_bugnote->reporter_id );
1705		$t_access_level_string = ' (' . access_level_get_string( $t_access_level ) . ')';
1706	} else {
1707		$t_access_level_string = '';
1708	}
1709
1710	$t_private = ( $p_bugnote->view_state == VS_PUBLIC ) ? '' : ' (' . lang_get( 'private' ) . ')';
1711
1712	$t_string = ' (' . $t_formatted_bugnote_id . ') ' . user_get_name( $p_bugnote->reporter_id ) .
1713		$t_access_level_string . ' - ' . $t_last_modified . $t_private . "\n" .
1714		$t_time_tracking . ' ' . $t_bugnote_link;
1715
1716	$t_message  = $p_horizontal_separator . " \n";
1717	$t_message .= $t_string . " \n";
1718	$t_message .= $p_horizontal_separator . " \n";
1719	$t_message .= $p_bugnote->note . " \n";
1720
1721	return $t_message;
1722}
1723
1724/**
1725 * Build the bug info part of the message
1726 * @param array $p_visible_bug_data Bug data array to format.
1727 * @return string
1728 */
1729function email_format_bug_message( array $p_visible_bug_data ) {
1730	$t_normal_date_format = config_get( 'normal_date_format' );
1731	$t_complete_date_format = config_get( 'complete_date_format' );
1732
1733	$t_email_separator1 = config_get( 'email_separator1' );
1734	$t_email_separator2 = config_get( 'email_separator2' );
1735	$t_email_padding_length = config_get( 'email_padding_length' );
1736
1737	$p_visible_bug_data['email_date_submitted'] = date( $t_complete_date_format, $p_visible_bug_data['email_date_submitted'] );
1738	$p_visible_bug_data['email_last_modified'] = date( $t_complete_date_format, $p_visible_bug_data['email_last_modified'] );
1739
1740	$t_message = $t_email_separator1 . " \n";
1741
1742	if( isset( $p_visible_bug_data['email_bug_view_url'] ) ) {
1743		$t_message .= $p_visible_bug_data['email_bug_view_url'] . " \n";
1744		$t_message .= $t_email_separator1 . " \n";
1745	}
1746
1747	$t_message .= email_format_attribute( $p_visible_bug_data, 'email_reporter' );
1748	$t_message .= email_format_attribute( $p_visible_bug_data, 'email_handler' );
1749	$t_message .= $t_email_separator1 . " \n";
1750	$t_message .= email_format_attribute( $p_visible_bug_data, 'email_project' );
1751	$t_message .= email_format_attribute( $p_visible_bug_data, 'email_bug' );
1752	$t_message .= email_format_attribute( $p_visible_bug_data, 'email_category' );
1753
1754	if( isset( $p_visible_bug_data['email_tag'] ) ) {
1755		$t_message .= email_format_attribute( $p_visible_bug_data, 'email_tag' );
1756	}
1757
1758	if ( isset( $p_visible_bug_data[ 'email_reproducibility' ] ) ) {
1759		$p_visible_bug_data['email_reproducibility'] = get_enum_element( 'reproducibility', $p_visible_bug_data['email_reproducibility'] );
1760		$t_message .= email_format_attribute( $p_visible_bug_data, 'email_reproducibility' );
1761	}
1762
1763	if ( isset( $p_visible_bug_data[ 'email_severity' ] ) ) {
1764		$p_visible_bug_data['email_severity'] = get_enum_element( 'severity', $p_visible_bug_data['email_severity'] );
1765		$t_message .= email_format_attribute( $p_visible_bug_data, 'email_severity' );
1766	}
1767
1768	if ( isset( $p_visible_bug_data[ 'email_priority' ] ) ) {
1769		$p_visible_bug_data['email_priority'] = get_enum_element( 'priority', $p_visible_bug_data['email_priority'] );
1770		$t_message .= email_format_attribute( $p_visible_bug_data, 'email_priority' );
1771	}
1772
1773	if ( isset( $p_visible_bug_data[ 'email_status' ] ) ) {
1774		$t_status = $p_visible_bug_data['email_status'];
1775		$p_visible_bug_data['email_status'] = get_enum_element( 'status', $t_status );
1776		$t_message .= email_format_attribute( $p_visible_bug_data, 'email_status' );
1777	}
1778
1779	if ( isset( $p_visible_bug_data[ 'email_target_version' ] ) ) {
1780		$t_message .= email_format_attribute( $p_visible_bug_data, 'email_target_version' );
1781	}
1782
1783	# custom fields formatting
1784	foreach( $p_visible_bug_data['custom_fields'] as $t_custom_field_name => $t_custom_field_data ) {
1785		$t_message .= utf8_str_pad( lang_get_defaulted( $t_custom_field_name, null ) . ': ', $t_email_padding_length, ' ', STR_PAD_RIGHT );
1786		$t_message .= string_custom_field_value_for_email( $t_custom_field_data['value'], $t_custom_field_data['type'] );
1787		$t_message .= " \n";
1788	}
1789
1790	# end foreach custom field
1791
1792	if( isset( $t_status ) && config_get( 'bug_resolved_status_threshold' ) <= $t_status ) {
1793
1794		if ( isset( $p_visible_bug_data[ 'email_resolution' ] ) ) {
1795			$p_visible_bug_data['email_resolution'] = get_enum_element( 'resolution', $p_visible_bug_data['email_resolution'] );
1796			$t_message .= email_format_attribute( $p_visible_bug_data, 'email_resolution' );
1797		}
1798
1799		$t_message .= email_format_attribute( $p_visible_bug_data, 'email_fixed_in_version' );
1800	}
1801	$t_message .= $t_email_separator1 . " \n";
1802
1803	$t_message .= email_format_attribute( $p_visible_bug_data, 'email_date_submitted' );
1804	$t_message .= email_format_attribute( $p_visible_bug_data, 'email_last_modified' );
1805
1806	if( isset( $p_visible_bug_data['email_due_date'] ) ) {
1807		$t_message .= email_format_attribute( $p_visible_bug_data, 'email_due_date' );
1808	}
1809
1810	$t_message .= $t_email_separator1 . " \n";
1811
1812	$t_message .= email_format_attribute( $p_visible_bug_data, 'email_summary' );
1813
1814	$t_message .= lang_get( 'email_description' ) . ": \n" . $p_visible_bug_data['email_description'] . "\n";
1815
1816	if( isset( $p_visible_bug_data[ 'email_steps_to_reproduce' ] ) && !is_blank( $p_visible_bug_data['email_steps_to_reproduce'] ) ) {
1817		$t_message .= "\n" . lang_get( 'email_steps_to_reproduce' ) . ": \n" . $p_visible_bug_data['email_steps_to_reproduce'] . "\n";
1818	}
1819
1820	if( isset( $p_visible_bug_data[ 'email_additional_information' ] ) && !is_blank( $p_visible_bug_data['email_additional_information'] ) ) {
1821		$t_message .= "\n" . lang_get( 'email_additional_information' ) . ": \n" . $p_visible_bug_data['email_additional_information'] . "\n";
1822	}
1823
1824	if( isset( $p_visible_bug_data['relations'] ) ) {
1825		if( $p_visible_bug_data['relations'] != '' ) {
1826			$t_message .= $t_email_separator1 . "\n" . utf8_str_pad( lang_get( 'bug_relationships' ), 20 ) . utf8_str_pad( lang_get( 'id' ), 8 ) . lang_get( 'summary' ) . "\n" . $t_email_separator2 . "\n" . $p_visible_bug_data['relations'];
1827		}
1828	}
1829
1830	# Sponsorship
1831	if( isset( $p_visible_bug_data['sponsorship_total'] ) && ( $p_visible_bug_data['sponsorship_total'] > 0 ) ) {
1832		$t_message .= $t_email_separator1 . " \n";
1833		$t_message .= sprintf( lang_get( 'total_sponsorship_amount' ), sponsorship_format_amount( $p_visible_bug_data['sponsorship_total'] ) ) . "\n\n";
1834
1835		if( isset( $p_visible_bug_data['sponsorships'] ) ) {
1836			foreach( $p_visible_bug_data['sponsorships'] as $t_sponsorship ) {
1837				$t_date_added = date( config_get( 'normal_date_format' ), $t_sponsorship->date_submitted );
1838
1839				$t_message .= $t_date_added . ': ';
1840				$t_message .= user_get_name( $t_sponsorship->user_id );
1841				$t_message .= ' (' . sponsorship_format_amount( $t_sponsorship->amount ) . ')' . " \n";
1842			}
1843		}
1844	}
1845
1846	$t_message .= $t_email_separator1 . " \n\n";
1847
1848	# format bugnotes
1849	foreach( $p_visible_bug_data['bugnotes'] as $t_bugnote ) {
1850		# Show time tracking is always true, since data has already been filtered out when creating the bug visible data.
1851		$t_message .= email_format_bugnote( $t_bugnote, $p_visible_bug_data['email_project_id'],
1852				/* show_time_tracking */ true,  $t_email_separator2, $t_normal_date_format ) . "\n";
1853	}
1854
1855	# format history
1856	if( array_key_exists( 'history', $p_visible_bug_data ) ) {
1857		$t_message .= lang_get( 'bug_history' ) . " \n";
1858		$t_message .= utf8_str_pad( lang_get( 'date_modified' ), 17 ) . utf8_str_pad( lang_get( 'username' ), 15 ) . utf8_str_pad( lang_get( 'field' ), 25 ) . utf8_str_pad( lang_get( 'change' ), 20 ) . " \n";
1859
1860		$t_message .= $t_email_separator1 . " \n";
1861
1862		foreach( $p_visible_bug_data['history'] as $t_raw_history_item ) {
1863			$t_localized_item = history_localize_item(
1864				$t_raw_history_item['bug_id'],
1865				$t_raw_history_item['field'],
1866				$t_raw_history_item['type'],
1867				$t_raw_history_item['old_value'],
1868				$t_raw_history_item['new_value'],
1869				false
1870			);
1871
1872			$t_message .= utf8_str_pad( date( $t_normal_date_format, $t_raw_history_item['date'] ), 17 ) . utf8_str_pad( $t_raw_history_item['username'], 15 ) . utf8_str_pad( $t_localized_item['note'], 25 ) . utf8_str_pad( $t_localized_item['change'], 20 ) . "\n";
1873		}
1874		$t_message .= $t_email_separator1 . " \n\n";
1875	}
1876
1877	return $t_message;
1878}
1879
1880/**
1881 * if $p_visible_bug_data contains specified attribute the function
1882 * returns concatenated translated attribute name and original
1883 * attribute value. Else return empty string.
1884 * @param array  $p_visible_bug_data Visible Bug Data array.
1885 * @param string $p_attribute_id     Attribute ID.
1886 * @return string
1887 */
1888function email_format_attribute( array $p_visible_bug_data, $p_attribute_id ) {
1889	if( array_key_exists( $p_attribute_id, $p_visible_bug_data ) ) {
1890		return utf8_str_pad( lang_get( $p_attribute_id ) . ': ', config_get( 'email_padding_length' ), ' ', STR_PAD_RIGHT ) . $p_visible_bug_data[$p_attribute_id] . "\n";
1891	}
1892	return '';
1893}
1894
1895/**
1896 * Build the bug raw data visible for specified user to be translated and sent by email to the user
1897 * (Filter the bug data according to user access level)
1898 * return array with bug data. See usage in email_format_bug_message(...)
1899 * @param integer $p_user_id    A user identifier.
1900 * @param integer $p_bug_id     A bug identifier.
1901 * @param string  $p_message_id A message identifier.
1902 * @return array
1903 */
1904function email_build_visible_bug_data( $p_user_id, $p_bug_id, $p_message_id ) {
1905	# Override current user with user to construct bug data for.
1906	# This is to make sure that APIs that check against current user (e.g. relationship) work correctly.
1907	$t_current_user_id = current_user_set( $p_user_id );
1908
1909	$t_project_id = bug_get_field( $p_bug_id, 'project_id' );
1910	$t_user_access_level = user_get_access_level( $p_user_id, $t_project_id );
1911	$t_user_bugnote_order = user_pref_get_pref( $p_user_id, 'bugnote_order' );
1912	$t_user_bugnote_limit = user_pref_get_pref( $p_user_id, 'email_bugnote_limit' );
1913
1914	$t_row = bug_get_extended_row( $p_bug_id );
1915	$t_bug_data = array();
1916
1917	$t_bug_view_fields = config_get( 'bug_view_page_fields', null, $p_user_id, $t_row['project_id'] );
1918
1919	$t_bug_data['email_bug'] = $p_bug_id;
1920
1921	if( $p_message_id !== 'email_notification_title_for_action_bug_deleted' ) {
1922		$t_bug_data['email_bug_view_url'] = string_get_bug_view_url_with_fqdn( $p_bug_id );
1923	}
1924
1925	if( access_compare_level( $t_user_access_level, config_get( 'view_handler_threshold' ) ) ) {
1926		if( 0 != $t_row['handler_id'] ) {
1927			$t_bug_data['email_handler'] = user_get_name( $t_row['handler_id'] );
1928		} else {
1929			$t_bug_data['email_handler'] = '';
1930		}
1931	}
1932
1933	$t_bug_data['email_reporter'] = user_get_name( $t_row['reporter_id'] );
1934	$t_bug_data['email_project_id'] = $t_row['project_id'];
1935	$t_bug_data['email_project'] = project_get_field( $t_row['project_id'], 'name' );
1936
1937	$t_category_name = category_full_name( $t_row['category_id'], false );
1938	$t_bug_data['email_category'] = $t_category_name;
1939
1940	$t_tag_rows = tag_bug_get_attached( $p_bug_id );
1941	if( in_array( 'tags', $t_bug_view_fields ) && !empty( $t_tag_rows ) && access_compare_level( $t_user_access_level, config_get( 'tag_view_threshold' ) ) ) {
1942		$t_bug_data['email_tag'] = '';
1943
1944		foreach( $t_tag_rows as $t_tag ) {
1945			$t_bug_data['email_tag'] .= $t_tag['name'] . ', ';
1946		}
1947
1948		$t_bug_data['email_tag'] = trim( $t_bug_data['email_tag'], ', ' );
1949	}
1950
1951	$t_bug_data['email_date_submitted'] = $t_row['date_submitted'];
1952	$t_bug_data['email_last_modified'] = $t_row['last_updated'];
1953
1954	if( !date_is_null( $t_row['due_date'] ) && access_compare_level( $t_user_access_level, config_get( 'due_date_view_threshold' ) ) ) {
1955		$t_bug_data['email_due_date'] = date( config_get( 'short_date_format' ), $t_row['due_date'] );
1956	}
1957
1958	if ( in_array( 'status', $t_bug_view_fields ) ) {
1959		$t_bug_data['email_status'] = $t_row['status'];
1960	}
1961
1962	if ( in_array( 'severity', $t_bug_view_fields ) ) {
1963		$t_bug_data['email_severity'] = $t_row['severity'];
1964	}
1965
1966	if ( in_array( 'priority', $t_bug_view_fields ) ) {
1967		$t_bug_data['email_priority'] = $t_row['priority'];
1968	}
1969
1970	if ( in_array( 'reproducibility', $t_bug_view_fields ) ) {
1971		$t_bug_data['email_reproducibility'] = $t_row['reproducibility'];
1972	}
1973
1974	if ( in_array( 'resolution', $t_bug_view_fields ) ) {
1975		$t_bug_data['email_resolution'] = $t_row['resolution'];
1976	}
1977
1978	$t_bug_data['email_fixed_in_version'] = $t_row['fixed_in_version'];
1979
1980	if( in_array( 'target_version', $t_bug_view_fields ) && !is_blank( $t_row['target_version'] ) && access_compare_level( $t_user_access_level, config_get( 'roadmap_view_threshold' ) ) ) {
1981		$t_bug_data['email_target_version'] = $t_row['target_version'];
1982	}
1983
1984	$t_bug_data['email_summary'] = $t_row['summary'];
1985	$t_bug_data['email_description'] = $t_row['description'];
1986
1987	if( in_array( 'additional_info', $t_bug_view_fields ) ) {
1988		$t_bug_data['email_additional_information'] = $t_row['additional_information'];
1989	}
1990
1991	if ( in_array( 'steps_to_reproduce', $t_bug_view_fields ) ) {
1992		$t_bug_data['email_steps_to_reproduce'] = $t_row['steps_to_reproduce'];
1993	}
1994
1995	$t_bug_data['set_category'] = '[' . $t_bug_data['email_project'] . '] ' . $t_category_name;
1996
1997	$t_bug_data['custom_fields'] = custom_field_get_linked_fields( $p_bug_id, $t_user_access_level );
1998	$t_bug_data['bugnotes'] = bugnote_get_all_visible_bugnotes( $p_bug_id, $t_user_bugnote_order, $t_user_bugnote_limit, $p_user_id );
1999
2000	# put history data
2001	if( ( ON == config_get( 'history_default_visible' ) ) && access_compare_level( $t_user_access_level, config_get( 'view_history_threshold' ) ) ) {
2002		$t_bug_data['history'] = history_get_raw_events_array( $p_bug_id, $p_user_id );
2003	}
2004
2005	# Sponsorship Information
2006	if( ( config_get( 'enable_sponsorship' ) == ON ) && ( access_has_bug_level( config_get( 'view_sponsorship_total_threshold' ), $p_bug_id, $p_user_id ) ) ) {
2007		$t_sponsorship_ids = sponsorship_get_all_ids( $p_bug_id );
2008		$t_bug_data['sponsorship_total'] = sponsorship_get_amount( $t_sponsorship_ids );
2009
2010		if( access_has_bug_level( config_get( 'view_sponsorship_details_threshold' ), $p_bug_id, $p_user_id ) ) {
2011			$t_bug_data['sponsorships'] = array();
2012			foreach( $t_sponsorship_ids as $t_id ) {
2013				$t_bug_data['sponsorships'][] = sponsorship_get( $t_id );
2014			}
2015		}
2016	}
2017
2018	$t_bug_data['relations'] = email_relationship_get_summary_text( $p_bug_id );
2019
2020	current_user_set( $t_current_user_id );
2021
2022	return $t_bug_data;
2023}
2024
2025/**
2026 * return formatted string with all the details on the requested relationship
2027 * @param integer             $p_bug_id       A bug identifier.
2028 * @param BugRelationshipData $p_relationship A bug relationship object.
2029 * @return string
2030 */
2031function email_relationship_get_details( $p_bug_id, BugRelationshipData $p_relationship ) {
2032	$t_summary_wrap_at = mb_strlen( config_get( 'email_separator2' ) ) - 28;
2033
2034	if( $p_bug_id == $p_relationship->src_bug_id ) {
2035		# root bug is in the source side, related bug in the destination side
2036		$t_related_project_id = $p_relationship->dest_bug_id;
2037		$t_related_bug_id = $p_relationship->dest_bug_id;
2038		$t_relationship_descr = relationship_get_description_src_side( $p_relationship->type );
2039	} else {
2040		# root bug is in the dest side, related bug in the source side
2041		$t_related_project_id = $p_relationship->src_bug_id;
2042		$t_related_bug_id = $p_relationship->src_bug_id;
2043		$t_relationship_descr = relationship_get_description_dest_side( $p_relationship->type );
2044	}
2045
2046	# related bug not existing...
2047	if( !bug_exists( $t_related_bug_id ) ) {
2048		return '';
2049	}
2050
2051	# user can access to the related bug at least as a viewer
2052	if( !access_has_bug_level( config_get( 'view_bug_threshold', null, null, $t_related_project_id ), $t_related_bug_id ) ) {
2053		return '';
2054	}
2055
2056	# get the information from the related bug and prepare the link
2057	$t_bug = bug_get( $t_related_bug_id, false );
2058
2059	$t_relationship_info_text = utf8_str_pad( $t_relationship_descr, 20 );
2060	$t_relationship_info_text .= utf8_str_pad( bug_format_id( $t_related_bug_id ), 8 );
2061
2062	# add summary
2063	if( mb_strlen( $t_bug->summary ) <= $t_summary_wrap_at ) {
2064		$t_relationship_info_text .= string_email_links( $t_bug->summary );
2065	} else {
2066		$t_relationship_info_text .= mb_substr( string_email_links( $t_bug->summary ), 0, $t_summary_wrap_at - 3 ) . '...';
2067	}
2068
2069	$t_relationship_info_text .= "\n";
2070
2071	return $t_relationship_info_text;
2072}
2073
2074/**
2075 * Get ALL the RELATIONSHIPS OF A SPECIFIC BUG in text format (used by email_api.php
2076 * @param integer $p_bug_id A bug identifier.
2077 * @return string
2078 */
2079function email_relationship_get_summary_text( $p_bug_id ) {
2080	# A variable that will be set by the following call to indicate if relationships belong
2081	# to multiple projects.
2082	$t_show_project = false;
2083
2084	$t_relationship_all = relationship_get_all( $p_bug_id, $t_show_project );
2085	$t_relationship_all_count = count( $t_relationship_all );
2086
2087	# prepare the relationships table
2088	$t_summary = '';
2089	for( $i = 0; $i < $t_relationship_all_count; $i++ ) {
2090		$t_summary .= email_relationship_get_details( $p_bug_id, $t_relationship_all[$i] );
2091	}
2092
2093	return $t_summary;
2094}
2095
2096/**
2097 * The email sending shutdown function
2098 * Will send any queued emails, except when $g_email_send_using_cronjob = ON.
2099 * If $g_email_shutdown_processing EMAIL_SHUTDOWN_FORCE flag is set, emails
2100 * will be sent regardless of cronjob setting.
2101 * @return void
2102 */
2103function email_shutdown_function() {
2104	global $g_email_shutdown_processing;
2105
2106	# Nothing to do if
2107	# - no emails have been generated in the current request
2108	# - system is configured to use cron job (unless processing is forced)
2109	if(    $g_email_shutdown_processing == EMAIL_SHUTDOWN_SKIP
2110		|| (   !( $g_email_shutdown_processing & EMAIL_SHUTDOWN_FORCE )
2111			&& config_get( 'email_send_using_cronjob' )
2112		   )
2113	) {
2114		return;
2115	}
2116
2117	$t_msg ='Shutdown function called for ' . $_SERVER['SCRIPT_NAME'];
2118	if( $g_email_shutdown_processing & EMAIL_SHUTDOWN_FORCE ) {
2119		$t_msg .= ' (email processing forced)';
2120	}
2121
2122	log_event( LOG_EMAIL_VERBOSE, $t_msg );
2123
2124	if( $g_email_shutdown_processing ) {
2125		email_send_all();
2126	}
2127}
2128
2129/**
2130 * Get the list of supported email actions.
2131 *
2132 * @return array List of actions
2133 */
2134function email_get_actions() {
2135	$t_actions = array( 'updated', 'owner', 'reopened', 'deleted', 'bugnote', 'relation', 'monitor' );
2136
2137	if( config_get( 'enable_sponsorship' ) == ON ) {
2138		$t_actions[] = 'sponsor';
2139	}
2140
2141	$t_statuses = MantisEnum::getAssocArrayIndexedByValues( config_get( 'status_enum_string' ) );
2142	ksort( $t_statuses );
2143	reset( $t_statuses );
2144
2145	foreach( $t_statuses as $t_label ) {
2146		$t_actions[] = $t_label;
2147	}
2148
2149	return $t_actions;
2150}
2151