您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

TicketNotifier.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. /*
  2. * Copyright 2014 gitblit.com.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.gitblit.tickets;
  17. import java.io.IOException;
  18. import java.io.InputStream;
  19. import java.text.DateFormat;
  20. import java.text.MessageFormat;
  21. import java.text.SimpleDateFormat;
  22. import java.util.ArrayList;
  23. import java.util.Arrays;
  24. import java.util.Collections;
  25. import java.util.HashMap;
  26. import java.util.HashSet;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.Set;
  30. import java.util.TreeMap;
  31. import java.util.TreeSet;
  32. import java.util.regex.Matcher;
  33. import java.util.regex.Pattern;
  34. import org.apache.commons.io.IOUtils;
  35. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  36. import org.eclipse.jgit.lib.Repository;
  37. import org.eclipse.jgit.revwalk.RevCommit;
  38. import org.slf4j.Logger;
  39. import org.slf4j.LoggerFactory;
  40. import com.gitblit.Constants;
  41. import com.gitblit.IStoredSettings;
  42. import com.gitblit.Keys;
  43. import com.gitblit.git.PatchsetCommand;
  44. import com.gitblit.manager.INotificationManager;
  45. import com.gitblit.manager.IRepositoryManager;
  46. import com.gitblit.manager.IRuntimeManager;
  47. import com.gitblit.manager.IUserManager;
  48. import com.gitblit.models.Mailing;
  49. import com.gitblit.models.PathModel.PathChangeModel;
  50. import com.gitblit.models.RepositoryModel;
  51. import com.gitblit.models.TicketModel;
  52. import com.gitblit.models.TicketModel.Change;
  53. import com.gitblit.models.TicketModel.Field;
  54. import com.gitblit.models.TicketModel.Patchset;
  55. import com.gitblit.models.TicketModel.Review;
  56. import com.gitblit.models.TicketModel.Status;
  57. import com.gitblit.models.UserModel;
  58. import com.gitblit.utils.ArrayUtils;
  59. import com.gitblit.utils.DiffUtils;
  60. import com.gitblit.utils.DiffUtils.DiffStat;
  61. import com.gitblit.utils.JGitUtils;
  62. import com.gitblit.utils.MarkdownUtils;
  63. import com.gitblit.utils.StringUtils;
  64. /**
  65. * Formats and queues ticket/patch notifications for dispatch to the
  66. * mail executor upon completion of a push or a ticket update. Messages are
  67. * created as Markdown and then transformed to html.
  68. *
  69. * @author James Moger
  70. *
  71. */
  72. public class TicketNotifier {
  73. protected final Map<Long, Mailing> queue = new TreeMap<Long, Mailing>();
  74. private final String SOFT_BRK = "\n";
  75. private final String HARD_BRK = "\n\n";
  76. private final String HR = "----\n\n";
  77. private final IStoredSettings settings;
  78. private final INotificationManager notificationManager;
  79. private final IUserManager userManager;
  80. private final IRepositoryManager repositoryManager;
  81. private final ITicketService ticketService;
  82. private final String addPattern = "<span style=\"color:darkgreen;\">+{0}</span>";
  83. private final String delPattern = "<span style=\"color:darkred;\">-{0}</span>";
  84. private final Logger log = LoggerFactory.getLogger(getClass());
  85. public TicketNotifier(
  86. IRuntimeManager runtimeManager,
  87. INotificationManager notificationManager,
  88. IUserManager userManager,
  89. IRepositoryManager repositoryManager,
  90. ITicketService ticketService) {
  91. this.settings = runtimeManager.getSettings();
  92. this.notificationManager = notificationManager;
  93. this.userManager = userManager;
  94. this.repositoryManager = repositoryManager;
  95. this.ticketService = ticketService;
  96. }
  97. public void sendAll() {
  98. for (Mailing mail : queue.values()) {
  99. notificationManager.send(mail);
  100. }
  101. }
  102. public void sendMailing(TicketModel ticket) {
  103. queueMailing(ticket);
  104. sendAll();
  105. }
  106. /**
  107. * Queues an update notification.
  108. *
  109. * @param ticket
  110. * @return a notification object used for testing
  111. */
  112. public Mailing queueMailing(TicketModel ticket) {
  113. try {
  114. // format notification message
  115. String markdown = formatLastChange(ticket);
  116. StringBuilder html = new StringBuilder();
  117. html.append("<head>");
  118. html.append(readStyle());
  119. html.append(readViewTicketAction(ticket));
  120. html.append("</head>");
  121. html.append("<body>");
  122. html.append(MarkdownUtils.transformGFM(settings, markdown, ticket.repository));
  123. html.append("</body>");
  124. Mailing mailing = Mailing.newHtml();
  125. mailing.from = getUserModel(ticket.updatedBy == null ? ticket.createdBy : ticket.updatedBy).getDisplayName();
  126. mailing.subject = getSubject(ticket);
  127. mailing.content = html.toString();
  128. mailing.id = "ticket." + ticket.number + "." + StringUtils.getSHA1(ticket.repository + ticket.number);
  129. setRecipients(ticket, mailing);
  130. queue.put(ticket.number, mailing);
  131. return mailing;
  132. } catch (Exception e) {
  133. log.error("failed to queue mailing for #{}", ticket.number, e);
  134. }
  135. return null;
  136. }
  137. protected String getSubject(TicketModel ticket) {
  138. Change lastChange = ticket.changes.get(ticket.changes.size() - 1);
  139. boolean newTicket = lastChange.isStatusChange() && ticket.changes.size() == 1;
  140. String re = newTicket ? "" : "Re: ";
  141. String subject = MessageFormat.format("{0}[{1}] {2} (#{3,number,0})",
  142. re, StringUtils.stripDotGit(ticket.repository), ticket.title, ticket.number);
  143. return subject;
  144. }
  145. protected String formatLastChange(TicketModel ticket) {
  146. Change lastChange = ticket.changes.get(ticket.changes.size() - 1);
  147. UserModel user = getUserModel(lastChange.author);
  148. // define the fields we do NOT want to see in an email notification
  149. Set<TicketModel.Field> fieldExclusions = new HashSet<TicketModel.Field>();
  150. fieldExclusions.addAll(Arrays.asList(Field.watchers, Field.voters));
  151. StringBuilder sb = new StringBuilder();
  152. boolean newTicket = lastChange.isStatusChange() && Status.New == lastChange.getStatus();
  153. boolean isFastForward = true;
  154. List<RevCommit> commits = null;
  155. DiffStat diffstat = null;
  156. String pattern;
  157. if (lastChange.hasPatchset()) {
  158. // patchset uploaded
  159. Patchset patchset = lastChange.patchset;
  160. String base = "";
  161. // determine the changed paths
  162. Repository repo = null;
  163. try {
  164. repo = repositoryManager.getRepository(ticket.repository);
  165. if (patchset.isFF() && (patchset.rev > 1)) {
  166. // fast-forward update, just show the new data
  167. isFastForward = true;
  168. Patchset prev = ticket.getPatchset(patchset.number, patchset.rev - 1);
  169. base = prev.tip;
  170. } else {
  171. // proposal OR non-fast-forward update
  172. isFastForward = false;
  173. base = patchset.base;
  174. }
  175. diffstat = DiffUtils.getDiffStat(repo, base, patchset.tip);
  176. commits = JGitUtils.getRevLog(repo, base, patchset.tip);
  177. } catch (Exception e) {
  178. log.error("failed to get changed paths", e);
  179. } finally {
  180. if (repo != null) {
  181. repo.close();
  182. }
  183. }
  184. String compareUrl = ticketService.getCompareUrl(ticket, base, patchset.tip);
  185. if (newTicket) {
  186. // new proposal
  187. pattern = "**{0}** is proposing a change.";
  188. sb.append(MessageFormat.format(pattern, user.getDisplayName()));
  189. fieldExclusions.add(Field.status);
  190. fieldExclusions.add(Field.title);
  191. fieldExclusions.add(Field.body);
  192. } else {
  193. // describe the patchset
  194. if (patchset.isFF()) {
  195. pattern = "**{0}** added {1} {2} to patchset {3}.";
  196. sb.append(MessageFormat.format(pattern, user.getDisplayName(), patchset.added, patchset.added == 1 ? "commit" : "commits", patchset.number));
  197. } else {
  198. pattern = "**{0}** uploaded patchset {1}. *({2})*";
  199. sb.append(MessageFormat.format(pattern, user.getDisplayName(), patchset.number, patchset.type.toString().toUpperCase()));
  200. }
  201. }
  202. sb.append(HARD_BRK);
  203. sb.append(MessageFormat.format("{0} {1}, {2} {3}, <span style=\"color:darkgreen;\">+{4} insertions</span>, <span style=\"color:darkred;\">-{5} deletions</span> from {6}. [compare]({7})",
  204. commits.size(), commits.size() == 1 ? "commit" : "commits",
  205. diffstat.paths.size(),
  206. diffstat.paths.size() == 1 ? "file" : "files",
  207. diffstat.getInsertions(),
  208. diffstat.getDeletions(),
  209. isFastForward ? "previous revision" : "merge base",
  210. compareUrl));
  211. // note commit additions on a rebase,if any
  212. switch (lastChange.patchset.type) {
  213. case Rebase:
  214. if (lastChange.patchset.added > 0) {
  215. sb.append(SOFT_BRK);
  216. sb.append(MessageFormat.format("{0} {1} added.", lastChange.patchset.added, lastChange.patchset.added == 1 ? "commit" : "commits"));
  217. }
  218. break;
  219. default:
  220. break;
  221. }
  222. sb.append(HARD_BRK);
  223. } else if (lastChange.isStatusChange()) {
  224. if (newTicket) {
  225. fieldExclusions.add(Field.status);
  226. fieldExclusions.add(Field.title);
  227. fieldExclusions.add(Field.body);
  228. pattern = "**{0}** created this ticket.";
  229. sb.append(MessageFormat.format(pattern, user.getDisplayName()));
  230. } else if (lastChange.hasField(Field.mergeSha)) {
  231. // closed by merged
  232. pattern = "**{0}** closed this ticket by merging {1} to {2}.";
  233. // identify patchset that closed the ticket
  234. String merged = ticket.mergeSha;
  235. for (Patchset patchset : ticket.getPatchsets()) {
  236. if (patchset.tip.equals(ticket.mergeSha)) {
  237. merged = patchset.toString();
  238. break;
  239. }
  240. }
  241. sb.append(MessageFormat.format(pattern, user.getDisplayName(), merged, ticket.mergeTo));
  242. } else {
  243. // workflow status change by user
  244. pattern = "**{0}** changed the status of this ticket to **{1}**.";
  245. sb.append(MessageFormat.format(pattern, user.getDisplayName(), lastChange.getStatus().toString().toUpperCase()));
  246. }
  247. sb.append(HARD_BRK);
  248. } else if (lastChange.hasReview()) {
  249. // review
  250. Review review = lastChange.review;
  251. pattern = "**{0}** has reviewed patchset {1,number,0} revision {2,number,0}.";
  252. sb.append(MessageFormat.format(pattern, user.getDisplayName(), review.patchset, review.rev));
  253. sb.append(HARD_BRK);
  254. String d = settings.getString(Keys.web.datestampShortFormat, "yyyy-MM-dd");
  255. String t = settings.getString(Keys.web.timeFormat, "HH:mm");
  256. DateFormat df = new SimpleDateFormat(d + " " + t);
  257. List<Change> reviews = ticket.getReviews(ticket.getPatchset(review.patchset, review.rev));
  258. sb.append("| Date | Reviewer | Score | Description |\n");
  259. sb.append("| :--- | :------------ | :---: | :----------- |\n");
  260. for (Change change : reviews) {
  261. String name = change.author;
  262. UserModel u = userManager.getUserModel(change.author);
  263. if (u != null) {
  264. name = u.getDisplayName();
  265. }
  266. String score;
  267. switch (change.review.score) {
  268. case approved:
  269. score = MessageFormat.format(addPattern, change.review.score.getValue());
  270. break;
  271. case vetoed:
  272. score = MessageFormat.format(delPattern, Math.abs(change.review.score.getValue()));
  273. break;
  274. default:
  275. score = "" + change.review.score.getValue();
  276. }
  277. String date = df.format(change.date);
  278. sb.append(String.format("| %1$s | %2$s | %3$s | %4$s |\n",
  279. date, name, score, change.review.score.toString()));
  280. }
  281. sb.append(HARD_BRK);
  282. } else if (lastChange.hasComment()) {
  283. // comment update
  284. sb.append(MessageFormat.format("**{0}** commented on this ticket.", user.getDisplayName()));
  285. sb.append(HARD_BRK);
  286. } else if (lastChange.hasReference()) {
  287. // reference update
  288. String type = "?";
  289. switch (lastChange.reference.getSourceType()) {
  290. case Commit: { type = "commit"; } break;
  291. case Ticket: { type = "ticket"; } break;
  292. default: { } break;
  293. }
  294. sb.append(MessageFormat.format("**{0}** referenced this ticket in {1} {2}", type, lastChange.toString()));
  295. sb.append(HARD_BRK);
  296. } else {
  297. // general update
  298. pattern = "**{0}** has updated this ticket.";
  299. sb.append(MessageFormat.format(pattern, user.getDisplayName()));
  300. sb.append(HARD_BRK);
  301. }
  302. // ticket link
  303. sb.append(MessageFormat.format("[view ticket {0,number,0}]({1})",
  304. ticket.number, ticketService.getTicketUrl(ticket)));
  305. sb.append(HARD_BRK);
  306. if (newTicket) {
  307. // ticket title
  308. sb.append(MessageFormat.format("### {0}", ticket.title));
  309. sb.append(HARD_BRK);
  310. // ticket description, on state change
  311. if (StringUtils.isEmpty(ticket.body)) {
  312. sb.append("<span style=\"color: #888;\">no description entered</span>");
  313. } else {
  314. sb.append(ticket.body);
  315. }
  316. sb.append(HARD_BRK);
  317. sb.append(HR);
  318. }
  319. // field changes
  320. if (lastChange.hasFieldChanges()) {
  321. Map<Field, String> filtered = new HashMap<Field, String>();
  322. for (Map.Entry<Field, String> fc : lastChange.fields.entrySet()) {
  323. if (!fieldExclusions.contains(fc.getKey())) {
  324. // field is included
  325. filtered.put(fc.getKey(), fc.getValue());
  326. }
  327. }
  328. // sort by field ordinal
  329. List<Field> fields = new ArrayList<Field>(filtered.keySet());
  330. Collections.sort(fields);
  331. if (filtered.size() > 0) {
  332. sb.append(HARD_BRK);
  333. sb.append("| Field Changes ||\n");
  334. sb.append("| ------------: | :----------- |\n");
  335. for (Field field : fields) {
  336. String value;
  337. if (filtered.get(field) == null) {
  338. value = "";
  339. } else {
  340. value = filtered.get(field).replace("\r\n", "<br/>").replace("\n", "<br/>").replace("|", "&#124;");
  341. }
  342. sb.append(String.format("| **%1$s:** | %2$s |\n", field.name(), value));
  343. }
  344. sb.append(HARD_BRK);
  345. }
  346. }
  347. // new comment
  348. if (lastChange.hasComment()) {
  349. sb.append(HR);
  350. sb.append(lastChange.comment.text);
  351. sb.append(HARD_BRK);
  352. }
  353. // insert the patchset details and review instructions
  354. if (lastChange.hasPatchset() && ticket.isOpen()) {
  355. if (commits != null && commits.size() > 0) {
  356. // append the commit list
  357. String title = isFastForward ? "Commits added to previous patchset revision" : "All commits in patchset";
  358. sb.append(MessageFormat.format("| {0} |||\n", title));
  359. sb.append("| SHA | Author | Title |\n");
  360. sb.append("| :-- | :----- | :---- |\n");
  361. for (RevCommit commit : commits) {
  362. sb.append(MessageFormat.format("| {0} | {1} | {2} |\n",
  363. commit.getName(), commit.getAuthorIdent().getName(),
  364. StringUtils.trimString(commit.getShortMessage(), Constants.LEN_SHORTLOG).replace("|", "&#124;")));
  365. }
  366. sb.append(HARD_BRK);
  367. }
  368. if (diffstat != null) {
  369. // append the changed path list
  370. String title = isFastForward ? "Files changed since previous patchset revision" : "All files changed in patchset";
  371. sb.append(MessageFormat.format("| {0} |||\n", title));
  372. sb.append("| :-- | :----------- | :-: |\n");
  373. for (PathChangeModel path : diffstat.paths) {
  374. String add = MessageFormat.format(addPattern, path.insertions);
  375. String del = MessageFormat.format(delPattern, path.deletions);
  376. String diff = null;
  377. switch (path.changeType) {
  378. case ADD:
  379. diff = add;
  380. break;
  381. case DELETE:
  382. diff = del;
  383. break;
  384. case MODIFY:
  385. if (path.insertions > 0 && path.deletions > 0) {
  386. // insertions & deletions
  387. diff = add + "/" + del;
  388. } else if (path.insertions > 0) {
  389. // just insertions
  390. diff = add;
  391. } else {
  392. // just deletions
  393. diff = del;
  394. }
  395. break;
  396. default:
  397. diff = path.changeType.name();
  398. break;
  399. }
  400. sb.append(MessageFormat.format("| {0} | {1} | {2} |\n",
  401. getChangeType(path.changeType), path.name, diff));
  402. }
  403. sb.append(HARD_BRK);
  404. }
  405. sb.append(formatPatchsetInstructions(ticket, lastChange.patchset));
  406. }
  407. return sb.toString();
  408. }
  409. protected String getChangeType(ChangeType type) {
  410. String style = null;
  411. switch (type) {
  412. case ADD:
  413. style = "color:darkgreen;";
  414. break;
  415. case COPY:
  416. style = "";
  417. break;
  418. case DELETE:
  419. style = "color:darkred;";
  420. break;
  421. case MODIFY:
  422. style = "";
  423. break;
  424. case RENAME:
  425. style = "";
  426. break;
  427. default:
  428. break;
  429. }
  430. String code = type.name().toUpperCase().substring(0, 1);
  431. if (style == null) {
  432. return code;
  433. } else {
  434. return MessageFormat.format("<strong><span style=\"{0}padding:2px;margin:2px;border:1px solid #ddd;\">{1}</span></strong>", style, code);
  435. }
  436. }
  437. /**
  438. * Generates patchset review instructions for command-line git
  439. *
  440. * @param patchset
  441. * @return instructions
  442. */
  443. protected String formatPatchsetInstructions(TicketModel ticket, Patchset patchset) {
  444. String canonicalUrl = settings.getString(Keys.web.canonicalUrl, "https://localhost:8443");
  445. String repositoryUrl = canonicalUrl + Constants.R_PATH + ticket.repository;
  446. String ticketBranch = Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticket.number));
  447. String patchsetBranch = PatchsetCommand.getPatchsetBranch(ticket.number, patchset.number);
  448. String reviewBranch = PatchsetCommand.getReviewBranch(ticket.number);
  449. String instructions = readResource("commands.md");
  450. instructions = instructions.replace("${ticketId}", "" + ticket.number);
  451. instructions = instructions.replace("${patchset}", "" + patchset.number);
  452. instructions = instructions.replace("${repositoryUrl}", repositoryUrl);
  453. instructions = instructions.replace("${ticketRef}", ticketBranch);
  454. instructions = instructions.replace("${patchsetRef}", patchsetBranch);
  455. instructions = instructions.replace("${reviewBranch}", reviewBranch);
  456. instructions = instructions.replace("${ticketBranch}", ticketBranch);
  457. return instructions;
  458. }
  459. /**
  460. * Gets the usermodel for the username. Creates a temp model, if required.
  461. *
  462. * @param username
  463. * @return a usermodel
  464. */
  465. protected UserModel getUserModel(String username) {
  466. UserModel user = userManager.getUserModel(username);
  467. if (user == null) {
  468. // create a temporary user model (for unit tests)
  469. user = new UserModel(username);
  470. }
  471. return user;
  472. }
  473. /**
  474. * Set the proper recipients for a ticket.
  475. *
  476. * @param ticket
  477. * @param mailing
  478. */
  479. protected void setRecipients(TicketModel ticket, Mailing mailing) {
  480. RepositoryModel repository = repositoryManager.getRepositoryModel(ticket.repository);
  481. //
  482. // Direct TO recipients
  483. // reporter & responsible
  484. //
  485. Set<String> tos = new TreeSet<String>();
  486. tos.add(ticket.createdBy);
  487. if (!StringUtils.isEmpty(ticket.responsible)) {
  488. tos.add(ticket.responsible);
  489. }
  490. Set<String> toAddresses = new TreeSet<String>();
  491. for (String name : tos) {
  492. UserModel user = userManager.getUserModel(name);
  493. if (user != null && !user.disabled) {
  494. if (!StringUtils.isEmpty(user.emailAddress)) {
  495. if (user.canView(repository)) {
  496. toAddresses.add(user.emailAddress);
  497. } else {
  498. log.warn("ticket {}-{}: {} can not receive notification", repository.name, ticket.number, user.username);
  499. }
  500. }
  501. }
  502. }
  503. //
  504. // CC recipients
  505. //
  506. Set<String> ccs = new TreeSet<String>();
  507. // repository owners
  508. if (!ArrayUtils.isEmpty(repository.owners)) {
  509. ccs.addAll(repository.owners);
  510. }
  511. // cc users mentioned in last comment
  512. Change lastChange = ticket.changes.get(ticket.changes.size() - 1);
  513. if (lastChange.hasComment()) {
  514. Pattern p = Pattern.compile(Constants.REGEX_TICKET_MENTION);
  515. Matcher m = p.matcher(lastChange.comment.text);
  516. while (m.find()) {
  517. String username = m.group("user");
  518. ccs.add(username);
  519. }
  520. }
  521. // cc users who are watching the ticket
  522. ccs.addAll(ticket.getWatchers());
  523. // TODO cc users who are watching the repository
  524. Set<String> ccAddresses = new TreeSet<String>();
  525. for (String name : ccs) {
  526. UserModel user = userManager.getUserModel(name);
  527. if (user != null && !user.disabled) {
  528. if (!StringUtils.isEmpty(user.emailAddress)) {
  529. if (user.canView(repository)) {
  530. ccAddresses.add(user.emailAddress);
  531. } else {
  532. log.warn("ticket {}-{}: {} can not receive notification", repository.name, ticket.number, user.username);
  533. }
  534. }
  535. }
  536. }
  537. // cc repository mailing list addresses
  538. if (!ArrayUtils.isEmpty(repository.mailingLists)) {
  539. ccAddresses.addAll(repository.mailingLists);
  540. }
  541. ccAddresses.addAll(settings.getStrings(Keys.mail.mailingLists));
  542. // respect the author's email preference
  543. UserModel lastAuthor = userManager.getUserModel(lastChange.author);
  544. if (lastAuthor != null && !lastAuthor.getPreferences().isEmailMeOnMyTicketChanges()) {
  545. toAddresses.remove(lastAuthor.emailAddress);
  546. ccAddresses.remove(lastAuthor.emailAddress);
  547. }
  548. mailing.setRecipients(toAddresses);
  549. mailing.setCCs(ccAddresses);
  550. }
  551. protected String readStyle() {
  552. StringBuilder sb = new StringBuilder();
  553. sb.append("<style>\n");
  554. sb.append(readResource("email.css"));
  555. sb.append("</style>\n");
  556. return sb.toString();
  557. }
  558. protected String readViewTicketAction(TicketModel ticket) {
  559. String action = readResource("viewTicket.html");
  560. action = action.replace("${url}", ticketService.getTicketUrl(ticket));
  561. return action;
  562. }
  563. protected String readResource(String resource) {
  564. StringBuilder sb = new StringBuilder();
  565. InputStream is = null;
  566. try {
  567. is = getClass().getResourceAsStream(resource);
  568. List<String> lines = IOUtils.readLines(is);
  569. for (String line : lines) {
  570. sb.append(line).append('\n');
  571. }
  572. } catch (IOException e) {
  573. } finally {
  574. if (is != null) {
  575. try {
  576. is.close();
  577. } catch (IOException e) {
  578. }
  579. }
  580. }
  581. return sb.toString();
  582. }
  583. }