notification_service.rb 19.3 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
# rubocop:disable GitlabSecurity/PublicSend

5 6
# NotificationService class
#
Johannes's avatar
Johannes committed
7
# Used for notifying users with emails about different events
8 9 10 11
#
# Ex.
#   NotificationService.new.new_issue(issue, current_user)
#
12 13 14 15 16 17
# When calculating the recipients of a notification is expensive (for instance,
# in the new issue case), `#async` will make that calculation happen in Sidekiq
# instead:
#
#   NotificationService.new.async.new_issue(issue, current_user)
#
18
class NotificationService
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
  class Async
    attr_reader :parent
    delegate :respond_to_missing, to: :parent

    def initialize(parent)
      @parent = parent
    end

    def method_missing(meth, *args)
      return super unless parent.respond_to?(meth)

      MailScheduler::NotificationServiceWorker.perform_async(meth.to_s, *args)
    end
  end

  def async
    @async ||= Async.new(self)
  end

38 39
  # Always notify user about ssh key added
  # only if ssh key is not deploy key
40 41
  #
  # This is security email so it will be sent
jneen's avatar
jneen committed
42 43 44
  # even if user disabled notifications. However,
  # it won't be sent to internal users like the
  # ghost user or the EE support bot.
45
  def new_key(key)
jneen's avatar
jneen committed
46
    if key.user&.can?(:receive_notifications)
Valeriy's avatar
Valeriy committed
47
      mailer.new_ssh_key_email(key.id).deliver_later
48 49 50
    end
  end

51 52
  # Always notify the user about gpg key added
  #
Takuya Noguchi's avatar
Takuya Noguchi committed
53
  # This is a security email so it will be sent even if the user disabled
54 55
  # notifications
  def new_gpg_key(gpg_key)
jneen's avatar
jneen committed
56
    if gpg_key.user&.can?(:receive_notifications)
57 58 59 60
      mailer.new_gpg_key_email(gpg_key.id).deliver_later
    end
  end

61
  # When create an issue we should send an email to:
62
  #
63
  #  * issue assignee if their notification level is not Disabled
64
  #  * project team members with notification level higher then Participating
65
  #  * watchers of the issue's labels
66
  #  * users with custom level checked with "new issue"
67 68
  #
  def new_issue(issue, current_user)
jneen's avatar
jneen committed
69
    new_resource_email(issue, :new_issue_email)
70 71
  end

72 73 74 75 76 77 78 79 80 81 82 83 84
  # When issue text is updated, we should send an email to:
  #
  #  * newly mentioned project team members with notification level higher than Participating
  #
  def new_mentions_in_issue(issue, new_mentioned_users, current_user)
    new_mentions_in_resource_email(
      issue,
      new_mentioned_users,
      current_user,
      :new_mention_in_issue_email
    )
  end

85
  # When we close an issue we should send an email to:
86
  #
87 88
  #  * issue author if their notification level is not Disabled
  #  * issue assignee if their notification level is not Disabled
89
  #  * project team members with notification level higher then Participating
90
  #  * users with custom level checked with "close issue"
91 92
  #
  def close_issue(issue, current_user)
jneen's avatar
jneen committed
93
    close_resource_email(issue, current_user, :closed_issue_email)
94 95
  end

96
  # When we reassign an issue we should send an email to:
97
  #
98 99
  #  * issue old assignees if their notification level is not Disabled
  #  * issue new assignees if their notification level is not Disabled
100
  #  * users with custom level checked with "reassign issue"
101
  #
102
  def reassigned_issue(issue, current_user, previous_assignees = [])
103
    recipients = NotificationRecipientService.build_recipients(
104 105 106
      issue,
      current_user,
      action: "reassign",
107
      previous_assignees: previous_assignees
108 109
    )

110 111
    previous_assignee_ids = previous_assignees.map(&:id)

112 113 114
    recipients.each do |recipient|
      mailer.send(
        :reassigned_issue_email,
115
        recipient.user.id,
116
        issue.id,
117
        previous_assignee_ids,
118 119
        current_user.id,
        recipient.reason
120 121
      ).deliver_later
    end
122 123
  end

124
  # When we add labels to an issue we should send an email to:
125
  #
126 127 128
  #  * watchers of the issue's labels
  #
  def relabeled_issue(issue, added_labels, current_user)
jneen's avatar
jneen committed
129
    relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email)
130 131
  end

132 133 134 135 136 137 138 139
  def removed_milestone_issue(issue, current_user)
    removed_milestone_resource_email(issue, current_user, :removed_milestone_issue_email)
  end

  def changed_milestone_issue(issue, new_milestone, current_user)
    changed_milestone_resource_email(issue, new_milestone, current_user, :changed_milestone_issue_email)
  end

