]> source.dussan.org Git - gitblit.git/commitdiff
HTML email notification methods and hook (pull request #51)
authorGuillaume Sauthier <guillaume.sauthier@peergreen.com>
Mon, 5 Nov 2012 16:30:06 +0000 (11:30 -0500)
committerJames Moger <james.moger@gitblit.com>
Mon, 5 Nov 2012 16:30:06 +0000 (11:30 -0500)
docs/04_releases.mkd
groovy/sendmail-html.groovy [new file with mode: 0644]
src/com/gitblit/GitBlit.java
tests/com/gitblit/tests/GroovyScriptTest.java

index 2d5d7e55dec5249282b7b6027f37cdd6c6de4757..f0470e5c37359d2cbd08e533e853cfdab0871690 100644 (file)
@@ -50,6 +50,7 @@ In order to fork a repository, the user account must have the *fork* permission
 - Added support for X-Forwarded-Context for Apache subdomain proxy configurations (issue 135)\r
 - Delete branch feature (issue 121, Github/ajermakovics)\r
 - Added line links to blob view (issue 130)\r
+- Added HTML sendmail hook script and Gitblit.sendHtmlMail method (github/sauthieg)\r
 - Added RedmineUserService (github/mallowlabs)\r
 - Support for committer verification.  Requires use of *--no-ff* when merging branches or pull requests.  See setup page for details.\r
 \r
diff --git a/groovy/sendmail-html.groovy b/groovy/sendmail-html.groovy
new file mode 100644 (file)
index 0000000..fb89a14
--- /dev/null
@@ -0,0 +1,544 @@
+/*\r
+ * Copyright 2012 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *     http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+import com.gitblit.GitBlit\r
+import com.gitblit.Keys\r
+import com.gitblit.models.RepositoryModel\r
+import com.gitblit.models.TeamModel\r
+import com.gitblit.models.UserModel\r
+import com.gitblit.utils.JGitUtils\r
+import java.text.SimpleDateFormat\r
+\r
+import org.eclipse.jgit.api.Status;\r
+import org.eclipse.jgit.api.errors.JGitInternalException;\r
+import org.eclipse.jgit.diff.DiffEntry;\r
+import org.eclipse.jgit.diff.DiffFormatter;\r
+import org.eclipse.jgit.diff.RawTextComparator;\r
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;\r
+import org.eclipse.jgit.lib.Constants;\r
+import org.eclipse.jgit.lib.IndexDiff;\r
+import org.eclipse.jgit.lib.ObjectId;\r
+import org.eclipse.jgit.lib.Repository\r
+import org.eclipse.jgit.lib.Config\r
+import org.eclipse.jgit.patch.FileHeader;\r
+import org.eclipse.jgit.revwalk.RevCommit\r
+import org.eclipse.jgit.revwalk.RevWalk;\r
+import org.eclipse.jgit.transport.ReceiveCommand\r
+import org.eclipse.jgit.transport.ReceiveCommand.Result\r
+import org.eclipse.jgit.treewalk.FileTreeIterator;\r
+import org.eclipse.jgit.treewalk.EmptyTreeIterator;\r
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;\r
+import org.eclipse.jgit.util.io.DisabledOutputStream;\r
+import org.slf4j.Logger\r
+import groovy.xml.MarkupBuilder\r
+\r
+import java.io.IOException;\r
+import java.security.MessageDigest\r
+\r
+\r
+/**\r
+ * Sample Gitblit Post-Receive Hook: sendmail-html\r
+ *\r
+ * The Post-Receive hook is executed AFTER the pushed commits have been applied\r
+ * to the Git repository.  This is the appropriate point to trigger an\r
+ * integration build or to send a notification.\r
+ * \r
+ * This script is only executed when pushing to *Gitblit*, not to other Git\r
+ * tooling you may be using.\r
+ * \r
+ * If this script is specified in *groovy.postReceiveScripts* of gitblit.properties\r
+ * or web.xml then it will be executed by any repository when it receives a\r
+ * push.  If you choose to share your script then you may have to consider\r
+ * tailoring control-flow based on repository access restrictions.\r
+ *\r
+ * Scripts may also be specified per-repository in the repository settings page.\r
+ * Shared scripts will be excluded from this list of available scripts.\r
+ * \r
+ * This script is dynamically reloaded and it is executed within it's own\r
+ * exception handler so it will not crash another script nor crash Gitblit.\r
+ *\r
+ * If you want this hook script to fail and abort all subsequent scripts in the\r
+ * chain, "return false" at the appropriate failure points.\r
+ * \r
+ * Bound Variables:\r
+ *  gitblit         Gitblit Server               com.gitblit.GitBlit\r
+ *  repository      Gitblit Repository           com.gitblit.models.RepositoryModel\r
+ *  user            Gitblit User                 com.gitblit.models.UserModel\r
+ *  commands        JGit commands                Collection<org.eclipse.jgit.transport.ReceiveCommand>\r
+ *  url             Base url for Gitblit         java.lang.String\r
+ *  logger          Logs messages to Gitblit     org.slf4j.Logger\r
+ *  clientLogger    Logs messages to Git client  com.gitblit.utils.ClientLogger\r
+ *\r
+ * Accessing Gitblit Custom Fields:\r
+ *   def myCustomField = repository.customFields.myCustomField\r
+ *  \r
+ */\r
+\r
+com.gitblit.models.UserModel userModel = user\r
+\r
+// Indicate we have started the script\r
+logger.info("sendmail hook triggered by ${user.username} for ${repository.name}")\r
+\r
+/*\r
+ * Primitive email notification.\r
+ * This requires the mail settings to be properly configured in Gitblit.\r
+ */\r
+\r
+Repository r = gitblit.getRepository(repository.name)\r
+\r
+// reuse existing repository config settings, if available\r
+Config config = r.getConfig()\r
+def mailinglist = config.getString('hooks', null, 'mailinglist')\r
+def emailprefix = config.getString('hooks', null, 'emailprefix')\r
+\r
+// set default values\r
+def toAddresses = []\r
+if (emailprefix == null) {\r
+    emailprefix = '[Gitblit]'\r
+}\r
+\r
+if (mailinglist != null) {\r
+    def addrs = mailinglist.split(/(,|\s)/)\r
+    toAddresses.addAll(addrs)\r
+}\r
+\r
+// add all mailing lists defined in gitblit.properties or web.xml\r
+toAddresses.addAll(gitblit.getStrings(Keys.mail.mailingLists))\r
+\r
+// add all team mailing lists\r
+def teams = gitblit.getRepositoryTeams(repository)\r
+for (team in teams) {\r
+    TeamModel model = gitblit.getTeamModel(team)\r
+    if (model.mailingLists) {\r
+        toAddresses.addAll(model.mailingLists)\r
+    }\r
+}\r
+\r
+// add all mailing lists for the repository\r
+toAddresses.addAll(repository.mailingLists)\r
+\r
+// define the summary and commit urls\r
+def repo = repository.name\r
+def summaryUrl = url + "/summary?r=$repo"\r
+def baseCommitUrl = url + "/commit?r=$repo&h="\r
+def baseBlobDiffUrl = url + "/blobdiff/?r=$repo&h="\r
+def baseCommitDiffUrl = url + "/commitdiff/?r=$repo&h="\r
+\r
+if (gitblit.getBoolean(Keys.web.mountParameters, true)) {\r
+    repo = repo.replace('/', gitblit.getString(Keys.web.forwardSlashCharacter, '/')).replace('/', '%2F')\r
+    summaryUrl = url + "/summary/$repo"\r
+    baseCommitUrl = url + "/commit/$repo/"\r
+    baseBlobDiffUrl = url + "/blobdiff/$repo/"\r
+    baseCommitDiffUrl = url + "/commitdiff/$repo/"\r
+}\r
+\r
+class HtmlMailWriter {\r
+    Repository repository\r
+    def url\r
+    def baseCommitUrl\r
+    def baseCommitDiffUrl\r
+    def baseBlobDiffUrl\r
+    def mountParameters\r
+    def commitCount = 0\r
+    def commands\r
+    def writer = new StringWriter();\r
+    def builder = new MarkupBuilder(writer)\r
+\r
+    def writeStyle() {\r
+        builder.style(type:"text/css", '''\r
+    th, td {  \r
+        padding: 2px;  \r
+    }\r
+    thead {\r
+        text-align: left;\r
+        font-weight: bold; \r
+    }\r
+    thead tr {\r
+        border-bottom: 1px dotted #000; \r
+    }\r
+    a {\r
+        text-decoration: none;\r
+    }\r
+    .commits-table {\r
+        border-collapse: collapse;\r
+        font-family: sans-serif; \r
+        width: 100%; \r
+    }\r
+    .label-commit {\r
+        border-radius:4px;\r
+        background-color: #3A87AD;\r
+        padding: 2px 4px;\r
+        color: white;\r
+        vertical-align: baseline; \r
+        font-weight: bold; \r
+        font-family: monospace; \r
+    }\r
+    .label-add {\r
+        border-radius:4px;\r
+        background-color: green;\r
+        padding: 2px 4px;\r
+        color: white;\r
+        vertical-align: baseline; \r
+        font-weight: bold; \r
+        font-family: monospace; \r
+    }\r
+    .label-delete {\r
+        border-radius:4px;\r
+        background-color: grey;\r
+        padding: 2px 4px;\r
+        color: white;\r
+        vertical-align: baseline; \r
+        font-weight: bold; \r
+        font-family: monospace; \r
+    }\r
+    .label-rename {\r
+        border-radius:4px;\r
+        background-color: blue;\r
+        padding: 2px 4px;\r
+        color: white;\r
+        vertical-align: baseline; \r
+        font-weight: bold; \r
+        font-family: monospace; \r
+    }\r
+    .label-modify {\r
+        border-radius:4px;\r
+        background-color: orange;\r
+        padding: 2px 4px;\r
+        color: white;\r
+        vertical-align: baseline; \r
+        font-weight: bold; \r
+        font-family: monospace; \r
+    }\r
+    .label-copy {\r
+        border-radius:4px;\r
+        background-color: teal;\r
+        padding: 2px 4px;\r
+        color: white;\r
+        vertical-align: baseline; \r
+        font-weight: bold; \r
+        font-family: monospace; \r
+    }\r
+    .gravatar-column {\r
+        width: 5%; \r
+    }\r
+    .author-column {\r
+        width: 10%; \r
+    }\r
+    .commit-column {\r
+        width: 5%; \r
+    }\r
+    .status-column {\r
+        width: 10%;\r
+        padding-bottom: 5px; \r
+        padding-top: 5px; \r
+    }\r
+    ''')\r
+    }\r
+\r
+    def writeBranchTitle(type, name, action, number) {\r
+        builder.h2 {\r
+            mkp.yield "$type "\r
+            span(style:"font-family: monospace;", name )\r
+            mkp.yield " $action ($number commits)"\r
+        }\r
+    }\r
+\r
+    def writeBranchDeletedTitle(type, name) {\r
+        builder.h2 {\r
+            mkp.yield "$type "\r
+            span(style:"font-family: monospace;", name )\r
+            mkp.yield " deleted"\r
+        }\r
+    }\r
+\r
+    def commitUrl(RevCommit commit) {\r
+        "${baseCommitUrl}$commit.id.name"\r
+    }\r
+\r
+    def commitDiffUrl(RevCommit commit) {\r
+        "${baseCommitDiffUrl}$commit.id.name"\r
+    }\r
+\r
+    def encoded(String path) {\r
+        path.replace('/', '!')\r
+    }\r
+\r
+    def blobDiffUrl(objectId, path) {\r
+        if (mountParameters) {\r
+            // REST style\r
+            "${baseBlobDiffUrl}${objectId.name()}/${encoded(path)}"\r
+        } else {\r
+            "${baseBlobDiffUrl}${objectId.name()}&f=${path}"\r
+        }\r
+\r
+    }\r
+\r
+    def writeCommitTable(commits) {\r
+        // Write commits table\r
+        builder.table('class':"commits-table") {\r
+            thead {\r
+                tr {\r
+                    th(colspan:2, "Author")\r
+                    th( "Commit" )\r
+                    th( "Message" )\r
+                }\r
+            }\r
+            tbody() {\r
+\r
+                // Write all the commits\r
+                for (commit in commits) {\r
+                    writeCommit(commit)\r
+\r
+                    // Write detail on that particular commit\r
+                    tr {\r
+                        td (colspan:3)\r
+                        td { writeStatusTable(commit) }\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    def writeCommit(commit) {\r
+        def abbreviated = repository.newObjectReader().abbreviate(commit.id, 6).name()\r
+        def author = commit.authorIdent.name\r
+        def email = commit.authorIdent.emailAddress\r
+        def message = commit.shortMessage\r
+        builder.tr {\r
+            td('class':"gravatar-column") {\r
+                img(src:gravatarUrl(email))\r
+            }\r
+            td('class':"author-column") { p(author) }\r
+            td('class':"commit-column") {\r
+                a(href:commitUrl(commit)) {\r
+                    span('class':"label-commit",  abbreviated )\r
+                }\r
+            }\r
+            td {\r
+                mkp.yield message\r
+                a(href:commitDiffUrl(commit), " [commitdiff]" )\r
+            }\r
+        }\r
+    }\r
+\r
+    def writeStatusLabel(style, label) {\r
+        builder.span('class' : style,  label )\r
+    }\r
+\r
+    def writeAddStatusLine(ObjectId id, FileHeader header) {\r
+        builder.td('class':"status-column") {\r
+            a(href:blobDiffUrl(id, header.newPath)) { writeStatusLabel("label-add", "add") }\r
+        }\r
+        builder.td {\r
+            span(style:'font-family: monospace;', header.newPath)\r
+        }\r
+    }\r
+\r
+    def writeCopyStatusLine(ObjectId id, FileHeader header) {\r
+        builder.td('class':"status-column") {\r
+            a(href:blobDiffUrl(id, header.newPath)) { writeStatusLabel("label-copy", "copy") }\r
+        }\r
+        builder.td() {\r
+            span(style : "font-family: monospace; ", header.oldPath + " copied to " + header.newPath)\r
+        }\r
+    }\r
+\r
+    def writeDeleteStatusLine(ObjectId id, FileHeader header) {\r
+        builder.td('class':"status-column") {\r
+            a(href:blobDiffUrl(id, header.oldPath)) { writeStatusLabel("label-delete", "delete") }\r
+        }\r
+        builder.td() {\r
+            span(style : "font-family: monospace; ", header.oldPath)\r
+        }\r
+    }\r
+\r
+    def writeModifyStatusLine(ObjectId id, FileHeader header) {\r
+        builder.td('class':"status-column") {\r
+            a(href:blobDiffUrl(id, header.oldPath)) { writeStatusLabel("label-modify", "modify") }\r
+        }\r
+        builder.td() {\r
+            span(style : "font-family: monospace; ", header.oldPath)\r
+        }\r
+    }\r
+\r
+    def writeRenameStatusLine(ObjectId id, FileHeader header) {\r
+        builder.td('class':"status-column") {\r
+            a(href:blobDiffUrl(id, header.newPath)) { writeStatusLabel("label-rename", "rename") }\r
+        }\r
+        builder.td() {\r
+            span(style : "font-family: monospace; ", header.olPath + " -> " + header.newPath)\r
+        }\r
+    }\r
+\r
+    def writeStatusLine(ObjectId id, FileHeader header) {\r
+        builder.tr {\r
+            switch (header.changeType) {\r
+                case ChangeType.ADD:\r
+                    writeAddStatusLine(id, header)\r
+                    break;\r
+                case ChangeType.COPY:\r
+                    writeCopyStatusLine(id, header)\r
+                    break;\r
+                case ChangeType.DELETE:\r
+                    writeDeleteStatusLine(id, header)\r
+                    break;\r
+                case ChangeType.MODIFY:\r
+                    writeModifyStatusLine(id, header)\r
+                    break;\r
+                case ChangeType.RENAME:\r
+                    writeRenameStatusLine(id, header)\r
+                    break;\r
+            }\r
+        }\r
+    }\r
+\r
+    def writeStatusTable(RevCommit commit) {\r
+        DiffFormatter formatter = new DiffFormatter(DisabledOutputStream.INSTANCE)\r
+        formatter.setRepository(repository)\r
+        formatter.setDetectRenames(true)\r
+        formatter.setDiffComparator(RawTextComparator.DEFAULT);\r
+\r
+        def diffs\r
+        RevWalk rw = new RevWalk(repository);\r
+        if (commit.parentCount > 0) {\r
+            RevCommit parent = commit.parents[0]\r
+            diffs = formatter.scan(parent.tree, commit.tree)\r
+        } else {\r
+            diffs = formatter.scan(new EmptyTreeIterator(),\r
+                                   new CanonicalTreeParser(null, rw.objectReader, commit.tree))\r
+        }\r
+        // Write status table\r
+        builder.table('class':"commits-table") {\r
+            tbody() {\r
+                for (DiffEntry entry in diffs) {\r
+                    FileHeader header = formatter.toFileHeader(entry)\r
+                    writeStatusLine(commit.id, header)\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+\r
+    def md5(text) {\r
+\r
+        def digest = MessageDigest.getInstance("MD5")\r
+\r
+        //Quick MD5 of text\r
+        def hash = new BigInteger(1, digest.digest(text.getBytes()))\r
+                         .toString(16)\r
+                         .padLeft(32, "0")\r
+        hash.toString()\r
+    }\r
+\r
+    def gravatarUrl(email) {\r
+        def cleaned = email.trim().toLowerCase()\r
+        "http://www.gravatar.com/avatar/${md5(cleaned)}?s=30"\r
+    }\r
+\r
+    def writeNavbar() {\r
+        builder.div('class':"navbar navbar-fixed-top") {\r
+            div('class':"navbar-inner") {\r
+                div('class':"container") {\r
+                    a('class':"brand", href:"${url}", title:"GitBlit") {\r
+                        img(src:"${url}/gitblt_25_white.png",\r
+                            width:"79",\r
+                            height:"25",\r
+                            'class':"logo")\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    def write() {\r
+        builder.html {\r
+            head {\r
+                link(rel:"stylesheet", href:"${url}/bootstrap/css/bootstrap.css")\r
+                link(rel:"stylesheet", href:"${url}/gitblit.css")\r
+                writeStyle()\r
+            }\r
+            body {\r
+\r
+                writeNavbar()\r
+\r
+                for (command in commands) {\r
+                    def ref = command.refName\r
+                    def refType = 'Branch'\r
+                    if (ref.startsWith('refs/heads/')) {\r
+                        ref  = command.refName.substring('refs/heads/'.length())\r
+                    } else if (ref.startsWith('refs/tags/')) {\r
+                        ref  = command.refName.substring('refs/tags/'.length())\r
+                        refType = 'Tag'\r
+                    }\r
+\r
+                    switch (command.type) {\r
+                        case ReceiveCommand.Type.CREATE:\r
+                            def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()\r
+                            commitCount += commits.size()\r
+                            // new branch\r
+                            // Write header\r
+                            writeBranchTitle(refType, ref, "created", commits.size())\r
+                            writeCommitTable(commits)\r
+                            break\r
+                        case ReceiveCommand.Type.UPDATE:\r
+                            def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()\r
+                            commitCount += commits.size()\r
+                            // fast-forward branch commits table\r
+                            // Write header\r
+                            writeBranchTitle(refType, ref, "updated", commits.size())\r
+                            writeCommitTable(commits)\r
+                            break\r
+                        case ReceiveCommand.Type.UPDATE_NONFASTFORWARD:\r
+                            def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()\r
+                            commitCount += commits.size()\r
+                            // non-fast-forward branch commits table\r
+                            // Write header\r
+                            writeBranchTitle(refType, ref, "updated [NON fast-forward]", commits.size())\r
+                            writeCommitTable(commits)\r
+                            break\r
+                        case ReceiveCommand.Type.DELETE:\r
+                            // deleted branch/tag\r
+                            writeBranchDeletedTitle(refType, ref)\r
+                            break\r
+                        default:\r
+                            break\r
+                    }\r
+                }\r
+            }\r
+        }\r
+        writer.toString()\r
+    }\r
+\r
+}\r
+\r
+def mailWriter = new HtmlMailWriter()\r
+mailWriter.repository = r\r
+mailWriter.baseCommitUrl = baseCommitUrl\r
+mailWriter.baseBlobDiffUrl = baseBlobDiffUrl\r
+mailWriter.baseCommitDiffUrl = baseCommitDiffUrl\r
+mailWriter.commands = commands\r
+mailWriter.url = url\r
+mailWriter.mountParameters = gitblit.getBoolean(Keys.web.mountParameters, true)\r
+\r
+def content = mailWriter.write()\r
+\r
+// close the repository reference\r
+r.close()\r
+\r
+// tell Gitblit to send the message (Gitblit filters duplicate addresses)\r
+def repositoryName = repository.name.substring(0, repository.name.length() - 4)\r
+gitblit.sendHtmlMail("${emailprefix}[$repositoryName] ${userModel.displayName} pushed ${mailWriter.commitCount} commits",\r
+                     content,\r
+                     toAddresses)\r
index 0d37b44f96f188dade71a9d5dc36e5a76a8542ff..32f4c474a70d4653d8eac266184b64cbfd4cdcde 100644 (file)
@@ -2657,6 +2657,37 @@ public class GitBlit implements ServletContextListener {
                }\r
        }\r
 \r
+       /**\r
+        * Notify users by email of something.\r
+        * \r
+        * @param subject\r
+        * @param message\r
+        * @param toAddresses\r
+        */\r
+       public void sendHtmlMail(String subject, String message, Collection<String> toAddresses) {\r
+               this.sendHtmlMail(subject, message, toAddresses.toArray(new String[0]));\r
+       }\r
+\r
+       /**\r
+        * Notify users by email of something.\r
+        * \r
+        * @param subject\r
+        * @param message\r
+        * @param toAddresses\r
+        */\r
+       public void sendHtmlMail(String subject, String message, String... toAddresses) {\r
+               try {\r
+                       Message mail = mailExecutor.createMessage(toAddresses);\r
+                       if (mail != null) {\r
+                               mail.setSubject(subject);\r
+                               mail.setContent(message, "text/html");\r
+                               mailExecutor.queue(mail);\r
+                       }\r
+               } catch (MessagingException e) {\r
+                       logger.error("Messaging error", e);\r
+               }\r
+       }\r
+\r
        /**\r
         * Returns the descriptions/comments of the Gitblit config settings.\r
         * \r
index 3d3621dfac9925ba5866884d0805fb60379a2f14..47d20a4cadd28730ec66f476494fcbaae560c559 100644 (file)
@@ -69,6 +69,31 @@ public class GroovyScriptTest {
                }\r
        }\r
 \r
+       @Test\r
+       public void testSendHtmlMail() throws Exception {\r
+               MockGitblit gitblit = new MockGitblit();\r
+               MockLogger logger = new MockLogger();\r
+               MockClientLogger clientLogger = new MockClientLogger();\r
+               List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();\r
+               commands.add(new ReceiveCommand(ObjectId\r
+                               .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId\r
+                               .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master"));\r
+               commands.add(new ReceiveCommand(ObjectId\r
+                               .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId\r
+                               .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master2"));\r
+\r
+               RepositoryModel repository = GitBlit.self().getRepositoryModel("helloworld.git");\r
+               repository.mailingLists.add("list@helloworld.git");\r
+\r
+               test("sendmail-html.groovy", gitblit, logger, clientLogger, commands, repository);\r
+               assertEquals(1, logger.messages.size());\r
+               assertEquals(1, gitblit.messages.size());\r
+               MockMail m = gitblit.messages.get(0);\r
+               assertEquals(5, m.toAddresses.size());\r
+               assertTrue(m.message.contains("BIT"));\r
+               assertTrue(m.message.contains("<html>"));\r
+       }\r
+\r
        @Test\r
        public void testSendMail() throws Exception {\r
                MockGitblit gitblit = new MockGitblit();\r
@@ -296,6 +321,9 @@ public class GroovyScriptTest {
                public void sendMail(String subject, String message, Collection<String> toAddresses) {\r
                        messages.add(new MockMail(subject, message, toAddresses));\r
                }\r
+               public void sendHtmlMail(String subject, String message, Collection<String> toAddresses) {\r
+                       messages.add(new MockMail(subject, message, toAddresses));\r
+               }\r
        }\r
 \r
        class MockLogger {\r