Newer
Older
GitBucket / src / main / scala / gitbucket / core / util / Notifier.scala
package gitbucket.core.util

import gitbucket.core.model.{Session, Issue, Account}
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, SystemSettingsService}
import gitbucket.core.servlet.Database
import gitbucket.core.view.Markdown

import scala.concurrent._
import scala.util.{Success, Failure}
import ExecutionContext.Implicits.global
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.slf4j.LoggerFactory
import gitbucket.core.controller.Context
import SystemSettingsService.Smtp

/**
 * The trait for notifications.
 * This is used by notifications plugin, which provides notifications feature on GitBucket.
 * Please see the plugin for details.
 */
trait Notifier {

  def toNotify(subject: String, msg: String)
              (recipients: Account => Session => Seq[String])(implicit context: Context): Unit

}

object Notifier {
  def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
    case settings if (settings.notification && settings.useSMTP) => new Mailer(settings.smtp.get)
    case _ => new MockMailer
  }


  // TODO This class is temporary keeping the current feature until Notifications Plugin is available.
  class IssueHook extends gitbucket.core.plugin.IssueHook
    with RepositoryService with AccountService with IssuesService {

    override def created(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
      Notifier().toNotify(
        subject(issue, r),
        message(issue.content getOrElse "", r)(content => s"""
          |$content<br/>
          |--<br/>
          |<a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}"}">View it on GitBucket</a>
          """.stripMargin)
      )(recipients(issue))
    }

    override def addedComment(commentId: Int, content: String, issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
      Notifier().toNotify(
        subject(issue, r),
        message(content, r)(content => s"""
          |$content<br/>
          |--<br/>
          |<a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}#comment-$commentId"}">View it on GitBucket</a>
          """.stripMargin)
      )(recipients(issue))
    }

    override def closed(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
      Notifier().toNotify(
        subject(issue, r),
        message("close", r)(content => s"""
          |$content <a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}"}">#${issue.issueId}</a>
          """.stripMargin)
      )(recipients(issue))
    }

    override def reopened(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
      Notifier().toNotify(
        subject(issue, r),
        message("reopen", r)(content => s"""
          |$content <a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}"}">#${issue.issueId}</a>
          """.stripMargin)
      )(recipients(issue))
    }


    protected def subject(issue: Issue, r: RepositoryService.RepositoryInfo): String =
      s"[${r.owner}/${r.name}] ${issue.title} (#${issue.issueId})"

    protected def message(content: String, r: RepositoryService.RepositoryInfo)(msg: String => String)(implicit context: Context): String =
      msg(Markdown.toHtml(
        markdown         = content,
        repository       = r,
        enableWikiLink   = false,
        enableRefsLink   = true,
        enableAnchor     = false,
        enableLineBreaks = false
      ))

    protected val recipients: Issue => Account => Session => Seq[String] = {
      issue => loginAccount => implicit session =>
        (
          // individual repository's owner
          issue.userName ::
          // group members of group repository
          getGroupMembers(issue.userName).map(_.userName) :::
          // collaborators
          getCollaboratorUserNames(issue.userName, issue.repositoryName) :::
          // participants
          issue.openedUserName ::
          getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
        )
        .distinct
        .withFilter ( _ != loginAccount.userName )  // the operation in person is excluded
        .flatMap (
          getAccountByUserName(_)
            .filterNot (_.isGroupAccount)
            .filterNot (LDAPUtil.isDummyMailAddress)
            .map (_.mailAddress)
        )
    }
  }

  // TODO This class is temporary keeping the current feature until Notifications Plugin is available.
  class PullRequestHook extends IssueHook with gitbucket.core.plugin.PullRequestHook {
    override def created(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
      val url = s"${context.baseUrl}/${r.owner}/${r.name}/pull/${issue.issueId}"
      Notifier().toNotify(
        subject(issue, r),
        message(issue.content getOrElse "", r)(content => s"""
          |$content<hr/>
          |View, comment on, or merge it at:<br/>
          |<a href="$url">$url</a>
          """.stripMargin)
      )(recipients(issue))
    }

    override def addedComment(commentId: Int, content: String, issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
      Notifier().toNotify(
        subject(issue, r),
        message(content, r)(content => s"""
          |$content<br/>
          |--<br/>
          |<a href="${s"${context.baseUrl}/${r.owner}/${r.name}/pull/${issue.issueId}#comment-$commentId"}">View it on GitBucket</a>
          """.stripMargin)
      )(recipients(issue))
    }

    override def merged(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
      Notifier().toNotify(
        subject(issue, r),
        message("merge", r)(content => s"""
          |$content <a href="${s"${context.baseUrl}/${r.owner}/${r.name}/pull/${issue.issueId}"}">#${issue.issueId}</a>
          """.stripMargin)
      )(recipients(issue))
    }
  }

}

class Mailer(private val smtp: Smtp) extends Notifier {
  private val logger = LoggerFactory.getLogger(classOf[Mailer])

  def toNotify(subject: String, msg: String)
              (recipients: Account => Session => Seq[String])(implicit context: Context): Unit = {
    context.loginAccount.foreach { loginAccount =>
      val database = Database()

      val f = Future {
        database withSession { session =>
          recipients(loginAccount)(session) foreach { to =>
            send(to, subject, msg, loginAccount)
          }
        }
        "Notifications Successful."
      }
      f.onComplete {
        case Success(s) => logger.debug(s)
        case Failure(t) => logger.error("Notifications Failed.", t)
      }
    }
  }

  def send(to: String, subject: String, msg: String, loginAccount: Account): Unit = {
    val email = new HtmlEmail
    email.setHostName(smtp.host)
    email.setSmtpPort(smtp.port.get)
    smtp.user.foreach { user =>
      email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
    }
    smtp.ssl.foreach { ssl =>
      email.setSSLOnConnect(ssl)
      if(ssl == true) {
        email.setSslSmtpPort(smtp.port.get.toString)
      }
    }
    smtp.starttls.foreach { starttls =>
      email.setStartTLSEnabled(starttls)
      email.setStartTLSRequired(starttls)
    }
    smtp.fromAddress
      .map (_ -> smtp.fromName.getOrElse(loginAccount.userName))
      .orElse (Some("notifications@gitbucket.com" -> loginAccount.userName))
      .foreach { case (address, name) =>
        email.setFrom(address, name)
      }
    email.setCharset("UTF-8")
    email.setSubject(subject)
    email.setHtmlMsg(msg)

    email.addTo(to).send
  }

}
class MockMailer extends Notifier {
  def toNotify(subject: String, msg: String)
              (recipients: Account => Session => Seq[String])(implicit context: Context): Unit = ()
}