140
  # When create a merge request we should send an email to:
141
  #
Mark Chao's avatar
Mark Chao committed
142
  #  * mr author
143
  #  * mr assignees if their notification level is not Disabled
144 145
  #  * project team members with notification level higher then Participating
  #  * watchers of the mr's labels
146
  #  * users with custom level checked with "new merge request"
147
  #
148
  # In EE, approvers of the merge request are also included
149
  def new_merge_request(merge_request, current_user)
jneen's avatar
jneen committed
150
    new_resource_email(merge_request, :new_merge_request_email)
151
  end
152

153 154 155 156 157 158 159 160 161 162
  def push_to_merge_request(merge_request, current_user, new_commits: [], existing_commits: [])
    new_commits = new_commits.map { |c| { short_id: c.short_id, title: c.title } }
    existing_commits = existing_commits.map { |c| { short_id: c.short_id, title: c.title } }
    recipients = NotificationRecipientService.build_recipients(merge_request, current_user, action: "push_to")

    recipients.each do |recipient|
      mailer.send(:push_to_merge_request_email, recipient.user.id, merge_request.id, current_user.id, recipient.reason, new_commits: new_commits, existing_commits: existing_commits).deliver_later
    end
  end

163 164 165 166 167 168 169 170 171
  # When a merge request is found to be unmergeable, we should send an email to:
  #
  #  * mr author
  #  * mr merge user if set
  #
  def merge_request_unmergeable(merge_request)
    merge_request_unmergeable_email(merge_request)
  end

172 173 174 175 176 177 178 179 180 181 182 183 184
  # When merge request text is updated, we should send an email to:
  #
  #  * newly mentioned project team members with notification level higher than Participating
  #
  def new_mentions_in_merge_request(merge_request, new_mentioned_users, current_user)
    new_mentions_in_resource_email(
      merge_request,
      new_mentioned_users,
      current_user,
      :new_mention_in_merge_request_email
    )
  end

185
  # When we reassign a merge_request we should send an email to:
186
  #
187 188
  #  * merge_request old assignees if their notification level is not Disabled
  #  * merge_request new assignees if their notification level is not Disabled
189
  #  * users with custom level checked with "reassign merge request"
190
  #
191
  def reassigned_merge_request(merge_request, current_user, previous_assignees = [])
192 193 194 195
    recipients = NotificationRecipientService.build_recipients(
      merge_request,
      current_user,
      action: "reassign",
196
      previous_assignees: previous_assignees
197 198
    )

199 200
    previous_assignee_ids = previous_assignees.map(&:id)

201 202 203 204
    recipients.each do |recipient|
      mailer.reassigned_merge_request_email(
        recipient.user.id,
        merge_request.id,
205
        previous_assignee_ids,
206 207 208 209
        current_user.id,
        recipient.reason
      ).deliver_later
    end
210
  end
211

212
  # When we add labels to a merge request we should send an email to:
213
  #
214 215 216
  #  * watchers of the mr's labels
  #
  def relabeled_merge_request(merge_request, added_labels, current_user)
jneen's avatar
jneen committed
217
    relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email)
218 219
  end

220 221 222 223 224 225 226 227
  def removed_milestone_merge_request(merge_request, current_user)
    removed_milestone_resource_email(merge_request, current_user, :removed_milestone_merge_request_email)
  end

  def changed_milestone_merge_request(merge_request, new_milestone, current_user)
    changed_milestone_resource_email(merge_request, new_milestone, current_user, :changed_milestone_merge_request_email)
  end

228
  def close_mr(merge_request, current_user)
jneen's avatar
jneen committed
229
    close_resource_email(merge_request, current_user, :closed_merge_request_email)
230 231
  end

232
  def reopen_issue(issue, current_user)
jneen's avatar
jneen committed
233
    reopen_resource_email(issue, current_user, :issue_status_changed_email, 'reopened')
234 235
  end

236
  def merge_mr(merge_request, current_user)
Valeriy's avatar
Valeriy committed
237 238 239
    close_resource_email(
      merge_request,
      current_user,
240
      :merged_merge_request_email,
241
      skip_current_user: !merge_request.merge_when_pipeline_succeeds?
Valeriy's avatar
Valeriy committed
242
    )
243 244
  end

245
  def reopen_mr(merge_request, current_user)
Valeriy's avatar
Valeriy committed
246 247
    reopen_resource_email(
      merge_request,
248
      current_user,
249
      :merge_request_status_email,
Valeriy's avatar
Valeriy committed
250 251
      'reopened'
    )
252 253
  end

254
  def resolve_all_discussions(merge_request, current_user)
255
    recipients = NotificationRecipientService.build_recipients(
256 257 258
      merge_request,
      current_user,
      action: "resolve_all_discussions")
259 260

    recipients.each do |recipient|
261
      mailer.resolved_all_discussions_email(recipient.user.id, merge_request.id, current_user.id, recipient.reason).deliver_later
262 263 264
    end
  end

265
  # Notify new user with email after creation
266
  def new_user(user, token = nil)
jneen's avatar
jneen committed
267 268
    return true unless notifiable?(user, :mention)

Johannes's avatar
Johannes committed
269
    # Don't email omniauth created users
Valeriy's avatar
Valeriy committed
270
    mailer.new_user_email(user.id, token).deliver_later unless user.identities.any?
271 272 273 274
  end

  # Notify users on new note in system
  def new_note(note)
275 276
    return true unless note.noteable_type.present?

277
    # ignore gitlab service messages
Z.J. van de Weg's avatar
Z.J. van de Weg committed
278
    return true if note.cross_reference? && note.system?
279

280 281 282 283
    send_new_note_notifications(note)
  end

  def send_new_note_notifications(note)
Jarka Košanová's avatar
Jarka Košanová committed
284
    notify_method = "note_#{note.to_ability_name}_email".to_sym
285

jneen's avatar
jneen committed
286
    recipients = NotificationRecipientService.build_new_note_recipients(note)
287
    recipients.each do |recipient|
288
      mailer.send(notify_method, recipient.user.id, note.id).deliver_later
289 290
    end
  end
291

Rémy Coutable's avatar
Rémy Coutable committed
292 293
  # Members
  def new_access_request(member)
jneen's avatar
jneen committed
294 295
    return true unless member.notifiable?(:subscription)

296 297 298
    recipients = member.source.members.active_without_invites_and_requests.owners_and_maintainers
    if fallback_to_group_owners_maintainers?(recipients, member)
      recipients = member.source.group.members.active_without_invites_and_requests.owners_and_maintainers
299 300 301
    end

    recipients.each { |recipient| deliver_access_request_email(recipient, member) }
302 303
  end

Rémy Coutable's avatar
Rémy Coutable committed
304
  def decline_access_request(member)
jneen's avatar
jneen committed
305 306
    return true unless member.notifiable?(:subscription)

Rémy Coutable's avatar
Rémy Coutable committed
307
    mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later
308 309
  end

Rémy Coutable's avatar
Rémy Coutable committed
310
  # Project invite
Douwe Maan's avatar
Douwe Maan committed
311
  def invite_project_member(project_member, token)
jneen's avatar
jneen committed
312 313
    return true unless project_member.notifiable?(:subscription)

314
    mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later
Douwe Maan's avatar
Douwe Maan committed
315 316 317
  end

  def accept_project_invite(project_member)
jneen's avatar
jneen committed
318 319
    return true unless project_member.notifiable?(:subscription)

320
    mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later
Douwe Maan's avatar
Douwe Maan committed
321 322
  end

Douwe Maan's avatar
Douwe Maan committed
323
  def decline_project_invite(project_member)
324
    mailer.member_invite_declined_email(
325
      project_member.real_source_type,
Valeriy's avatar
Valeriy committed
326 327 328 329
      project_member.project.id,
      project_member.invite_email,
      project_member.created_by_id
    ).deliver_later
Douwe Maan's avatar
Douwe Maan committed
330 331
  end

332
  def new_project_member(project_member)
333
    return true unless project_member.notifiable?(:mention, skip_read_ability: true)
jneen's avatar
jneen committed
334

335
    mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
336 337
  end

338
  def update_project_member(project_member)
jneen's avatar
jneen committed
339 340
    return true unless project_member.notifiable?(:mention)

341
    mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
342
  end
343

Rémy Coutable's avatar
Rémy Coutable committed
344
  # Group invite
Douwe Maan's avatar
Douwe Maan committed
345
  def invite_group_member(group_member, token)
346
    mailer.member_invited_email(group_member.real_source_type, group_member.id, token).deliver_later
Douwe Maan's avatar
Douwe Maan committed
347 348 349
  end

  def accept_group_invite(group_member)
350
    mailer.member_invite_accepted_email(group_member.real_source_type, group_member.id).deliver_later
Douwe Maan's avatar
Douwe Maan committed
351 352
  end

Douwe Maan's avatar
Douwe Maan committed
353
  def decline_group_invite(group_member)
jneen's avatar
jneen committed
354 355 356
    # always send this one, since it's a response to the user's own
    # action

357
    mailer.member_invite_declined_email(
358
      group_member.real_source_type,
Valeriy's avatar
Valeriy committed
359 360 361 362
      group_member.group.id,
      group_member.invite_email,
      group_member.created_by_id
    ).deliver_later
Douwe Maan's avatar
Douwe Maan committed
363 364
  end

365
  def new_group_member(group_member)
jneen's avatar
jneen committed
366 367
    return true unless group_member.notifiable?(:mention)

368
    mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
369 370
  end

371
  def update_group_member(group_member)
jneen's avatar
jneen committed
372 373
    return true unless group_member.notifiable?(:mention)

374
    mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
375 376
  end

377
  def project_was_moved(project, old_path_with_namespace)
378 379
    recipients = project.private? ? project.team.members_in_project_and_ancestors : project.team.members
    recipients = notifiable_users(recipients, :mention, project: project)
380 381

    recipients.each do |recipient|
Valeriy's avatar
Valeriy committed
382 383 384 385 386
      mailer.project_was_moved_email(
        project.id,
        recipient.id,
        old_path_with_namespace
      ).deliver_later
387 388 389
    end
  end

390
  def issue_moved(issue, new_issue, current_user)
jneen's avatar
jneen committed
391
    recipients = NotificationRecipientService.build_recipients(issue, current_user, action: 'moved')
392 393

    recipients.map do |recipient|
394
      email = mailer.issue_moved_email(recipient.user, issue, new_issue, current_user, recipient.reason)
395 396 397 398 399
      email.deliver_later
      email
    end
  end

400
  def project_exported(project, current_user)
jneen's avatar
jneen committed
401 402
    return true unless notifiable?(current_user, :mention, project: project)

403 404 405 406
    mailer.project_was_exported_email(current_user, project).deliver_later
  end

  def project_not_exported(project, current_user, errors)
jneen's avatar
jneen committed
407 408
    return true unless notifiable?(current_user, :mention, project: project)

409 410 411
    mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
  end

412 413 414 415 416
  def pipeline_finished(pipeline, recipients = nil)
    email_template = "pipeline_#{pipeline.status}_email"

    return unless mailer.respond_to?(email_template)

jneen's avatar
jneen committed
417
    recipients ||= notifiable_users(
418
      [pipeline.user], :watch,
419
      custom_action: :"#{pipeline.status}_pipeline",
420
      target: pipeline
Sean McGivern's avatar
Sean McGivern committed
421
    ).map(&:notification_email)
422

423 424 425
    if recipients.any?
      mailer.public_send(email_template, pipeline, recipients).deliver_later
    end
426 427
  end

428 429 430 431 432 433
  def autodevops_disabled(pipeline, recipients)
    recipients.each do |recipient|
      mailer.autodevops_disabled_email(pipeline, recipient).deliver_later
    end
  end

434
  def pages_domain_verification_succeeded(domain)
435 436
    project_maintainers_recipients(domain, action: 'succeeded').each do |recipient|
      mailer.pages_domain_verification_succeeded_email(domain, recipient.user).deliver_later
437 438 439 440
    end
  end

  def pages_domain_verification_failed(domain)
441 442
    project_maintainers_recipients(domain, action: 'failed').each do |recipient|
      mailer.pages_domain_verification_failed_email(domain, recipient.user).deliver_later
443 444 445 446
    end
  end

  def pages_domain_enabled(domain)
447 448
    project_maintainers_recipients(domain, action: 'enabled').each do |recipient|
      mailer.pages_domain_enabled_email(domain, recipient.user).deliver_later
449 450 451 452
    end
  end

  def pages_domain_disabled(domain)
453 454
    project_maintainers_recipients(domain, action: 'disabled').each do |recipient|
      mailer.pages_domain_disabled_email(domain, recipient.user).deliver_later
455 456 457
    end
  end

458
  def issue_due(issue)
stuart nelson's avatar
stuart nelson committed
459 460 461
    recipients = NotificationRecipientService.build_recipients(
      issue,
      issue.author,
462 463
      action: 'due',
      custom_action: :issue_due,
stuart nelson's avatar
stuart nelson committed
464
      skip_current_user: false
stuart nelson's avatar
stuart nelson committed
465 466 467 468 469 470 471
    )

    recipients.each do |recipient|
      mailer.send(:issue_due_email, recipient.user.id, issue.id, recipient.reason).deliver_later
    end
  end

472 473 474 475 476 477 478 479
  def repository_cleanup_success(project, user)
    mailer.send(:repository_cleanup_success_email, project, user).deliver_later
  end

  def repository_cleanup_failure(project, user, error)
    mailer.send(:repository_cleanup_failure_email, project, user, error).deliver_later
  end

480 481 482 483 484 485 486 487
  def remote_mirror_update_failed(remote_mirror)
    recipients = project_maintainers_recipients(remote_mirror, action: 'update_failed')

    recipients.each do |recipient|
      mailer.remote_mirror_update_failed_email(remote_mirror.id, recipient.user.id).deliver_later
    end
  end

488 489
  protected

jneen's avatar
jneen committed
490 491
  def new_resource_email(target, method)
    recipients = NotificationRecipientService.build_recipients(target, target.author, action: "new")
492 493

    recipients.each do |recipient|
494
      mailer.send(method, recipient.user.id, target.id, recipient.reason).deliver_later
495 496 497
    end
  end

jneen's avatar
jneen committed
498 499
  def new_mentions_in_resource_email(target, new_mentioned_users, current_user, method)
    recipients = NotificationRecipientService.build_recipients(target, current_user, action: "new")
500
    recipients = recipients.select {|r| new_mentioned_users.include?(r.user) }
501 502

    recipients.each do |recipient|
503
      mailer.send(method, recipient.user.id, target.id, current_user.id, recipient.reason).deliver_later
504 505 506
    end
  end

jneen's avatar
jneen committed
507
  def close_resource_email(target, current_user, method, skip_current_user: true)
508
    action = method == :merged_merge_request_email ? "merge" : "close"
509

510
    recipients = NotificationRecipientService.build_recipients(
511 512 513 514 515
      target,
      current_user,
      action: action,
      skip_current_user: skip_current_user
    )
516 517

    recipients.each do |recipient|
518
      mailer.send(method, recipient.user.id, target.id, current_user.id, recipient.reason).deliver_later
519 520 521
    end
  end

jneen's avatar
jneen committed
522
  def relabeled_resource_email(target, labels, current_user, method)
523
    recipients = labels.flat_map { |l| l.subscribers(target.project) }.uniq
jneen's avatar
jneen committed
524
    recipients = notifiable_users(
jneen's avatar
jneen committed
525 526
      recipients, :subscription,
      target: target,
jneen's avatar
jneen committed
527
      acting_user: current_user
jneen's avatar
jneen committed
528 529
    )

530 531 532
    label_names = labels.map(&:name)

    recipients.each do |recipient|
533
      mailer.send(method, recipient.id, target.id, label_names, current_user.id).deliver_later
534 535 536
    end
  end

537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
  def removed_milestone_resource_email(target, current_user, method)
    recipients = NotificationRecipientService.build_recipients(
      target,
      current_user,
      action: 'removed_milestone'
    )

    recipients.each do |recipient|
      mailer.send(method, recipient.user.id, target.id, current_user.id).deliver_later
    end
  end

  def changed_milestone_resource_email(target, milestone, current_user, method)
    recipients = NotificationRecipientService.build_recipients(
      target,
      current_user,
      action: 'changed_milestone'
    )

    recipients.each do |recipient|
      mailer.send(method, recipient.user.id, target.id, milestone, current_user.id).deliver_later
    end
  end

jneen's avatar
jneen committed
561 562
  def reopen_resource_email(target, current_user, method, status)
    recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen")
563 564

    recipients.each do |recipient|
565
      mailer.send(method, recipient.user.id, target.id, status, current_user.id, recipient.reason).deliver_later
566 567 568
    end
  end

569 570 571 572 573 574 575 576
  def merge_request_unmergeable_email(merge_request)
    recipients = NotificationRecipientService.build_merge_request_unmergeable_recipients(merge_request)

    recipients.each do |recipient|
      mailer.merge_request_unmergeable_email(recipient.user.id, merge_request.id).deliver_later
    end
  end

577
  def mailer
Valeriy's avatar
Valeriy committed
578
    Notify
579
  end
580

jneen's avatar
jneen committed
581 582
  private

583 584
  def project_maintainers_recipients(target, action:)
    NotificationRecipientService.build_project_maintainers_recipients(target, action: action)
585 586
  end

jneen's avatar
jneen committed
587 588 589 590 591 592 593
  def notifiable?(*args)
    NotificationRecipientService.notifiable?(*args)
  end

  def notifiable_users(*args)
    NotificationRecipientService.notifiable_users(*args)
  end
594 595 596 597 598

  def deliver_access_request_email(recipient, member)
    mailer.member_access_requested_email(member.real_source_type, member.id, recipient.user.notification_email).deliver_later
  end

599
  def fallback_to_group_owners_maintainers?(recipients, member)
600 601 602 603
    return false if recipients.present?

    member.source.respond_to?(:group) && member.source.group
  end
604
end